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.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping;
import com.google.javascript.jscomp.JsMessage.Hash;
import com.google.javascript.jscomp.JsMessage.Part;
import com.google.javascript.jscomp.JsMessage.PlaceholderFormatException;
import com.google.javascript.jscomp.JsMessage.PlaceholderReference;
import com.google.javascript.jscomp.JsMessage.StringPart;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.base.format.SimpleFormat;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jspecify.nullness.Nullable;

/**
 * Locates JS code that is intended to declare localizable messages.
 *
 * 

It passes each found message to either {@link * JsMessageVisitor#processJsMessageDefinition(JsMessageDefinition)} for {@code goog.getMsg()} calls * or {@link JsMessageVisitor#processIcuTemplateDefinition(IcuTemplateDefinition)} for {@code * goog.i18n.messages.declareIcuTemplate()} calls. */ @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 ICU_MSG_FUNCTION_NAME = "declareIcuTemplate"; private static final String ICU_MSG_FUNCTION_QNAME = "goog.i18n.messages." + ICU_MSG_FUNCTION_NAME; private static final String MSG_FALLBACK_FUNCTION_NAME = "goog.getMsgWithFallback"; /** * Identifies a message with a specific ID which doesn't get extracted from the JS code containing * its declaration. * *


   *   // A message with ID 1234 whose original definition comes from some source other than this
   *   // JS code.
   *   const MSG_EXTERNAL_1234 = goog.getMsg("Some message.");
   * 
*/ private static final String MSG_EXTERNAL_PREFIX = "MSG_EXTERNAL_"; 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."); public 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.error( "JSC_MSG_ORPHANED_NODE", "{0}() function may be used only with MSG_* property or variable"); public static final DiagnosticType MESSAGE_NOT_INITIALIZED_CORRECTLY = DiagnosticType.warning( "JSC_MSG_NOT_INITIALIZED_CORRECTLY", "Message must be initialized using a call to " + MSG_FUNCTION_NAME + " or " + ICU_MSG_FUNCTION_QNAME); public 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)); public static final DiagnosticType FALLBACK_ARG_ERROR = DiagnosticType.error( "JSC_MSG_FALLBACK_ARG_ERROR", "Could not find message entry for fallback argument {0}"); static final String MSG_PREFIX = "MSG_"; /** * ScopedAliases pass transforms the goog.scope declarations to have a unique id as prefix. * *

After '$jscomp$scope$' expect some sequence of digits and dollar signs ending in '$MSG_ */ private static final Pattern SCOPED_ALIASES_PREFIX_PATTERN = Pattern.compile("\\$jscomp\\$scope\\$\\S+\\$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 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 LinkedHashMap<>(); /** Track unnamed messages by Var, not string, as they are not guaranteed to be globally unique */ private final Map unnamedMessages = new LinkedHashMap<>(); /** * Set 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 LinkedHashSet<>(); /** * Creates JS message visitor. * * @param compiler the compiler instance * @param idGenerator generator that used for creating unique ID for the message */ protected JsMessageVisitor(AbstractCompiler compiler, JsMessage.IdGenerator idGenerator) { this.compiler = compiler; this.idGenerator = idGenerator; // 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, MESSAGE_NODE_IS_ORPHANED, msgNode.getFirstChild().getQualifiedName())); } } @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(); final String originalMessageKey; String possiblyObfuscatedMessageKey; final Node msgNode; final JSDocInfo jsDocInfo; switch (node.getToken()) { case NAME: // Case: `var MSG_HELLO = 'Message';` if (parent == null || !NodeUtil.isNameDeclaration(parent)) { return; } possiblyObfuscatedMessageKey = node.getString(); originalMessageKey = node.getOriginalName(); msgNode = node.getFirstChild(); jsDocInfo = parent.getJSDocInfo(); break; case ASSIGN: // Case: `somenamespace.someclass.MSG_HELLO = 'Message';` Node getProp = node.getFirstChild(); if (!getProp.isGetProp()) { return; } possiblyObfuscatedMessageKey = getProp.getString(); originalMessageKey = getProp.getOriginalName(); msgNode = node.getLastChild(); jsDocInfo = node.getJSDocInfo(); break; case STRING_KEY: // Case: `var t = {MSG_HELLO: 'Message'}`; if (node.isQuotedStringKey() || !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); possiblyObfuscatedMessageKey = node.getString(); originalMessageKey = node.getOriginalName(); msgNode = node.getFirstChild(); jsDocInfo = node.getJSDocInfo(); break; case MEMBER_FIELD_DEF: // Case: `class Foo { MSG_HELLO = 'Message'; }` possiblyObfuscatedMessageKey = node.getString(); originalMessageKey = node.getOriginalName(); msgNode = node.getFirstChild(); jsDocInfo = node.getJSDocInfo(); break; default: return; } String messageKeyFromLhs = originalMessageKey != null ? originalMessageKey : possiblyObfuscatedMessageKey; // 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 msgNodeIsACall = msgNode != null && msgNode.isCall(); if (!isMessageName(messageKeyFromLhs)) { return; } if (msgNode == null) { compiler.report(JSError.make(node, MESSAGE_HAS_NO_VALUE, messageKeyFromLhs)); 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 (msgNodeIsACall) { googMsgNodes.remove(msgNode); } else { compiler.report(JSError.make(node, MESSAGE_NOT_INITIALIZED_CORRECTLY)); return; } OriginalMapping mapping = compiler.getSourceMapping(traversal.getSourceName(), node.getLineno(), node.getCharno()); final String sourceName; if (mapping != null) { sourceName = mapping.getOriginalFile() + ":" + mapping.getLineNumber(); } else { sourceName = traversal.getSourceName() + ":" + node.getLineno(); } checkState(msgNode.isCall()); final Node fnNameNode = msgNode.getFirstChild(); try { if (isDeclareIcuTemplateCallee(fnNameNode)) { final IcuTemplateDefinition icuTemplateDefinition = extractIcuTemplateDefinition(msgNode, jsDocInfo, messageKeyFromLhs, sourceName); final JsMessage extractedMessage = icuTemplateDefinition.getMessage(); trackMessage(traversal, possiblyObfuscatedMessageKey, msgNode, extractedMessage); reportErrorIfEmptyMessage(node, extractedMessage); processIcuTemplateDefinition(icuTemplateDefinition); } else if (fnNameNode.matchesQualifiedName(MSG_FUNCTION_NAME)) { final JsMessageDefinition jsMessageDefinition = extractJsMessageDefinition(msgNode, jsDocInfo, messageKeyFromLhs, sourceName); final JsMessage extractedMessage = jsMessageDefinition.getMessage(); trackMessage(traversal, possiblyObfuscatedMessageKey, msgNode, extractedMessage); reportErrorIfEmptyMessage(node, extractedMessage); // goog.getMsg() calls are required to have `@desc` unless they are external. final String desc = extractedMessage.getDesc(); if ((desc == null || desc.trim().isEmpty()) && !extractedMessage.isExternal()) { compiler.report( JSError.make(node, MESSAGE_HAS_NO_DESCRIPTION, extractedMessage.getKey())); } processJsMessageDefinition(jsMessageDefinition); } else { compiler.report( JSError.make( msgNode, MESSAGE_TREE_MALFORMED, "Message must be initialized using a call to " + MSG_FUNCTION_NAME + " or " + ICU_MSG_FUNCTION_NAME + " (from goog.i18n.messages).")); } } catch (MalformedException ex) { compiler.report(JSError.make(ex.getNode(), MESSAGE_TREE_MALFORMED, ex.getMessage())); } } private boolean isDeclareIcuTemplateCallee(Node calleeNode) { // NOTE: We're requiring that the imported ICU template function name match its original name. // This is an intentional limitation, since it should be less confusing to users as well as // to this program. // TODO(bradfordcsmith): Make this check more robust, possibly building on ModuleMetadata or // ProcessClosurePrimitives. final String qualifiedName = calleeNode.getQualifiedName(); return qualifiedName != null && qualifiedName.endsWith(ICU_MSG_FUNCTION_NAME); } interface InputForBuildJsMsg { String getMessageKeyFromLhs(); String getSourceName(); String getMessageText(); ImmutableList getMessageParts(); Map getPlaceholderExampleMap(); Map getPlaceholderOriginalCodeMap(); String getDescription(); String getMeaning(); String getAlternateMessageId(); } private JsMessage buildJsMessage(InputForBuildJsMsg input) { final ImmutableList messageParts = input.getMessageParts(); final String messageKeyFromLhs = input.getMessageKeyFromLhs(); // non-null for `MSG_EXTERNAL_12345` final String externalMessageId = getExternalMessageId(messageKeyFromLhs); final boolean isAnonymous; final boolean isExternal; final String messageId; final String messageKeyFinal; if (externalMessageId != null) { // MSG_EXTERNAL_12345 = ... isAnonymous = false; isExternal = true; messageId = externalMessageId; messageKeyFinal = messageKeyFromLhs; } else { isExternal = false; // We will need to generate the message ID from a combination of the message text and // its "meaning". If the code explicitly specifies a "meaning" string we'll use that, // otherwise we'll use the message key as the meaning String meaningForIdGeneration = input.getMeaning(); if (isUnnamedMessageName(messageKeyFromLhs)) { // MSG_UNNAMED_XXXX = goog.getMsg(....); // JS code that is automatically generated uses this, since it is harder for it to create // message variable names that are guaranteed to be unique. isAnonymous = true; messageKeyFinal = generateKeyFromMessageText(input.getMessageText()); if (meaningForIdGeneration == null) { meaningForIdGeneration = messageKeyFinal; } } else { isAnonymous = false; messageKeyFinal = messageKeyFromLhs; if (meaningForIdGeneration == null) { // Transpilation of goog.scope() may have added a prefix onto the variable name, // which we need to strip off when we treat it as a "meaning" for ID generation purposes. // Otherwise, the message ID would change if a goog.scope() call were added or removed. meaningForIdGeneration = removeScopedAliasesPrefix(messageKeyFromLhs); } } messageId = idGenerator == null ? meaningForIdGeneration : idGenerator.generateId(meaningForIdGeneration, messageParts); } return new JsMessage.Builder() .appendParts(messageParts) .setPlaceholderNameToExampleMap(input.getPlaceholderExampleMap()) .setPlaceholderNameToOriginalCodeMap(input.getPlaceholderOriginalCodeMap()) .setIsAnonymous(isAnonymous) .setIsExternalMsg(isExternal) .setKey(messageKeyFinal) .setSourceName(input.getSourceName()) .setDesc(input.getDescription()) // NOTE: JsMessageXmbWriter does NOT just take this value and write it to the XMB // file as the message meaning. It has its own code that defaults a null value to // the value of the key field, then prefixes that with a project ID, if any. // The intention of that seems to have been to make sure the meaning value in the XMB // file matches what was actually used by the ID generator above. .setMeaning(input.getMeaning()) .setAlternateId(input.getAlternateMessageId()) .setId(messageId) .build(); } private void trackMessage( NodeTraversal traversal, String possiblyObfuscatedMessageKey, Node msgNode, JsMessage extractedMessage) { // If asked to check named internal messages. if (!extractedMessage.isAnonymous() && !extractedMessage.isExternal()) { checkIfMessageDuplicated(extractedMessage.getKey(), msgNode); } if (extractedMessage.isAnonymous()) { trackUnnamedMessage(traversal, extractedMessage, possiblyObfuscatedMessageKey); } else { trackNormalMessage(extractedMessage, extractedMessage.getKey(), msgNode); } } private void reportErrorIfEmptyMessage(Node node, JsMessage extractedMessage) { 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, extractedMessage.getKey())); } } /** * Extracts an external message ID from the message key, if it contains one. * * @param messageKey the message key (usually the variable or property name) * @return the external ID if it is found, otherwise `null` */ public static @Nullable String getExternalMessageId(String messageKey) { if (messageKey.startsWith(MSG_EXTERNAL_PREFIX)) { int start = MSG_EXTERNAL_PREFIX.length(); int end = start; for (; end < messageKey.length(); end++) { char c = messageKey.charAt(end); if (c > '9' || c < '0') { break; } } if (end > start) { return messageKey.substring(start, end); } } return null; } private String generateKeyFromMessageText(String msgText) { long nonnegativeHash = Long.MAX_VALUE & Hash.hash64(msgText); return MSG_PREFIX + Ascii.toUpperCase(Long.toString(nonnegativeHash, 36)); } private void collectGetMsgCall(NodeTraversal traversal, Node call) { if (!call.isCall()) { return; } // goog.getMsg() final Node callee = call.getFirstChild(); if (callee.matchesQualifiedName(MSG_FUNCTION_NAME) || isDeclareIcuTemplateCallee(callee)) { googMsgNodes.add(call); } else if (callee.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 trackNormalMessage(JsMessage message, String msgName, Node msgNode) { MessageLocation location = new MessageLocation(message, msgNode); messageNames.put(msgName, location); } /** * Track an unnamed message for later retrieval. * *

This is used for figuring out message fallbacks. Message duplicates are allowed for unnamed * messages. */ private void trackUnnamedMessage(NodeTraversal t, JsMessage message, String msgNameInScope) { Var var = t.getScope().getVar(msgNameInScope); 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.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 // ConcretizeStaticInheritanceForInlining. 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 unnamed message */ private @Nullable JsMessage getTrackedUnnamedMessage(NodeTraversal t, String msgNameInScope) { Var var = t.getScope().getVar(msgNameInScope); if (var != null) { return unnamedMessages.get(var); } return null; } /** Get a previously tracked message. */ private @Nullable JsMessage getTrackedNormalMessage(String msgName) { MessageLocation location = messageNames.get(msgName); return location == null ? null : location.message; } /** * 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()))); } } /** * 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 node is not a string literal or concatenation of them */ private static String extractStringFromStringExprNode(Node node) throws MalformedException { switch (node.getToken()) { case STRINGLIT: 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.getFirstChild(); child != null; child = child.getNext()) { sb.append(extractStringFromStringExprNode(child)); } return sb.toString(); default: throw new MalformedException("literal string or concatenation expected", node); } } /** * Extract data from a call to {@code goog.getMsg(...)}. * *

Here's an example with all possible arguments and options present. * *


   *   const MSG_HELLO =
   *       goog.getMsg(
   *           'Hello, {$name}', // message template string
   *           { name: getName() }, // placeholder replacement values
   *           { // options bag
   *             html: false,
   *             unescapeHtmlEntities: true,
   *             example: { name: 'George' },
   *             original_code: { name: 'getName()' },
   *           });
   * 
* * @throws MalformedException if the code does not match the expected format. */ private JsMessageDefinition extractJsMessageDefinition( Node msgNode, JSDocInfo jsDocInfo, String messageKeyFromLhs, String sourceName) throws MalformedException { // Extract data from a call to `goog.getMsg(...)` // TODO(bradfordcsmith): Add these three fields to the options bag argument for goog.getMsg(). // Specifying them there instead of in annotations will make them available to the uncompiled // `goog.getMsg()` call during runtime, which could enable runtime lookup of translated messages // in uncompiled code. final @Nullable String description; final @Nullable String meaning; final @Nullable String alternateMessageId; if (jsDocInfo == null) { description = null; meaning = null; alternateMessageId = null; } else { description = jsDocInfo.getDescription(); meaning = jsDocInfo.getMeaning(); alternateMessageId = jsDocInfo.getAlternateMessageId(); } // first child of the call is the `goog.getMsg` name // second is the message string value final Node msgTextNode = msgNode.getSecondChild(); if (msgTextNode == null) { throw new MalformedException("Message string literal expected", msgNode); } // third is the optional object literal mapping placeholder names to value expressions final Node valuesObjLit = msgTextNode.getNext(); // fourth is the optional options-bag argument final Node optionsBagArgument = valuesObjLit == null ? null : valuesObjLit.getNext(); final Node unexpectedArgument = optionsBagArgument == null ? null : optionsBagArgument.getNext(); if (unexpectedArgument != null) { throw new MalformedException("too many arguments", unexpectedArgument); } // Extract and parse the message text // The message string can be a string literal or a concatenation of string literals. // We want the whole thing as a single string here. String msgTextString = extractStringFromStringExprNode(msgTextNode); final GoogGetMsgParsedText googGetMsgParsedText; try { googGetMsgParsedText = extractGoogGetMsgParsedText(msgTextString); } catch (PlaceholderFormatException e) { throw new MalformedException(e.getMessage(), msgTextNode); } // Extract the placeholder values object // NOTE: The extract method returns an effectively "empty" value for `null` final ObjectLiteralMap placeholderObjectLiteralMap = extractObjectLiteralMap(valuesObjLit); // Confirm that we can find a value map entry for every placeholder name referenced in the text. final ImmutableSet placeholderNamesFromText = googGetMsgParsedText.getPlaceholderNames(); placeholderObjectLiteralMap.checkForRequiredKeys( placeholderNamesFromText, placeholderName -> new MalformedException( "Unrecognized message placeholder referenced: " + placeholderName, msgTextNode)); // Confirm that every placeholder name for which we have a value is actually referenced in the // text. placeholderObjectLiteralMap.checkForUnexpectedKeys( placeholderNamesFromText, placeholderName -> "Unused message placeholder: " + placeholderName); // Get a map from placeholder name to Node value final ImmutableMap placeholderValuesMap = placeholderObjectLiteralMap.extractAsValueMap(); // Extract the Options bag data // NOTE: the extract method returns an "empty" value for `null` final JsMessageOptions jsMessageOptions = extractJsMessageOptions(optionsBagArgument); // Confirm that any example or orignal_code placeholder information we have refers only to // placeholder names that appear in the message text. jsMessageOptions.checkForUnknownPlaceholders(placeholderNamesFromText); final JsMessage jsExtractedMessage = buildJsMessage( new InputForBuildJsMsg() { @Override public String getMessageKeyFromLhs() { return messageKeyFromLhs; } @Override public String getSourceName() { return sourceName; } @Override public String getMessageText() { return googGetMsgParsedText.getText(); } @Override public ImmutableList getMessageParts() { return googGetMsgParsedText.getParts(); } @Override public ImmutableMap getPlaceholderExampleMap() { return jsMessageOptions.getPlaceholderExampleMap(); } @Override public ImmutableMap getPlaceholderOriginalCodeMap() { return jsMessageOptions.getPlaceholderOriginalCodeMap(); } @Override public String getDescription() { return description; } @Override public String getMeaning() { return meaning; } @Override public String getAlternateMessageId() { return alternateMessageId; } }); return new JsMessageDefinition() { @Override public JsMessage getMessage() { return jsExtractedMessage; } @Override public Node getMessageNode() { return msgNode; } @Override public Node getTemplateTextNode() { return msgTextNode; } @Override public @Nullable Node getPlaceholderValuesNode() { return valuesObjLit; } @Override public ImmutableMap getPlaceholderValueMap() { return placeholderValuesMap; } @Override public boolean shouldEscapeLessThan() { return jsMessageOptions.isEscapeLessThan(); } @Override public boolean shouldUnescapeHtmlEntities() { return jsMessageOptions.isUnescapeHtmlEntities(); } }; } /** * Extract message data from a `declareIcuTemplate()` call. * *

Example: * *


   *   const MSG_ICU_EXAMPLE =
   *       declareIcuTemplate(
   *           `{NUM_PEOPLE, plural, offset:1
   *           =0 {I see {BEGIN_BOLD}no one at all{END_BOLD} in {INTERPOLATION_2}.}
   *           =1 {I see {INTERPOLATION_1} in {INTERPOLATION_2}.}
   *           =2 {I see {INTERPOLATION_1} and one other person in {INTERPOLATION_2}.}
   *           other {I see {INTERPOLATION_1} and some other people in {INTERPOLATION_2}.}
   *       }`,
   *       {
   *         description: 'required message description here',
   *         meaning: 'optional meaning here',
   *         alternate_message_id: 'optional alternate message ID here',
   *         // optional
   *         original_code: {
   *           'INTERPOLATION_1': '{{getPerson()}}',
   *           'INTERPOLATION_2': '{{getPlaceName()}}',
   *         },
   *         // optional
   *         example: {
   *           'INTERPOLATION_1': 'Jane Doe',
   *           'INTERPOLATION_2': 'Paris, France',
   *         }
   *       });
   * 
*/ private IcuTemplateDefinition extractIcuTemplateDefinition( Node msgNode, JSDocInfo jsDocInfo, String messageKeyFromLhs, String sourceName) throws MalformedException { if (jsDocInfo != null) { // For declareIcuTemplateData() it is not valid to use the @desc, @meaning, and // @alternateMessageId annotations. // Instead, that information should go into the options bag parameter. if (jsDocInfo.getAlternateMessageId() != null) { throw new MalformedException( "Use the 'alternate_message_id' option, not the '@alternateMessageId' annotation", msgNode); } if (jsDocInfo.getDescription() != null) { throw new MalformedException( "Use the 'description' option, not the '@desc' annotation", msgNode); } if (jsDocInfo.getMeaning() != null) { throw new MalformedException( "Use the 'meaning' option, not the '@meaning' annotation", msgNode); } } // The first argument is the message string final Node stringLiteralExpression = msgNode.getSecondChild(); if (stringLiteralExpression == null) { throw new MalformedException("message string argument expected", msgNode); } final IcuMessageTemplateString icuMessageTemplateString = extractIcuMessageTemplateString(stringLiteralExpression); // The second argument is the options bag final Node optionsBagNode = stringLiteralExpression.getNext(); if (optionsBagNode == null) { throw new MalformedException("options argument expected", msgNode); } final IcuTemplateOptions icuTemplateOptions = extractIcuTemplateOptions(optionsBagNode); // Parsing of the template string into parts depends on which placeholders are mentioned // in the options. Placeholders that do not have example or original_code text will not be // extracted from the template as separate parts. final ExtractedIcuTemplateParts extractedIcuTemplateParts = icuMessageTemplateString.extractParts(icuTemplateOptions.getPlaceholderNames()); icuTemplateOptions.checkForUnknownPlaceholders( extractedIcuTemplateParts.extractedPlaceholderNames); // There shouldn't be another argument final Node unexpectedArgument = optionsBagNode.getNext(); if (unexpectedArgument != null) { throw new MalformedException("too many arguments", unexpectedArgument); } final JsMessage extractedMessage = buildJsMessage( new InputForBuildJsMsg() { @Override public String getMessageKeyFromLhs() { return messageKeyFromLhs; } @Override public String getSourceName() { return sourceName; } @Override public String getMessageText() { return icuMessageTemplateString.template; } @Override public ImmutableList getMessageParts() { return extractedIcuTemplateParts.extractedParts; } @Override public ImmutableMap getPlaceholderExampleMap() { return icuTemplateOptions.getPlaceholderExampleMap(); } @Override public ImmutableMap getPlaceholderOriginalCodeMap() { return icuTemplateOptions.getPlaceholderOriginalCodeMap(); } @Override public String getDescription() { return icuTemplateOptions.getDescription(); } @Override public String getMeaning() { return icuTemplateOptions.getMeaning(); } @Override public String getAlternateMessageId() { return icuTemplateOptions.getAlternateMessageId(); } }); return new IcuTemplateDefinition() { @Override public JsMessage getMessage() { return extractedMessage; } @Override public Node getMessageNode() { return msgNode; } @Override public Node getTemplateTextNode() { return stringLiteralExpression; } }; } private static final Pattern ICU_PLACEHOLDER_RE = Pattern.compile("\\{([A-Z_0-9]+)\\}"); @VisibleForTesting static final class IcuMessageTemplateString { private final String template; IcuMessageTemplateString(String template) { this.template = template; } /** * Split the template string into plain string parts and placeholders. * *

Although this method will recognize all ICU placeholders in the string ("{NAME}"), it will * only extract as separate parts those that are named in {@code placholderNamesToExtract}. * Those are the ones for which we will need to create placeholder elements, so we can attach * example text or original code snippets to them when we put the message into an XMB file. */ ExtractedIcuTemplateParts extractParts(Set placholderNamesToExtract) { ImmutableSet.Builder extractedPlaceholderNames = ImmutableSet.builder(); ImmutableList.Builder partsBuilder = ImmutableList.builder(); final Matcher matcher = ICU_PLACEHOLDER_RE.matcher(template); int remainingTemplateStartIndex = 0; while (matcher.find()) { String placeholderName = matcher.group(1); if (placholderNamesToExtract.contains(placeholderName)) { extractedPlaceholderNames.add(placeholderName); String nonPlaceholderPrefix = template.substring(remainingTemplateStartIndex, matcher.start()); remainingTemplateStartIndex = matcher.end(); partsBuilder.add(StringPart.create(nonPlaceholderPrefix)); partsBuilder.add(PlaceholderReference.createForCanonicalName(placeholderName)); } // else we have no need to create a separate part for this placeholder. } if (remainingTemplateStartIndex < template.length()) { final String remainingTemplate = template.substring(remainingTemplateStartIndex); partsBuilder.add(StringPart.create(remainingTemplate)); } return new ExtractedIcuTemplateParts(extractedPlaceholderNames.build(), partsBuilder.build()); } } static class ExtractedIcuTemplateParts { final ImmutableSet extractedPlaceholderNames; final ImmutableList extractedParts; ExtractedIcuTemplateParts( ImmutableSet extractedPlaceholderNames, ImmutableList extractedParts) { this.extractedPlaceholderNames = extractedPlaceholderNames; this.extractedParts = extractedParts; } } private IcuMessageTemplateString extractIcuMessageTemplateString(Node stringExpression) throws MalformedException { final String templateString = extractStringFromStringExprNode(stringExpression); return new IcuMessageTemplateString(templateString); } private interface JsMessageOptions { // Replace `'<'` with `'<'` in the message. boolean isEscapeLessThan(); // Replace these escaped entities with their literal characters in the message // (Overrides escapeLessThan) // '<' -> '<' // '>' -> '>' // ''' -> "'" // '"' -> '"' // '&' -> '&' boolean isUnescapeHtmlEntities(); ImmutableMap getPlaceholderExampleMap(); ImmutableMap getPlaceholderOriginalCodeMap(); void checkForUnknownPlaceholders(Set knownPlaceholders) throws MalformedException; } private static final ImmutableSet MESSAGE_OPTION_NAMES = ImmutableSet.of("html", "unescapeHtmlEntities", "example", "original_code"); private JsMessageOptions extractJsMessageOptions(@Nullable Node optionsBag) throws MalformedException { ObjectLiteralMap objectLiteralMap = extractObjectLiteralMap(optionsBag); objectLiteralMap.checkForUnexpectedKeys( MESSAGE_OPTION_NAMES, optionName -> "Unknown option: " + optionName); final boolean isEscapeLessThan = objectLiteralMap.getBooleanValueOrFalse("html"); final boolean isUnescapeHtmlEntities = objectLiteralMap.getBooleanValueOrFalse("unescapeHtmlEntities"); final Node exampleValueNode = objectLiteralMap.getValueNode("example"); final ObjectLiteralMap exampleObjectLiteralMap = extractObjectLiteralMap(exampleValueNode); final ImmutableMap placeholderExamplesMap = exampleObjectLiteralMap.extractAsStringToStringMap(); final Node originalCodeValueNode = objectLiteralMap.getValueNode("original_code"); final ObjectLiteralMap originalCodeObjectLiteralMap = extractObjectLiteralMap(originalCodeValueNode); final ImmutableMap placeholderOriginalCodeMap = originalCodeObjectLiteralMap.extractAsStringToStringMap(); // NOTE: The getX() methods below should all do little to no computation. // In particular, all checking for MalformedExceptions must be done before creating this object. return new JsMessageOptions() { @Override public boolean isEscapeLessThan() { return isEscapeLessThan; } @Override public boolean isUnescapeHtmlEntities() { return isUnescapeHtmlEntities; } @Override public ImmutableMap getPlaceholderExampleMap() { return placeholderExamplesMap; } @Override public ImmutableMap getPlaceholderOriginalCodeMap() { return placeholderOriginalCodeMap; } @Override public void checkForUnknownPlaceholders(Set knownPlaceholders) throws MalformedException { exampleObjectLiteralMap.checkForUnexpectedKeys( knownPlaceholders, unknownName -> "Unknown placeholder: " + unknownName); originalCodeObjectLiteralMap.checkForUnexpectedKeys( knownPlaceholders, unknownName -> "Unknown placeholder: " + unknownName); } }; } private interface IcuTemplateOptions { String getDescription(); @Nullable String getMeaning(); @Nullable String getAlternateMessageId(); ImmutableMap getPlaceholderExampleMap(); ImmutableMap getPlaceholderOriginalCodeMap(); void checkForUnknownPlaceholders(Set knownPlaceholders) throws MalformedException; /** All the placeholder names mentioned in the options. */ ImmutableSet getPlaceholderNames(); } /** * Property names that are expected to appear in the options bag argument of declareIcuTemplate(). */ private static final ImmutableSet ICU_TEMPLATE_OPTION_NAMES = ImmutableSet.of("description", "meaning", "alternate_message_id", "example", "original_code"); /** * Extract the options from the second argument to a `declareIcuTemplate()` call * *

Example: * *


   *   {
   *     description: 'required message description here',
   *     meaning: 'optional meaning here',
   *     alternate_message_id: 'optional alternate message ID here',
   *     // optional
   *     original_code: {
   *       'INTERPOLATION_1': '{{getPerson()}}',
   *       'INTERPOLATION_2': '{{getPlaceName()}}',
   *     },
   *     // optional
   *     example: {
   *       'INTERPOLATION_1': 'Jane Doe',
   *       'INTERPOLATION_2': 'Paris, France',
   *     }
   *   }
   * 
*/ private IcuTemplateOptions extractIcuTemplateOptions(Node optionsBag) throws MalformedException { ObjectLiteralMap objectLiteralMap = extractObjectLiteralMap(optionsBag); objectLiteralMap.checkForUnexpectedKeys( ICU_TEMPLATE_OPTION_NAMES, optionName -> "Unknown option: " + optionName); // required description string final @Nullable Node descriptionNode = objectLiteralMap.getValueNode("description"); if (descriptionNode == null) { throw new MalformedException("'description' option field is missing", optionsBag); } final String description = extractStringFromStringExprNode(descriptionNode); // optional meaning string final @Nullable Node meaningNode = objectLiteralMap.getValueNode("meaning"); final @Nullable String meaning = meaningNode == null ? null : extractStringFromStringExprNode(meaningNode); // optional alternate message ID final @Nullable Node alternateMessageIdNode = objectLiteralMap.getValueNode("alternate_message_id"); final @Nullable String alternateMessageId = alternateMessageIdNode == null ? null : extractStringFromStringExprNode(alternateMessageIdNode); // optional map of placeholder names to example string values final Node exampleValueNode = objectLiteralMap.getValueNode("example"); final ObjectLiteralMap exampleObjectLiteralMap = extractObjectLiteralMap(exampleValueNode); final ImmutableMap placeholderExamplesMap = exampleObjectLiteralMap.extractAsStringToStringMap(); // optional map of placeholder names to original_code string values final Node originalCodeValueNode = objectLiteralMap.getValueNode("original_code"); final ObjectLiteralMap originalCodeObjectLiteralMap = extractObjectLiteralMap(originalCodeValueNode); final ImmutableMap placeholderOriginalCodeMap = originalCodeObjectLiteralMap.extractAsStringToStringMap(); // All the placeholder names mentioned in the 2 optional maps. final ImmutableSet placeholderNames = ImmutableSet.builder() .addAll(placeholderExamplesMap.keySet()) .addAll(placeholderOriginalCodeMap.keySet()) .build(); // NOTE: The getX() methods below should all do little to no computation. // In particular, all checking for MalformedExceptions must be done before creating this object. return new IcuTemplateOptions() { @Override public String getDescription() { return description; } @Override public @Nullable String getMeaning() { return meaning; } @Override public @Nullable String getAlternateMessageId() { return alternateMessageId; } @Override public ImmutableMap getPlaceholderExampleMap() { return placeholderExamplesMap; } @Override public ImmutableMap getPlaceholderOriginalCodeMap() { return placeholderOriginalCodeMap; } @Override public ImmutableSet getPlaceholderNames() { return placeholderNames; } @Override public void checkForUnknownPlaceholders(Set knownPlaceholders) throws MalformedException { exampleObjectLiteralMap.checkForUnexpectedKeys( knownPlaceholders, unknownName -> "Unknown placeholder: " + unknownName); originalCodeObjectLiteralMap.checkForUnexpectedKeys( knownPlaceholders, unknownName -> "Unknown placeholder: " + unknownName); } }; } private static boolean extractBooleanStringKeyValue(@Nullable Node stringKeyNode) throws MalformedException { if (stringKeyNode == null) { return false; } else { final Node valueNode = stringKeyNode.getOnlyChild(); if (valueNode.isTrue()) { return true; } else if (valueNode.isFalse()) { return false; } else { throw new MalformedException( stringKeyNode.getString() + ": Literal true or false expected", valueNode); } } } /** * Represents the contents of a object literal Node in the AST. * *

The object literal may not have any computed keys or methods. * *

Use {@link #extractObjectLiteralMap(Node)} to get an instance. */ public interface ObjectLiteralMap { boolean getBooleanValueOrFalse(String key) throws MalformedException; /** * Returns a map from object property names the Node values they have in the AST, building the * map first, if necessary. */ ImmutableMap extractAsValueMap(); /** * Returns a map from object property names to string values, building it first, if necessary. * * @throws MalformedException if any of the values are not actually simple string literals or * concatenations of string literals. */ ImmutableMap extractAsStringToStringMap() throws MalformedException; /** * Get the value node for a key. * *

This method avoids the work of extracting the full value map. */ @Nullable Node getValueNode(String key); /** * Throws a {@link MalformedException} if the object literal has keys not in the expected set. */ void checkForUnexpectedKeys( Set expectedKeys, Function createErrorMessage) throws MalformedException; /** * Throws a {@link MalformedException} if the object literal does not have one all the keys in * the required set. */ void checkForRequiredKeys( Set requiredKeys, Function createException) throws MalformedException; } private static class ObjectLiteralMapImpl implements ObjectLiteralMap { private final Map stringToStringKeyMap; // The result of extractValueMap(). It will be populated if requested. private ImmutableMap valueMap = null; // The result of extractStringMap(). It will be populated if requested. private ImmutableMap stringMap = null; private ObjectLiteralMapImpl(Map stringToStringKeyMap) { this.stringToStringKeyMap = stringToStringKeyMap; } @Override public @Nullable Node getValueNode(String key) { final Node stringKeyNode = stringToStringKeyMap.get(key); return stringKeyNode == null ? null : stringKeyNode.getOnlyChild(); } @Override public boolean getBooleanValueOrFalse(String key) throws MalformedException { final Node stringKeyNode = stringToStringKeyMap.get(key); return extractBooleanStringKeyValue(stringKeyNode); } @Override public ImmutableMap extractAsValueMap() { if (valueMap == null) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Entry entry : stringToStringKeyMap.entrySet()) { builder.put(entry.getKey(), entry.getValue().getOnlyChild()); } valueMap = builder.buildOrThrow(); } return valueMap; } @Override public ImmutableMap extractAsStringToStringMap() throws MalformedException { if (stringMap == null) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Entry entry : stringToStringKeyMap.entrySet()) { builder.put( entry.getKey(), extractStringFromStringExprNode(entry.getValue().getOnlyChild())); } stringMap = builder.buildOrThrow(); } return stringMap; } @Override public void checkForUnexpectedKeys( Set expectedKeys, Function createErrorMessage) throws MalformedException { for (String key : stringToStringKeyMap.keySet()) { if (!expectedKeys.contains(key)) { throw new MalformedException( createErrorMessage.apply(key), stringToStringKeyMap.get(key)); } } } @Override public void checkForRequiredKeys( Set requiredKeys, Function createException) throws MalformedException { for (String requiredKey : requiredKeys) { if (!stringToStringKeyMap.containsKey(requiredKey)) { throw createException.apply(requiredKey); } } } } /** * Returns an object to represent an object literal Node from the AST. * *

The object literal's members must all be `STRING_KEY` nodes. * *

No duplicate string keys are allowed. * *

A `null` argument is treated as if it were an empty object literal. * * @throws MalformedException If the Node does not meet the above requirements. */ public static ObjectLiteralMap extractObjectLiteralMap(@Nullable Node objLit) throws MalformedException { if (objLit == null) { return new ObjectLiteralMapImpl(ImmutableMap.of()); } if (!objLit.isObjectLit()) { throw new MalformedException("object literal expected", objLit); } final LinkedHashMap stringToStringKeyMap = new LinkedHashMap<>(); for (Node stringKey = objLit.getFirstChild(); stringKey != null; stringKey = stringKey.getNext()) { if (!stringKey.isStringKey()) { throw new MalformedException("string key expected", stringKey); } final String key = stringKey.getString(); if (stringToStringKeyMap.containsKey(key)) { throw new MalformedException("duplicate string key: " + key, stringKey); } stringToStringKeyMap.put(stringKey.getString(), stringKey); } return new ObjectLiteralMapImpl(stringToStringKeyMap); } public static ImmutableList parseJsMessageTextIntoParts(String originalMsgText) throws PlaceholderFormatException { GoogGetMsgParsedText googGetMsgParsedText = extractGoogGetMsgParsedText(originalMsgText); return googGetMsgParsedText.getParts(); } private static GoogGetMsgParsedText extractGoogGetMsgParsedText(final String originalMsgText) throws PlaceholderFormatException { String msgText = originalMsgText; ImmutableList.Builder partsBuilder = ImmutableList.builder(); ImmutableSet.Builder placeholderNamesBuilder = ImmutableSet.builder(); while (true) { int phBegin = msgText.indexOf(JsMessage.PH_JS_PREFIX); if (phBegin < 0) { // Just a string literal partsBuilder.add(StringPart.create(msgText)); break; } else { if (phBegin > 0) { // A string literal followed by a placeholder partsBuilder.add(StringPart.create(msgText.substring(0, phBegin))); } // A placeholder. Find where it ends int phEnd = msgText.indexOf(JsMessage.PH_JS_SUFFIX, phBegin); if (phEnd < 0) { throw new PlaceholderFormatException("Placeholder incorrectly formatted"); } String phName = msgText.substring(phBegin + JsMessage.PH_JS_PREFIX.length(), phEnd); if (!JsMessage.isLowerCamelCaseWithNumericSuffixes(phName)) { throw new PlaceholderFormatException("Placeholder name not in lowerCamelCase: " + phName); } placeholderNamesBuilder.add(phName); partsBuilder.add(PlaceholderReference.createForJsName(phName)); int nextPos = phEnd + JsMessage.PH_JS_SUFFIX.length(); if (nextPos < msgText.length()) { // Iterate on the rest of the message value msgText = msgText.substring(nextPos); } else { // The message is parsed break; } } } final ImmutableSet placeholderNames = placeholderNamesBuilder.build(); final ImmutableList parts = partsBuilder.build(); // NOTE: The methods defined below should do little to no computation, and definitely should // not throw exceptions. return new GoogGetMsgParsedText() { @Override public String getText() { return originalMsgText; } @Override public ImmutableSet getPlaceholderNames() { return placeholderNames; } @Override public ImmutableList getParts() { return parts; } }; } private interface GoogGetMsgParsedText { String getText(); ImmutableSet getPlaceholderNames(); ImmutableList getParts(); } /** 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(); JsMessage firstMessage = getJsMessageFromNode(t, firstArg); if (firstMessage == null) { compiler.report(JSError.make(firstArg, FALLBACK_ARG_ERROR, firstArg.getQualifiedName())); return; } Node secondArg = firstArg.getNext(); JsMessage secondMessage = getJsMessageFromNode(t, secondArg); if (secondMessage == null) { compiler.report(JSError.make(secondArg, FALLBACK_ARG_ERROR, secondArg.getQualifiedName())); return; } processMessageFallback(call, firstMessage, secondMessage); } /** * Processes a found JS message that was defined with {@code goog.getMsg()}. * *

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 *
*/ protected abstract void processJsMessageDefinition(JsMessageDefinition definition); /** Processes a found call to {@code goog.i18n.messages.declareIcuTemplate()} */ protected abstract void processIcuTemplateDefinition(IcuTemplateDefinition 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) { return identifier.startsWith(MSG_PREFIX) || isScopedAliasesPrefix(identifier); } private static boolean isMessageIdentifier(Node node) { String qname = node.getQualifiedName(); return qname != null && qname.contains(MSG_PREFIX); } /** * 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 @Nullable JsMessage getJsMessageFromNode(NodeTraversal t, Node node) { String messageName = node.getQualifiedName(); if (messageName == null || !messageName.contains(MSG_PREFIX)) { return null; } String messageKey = messageName.substring(messageName.indexOf(MSG_PREFIX)); if (isUnnamedMessageName(messageKey)) { return getTrackedUnnamedMessage(t, messageName); } else { return getTrackedNormalMessage(messageKey); } } /** Returns whether the given message name is in the unnamed namespace. */ private static boolean isUnnamedMessageName(String identifier) { return MSG_UNNAMED_PATTERN.matcher(identifier).matches(); } /** * 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; } } public static boolean isScopedAliasesPrefix(String name) { return SCOPED_ALIASES_PREFIX_PATTERN.matcher(name).lookingAt(); } public static String removeScopedAliasesPrefix(String name) { return SCOPED_ALIASES_PREFIX_PATTERN.matcher(name).replaceFirst("MSG_"); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy