/* * Copyright 2012 Google Inc. * * 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.template.soy.jssrc.internal; import static com.google.template.soy.jssrc.dsl.CodeChunk.declare; import static com.google.template.soy.jssrc.dsl.CodeChunk.id; import static com.google.template.soy.jssrc.dsl.CodeChunk.ifStatement; import static com.google.template.soy.jssrc.dsl.CodeChunk.mapLiteral; import static com.google.template.soy.jssrc.dsl.CodeChunk.new_; import static com.google.template.soy.jssrc.dsl.CodeChunk.stringLiteral; import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_GET_MSG; import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_I18N_MESSAGE_FORMAT; import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.jssrc.SoyJsSrcOptions; import com.google.template.soy.jssrc.dsl.CodeChunk; import com.google.template.soy.jssrc.dsl.CodeChunkUtils; import com.google.template.soy.msgs.internal.IcuSyntaxUtils; import com.google.template.soy.msgs.internal.MsgUtils; import com.google.template.soy.msgs.restricted.SoyMsgPart; import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart; import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.CallParamNode; import com.google.template.soy.soytree.CaseOrDefaultNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; import com.google.template.soy.soytree.MsgHtmlTagNode; import com.google.template.soy.soytree.MsgNode; import com.google.template.soy.soytree.MsgPlaceholderNode; import com.google.template.soy.soytree.MsgPluralNode; import com.google.template.soy.soytree.MsgSelectNode; import com.google.template.soy.soytree.RawTextNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.BlockNode; import com.google.template.soy.soytree.SoyNode.CommandNode; import com.google.template.soy.soytree.SoyNode.StandaloneNode; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Assistant visitor for GenJsCodeVisitor to handle messages. * * <p> Precondition: MsgNode should not exist in the tree. * */ public class GenJsCodeVisitorAssistantForMsgs extends AbstractSoyNodeVisitor<Void> { /** Regex pattern for an underscore-number suffix. */ private static final Pattern UNDERSCORE_NUMBER_SUFFIX = Pattern.compile("_[0-9]+$"); /** The options for generating JS source code. */ private final SoyJsSrcOptions jsSrcOptions; /** Master instance of GenJsCodeVisitor. */ protected final GenJsCodeVisitor master; /** Instance of JsExprTranslator to use. */ private final JsExprTranslator jsExprTranslator; /** Instance of GenCallCodeUtils to use. */ private final GenCallCodeUtils genCallCodeUtils; /** The IsComputableAsJsExprsVisitor used by this instance. */ private final IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor; /** The GenJsExprsVisitor used for the current template. */ private final GenJsExprsVisitor genJsExprsVisitor; /** * Used for looking up the local name for a given template call to a fully qualified template * name. */ private final TemplateAliases templateAliases; protected final TranslationContext translationContext; private final ErrorReporter errorReporter; /** * @param master The master GenJsCodeVisitor instance. * @param jsExprTranslator Instance of JsExprTranslator to use. * @param genCallCodeUtils Instance of GenCallCodeUtils to use. * @param isComputableAsJsExprsVisitor The IsComputableAsJsExprsVisitor to use. * @param genJsExprsVisitor The current GenJsExprsVisitor. */ protected GenJsCodeVisitorAssistantForMsgs( GenJsCodeVisitor master, SoyJsSrcOptions jsSrcOptions, JsExprTranslator jsExprTranslator, GenCallCodeUtils genCallCodeUtils, IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor, TemplateAliases functionAliases, GenJsExprsVisitor genJsExprsVisitor, TranslationContext translationContext, ErrorReporter errorReporter) { this.master = master; this.jsSrcOptions = jsSrcOptions; this.jsExprTranslator = jsExprTranslator; this.genCallCodeUtils = genCallCodeUtils; this.isComputableAsJsExprsVisitor = isComputableAsJsExprsVisitor; this.templateAliases = functionAliases; this.genJsExprsVisitor = genJsExprsVisitor; this.translationContext = translationContext; this.errorReporter = errorReporter; } @Override public Void exec(SoyNode node) { throw new AssertionError(); } /** The JsCodeBuilder to build the current JS file being generated (during a run). */ protected JsCodeBuilder jsCodeBuilder() { return master.jsCodeBuilder; } /** * Generates Javascript statements that declare a translated variable, returning the variable name * for the caller to output (as an expression). MsgFallbackGroupNodes can only appear in let * blocks, which seems like unnecesary overhead since this already generates a perfectly usable * variable. However, this design makes incremental DOM codegen, which needs to apply complex * transforms on the translated variable, much simpler. Example: * * <pre> * {msg desc="Link to help content."}Learn more{/msg} * {msg desc="Tells user how to access a product." hidden="true"} * Click <a href="}{$url}">here</a> to access {$productName}. * {/msg} * </pre> * * might generate * * <pre> * /** @desc Link to help content. *{@literal /} * var MSG_UNNAMED_9 = goog.getMsg('Learn more'); * var msg_s9 = MSG_UNNAMED_9; * /** @desc Tells user how to access a product. * * @hidden *{@literal /} * var MSG_UNNAMED_10 = goog.getMsg( * 'Click {$startLink}here{$endLink} to access {$productName}.', * {startLink: '<a href="' + opt_data.url + '">', * endLink: '</a>', * productName: opt_data.productName}); * </pre> * * and return {@code "MSG_UNNAMED_10"}. */ public String generateMsgGroupVariable(MsgFallbackGroupNode node) { String tmpVarName = translationContext.nameGenerator().generateName("msg_s"); if (node.numChildren() == 1) { return generateSingleMsgVariable(node.getChild(0), tmpVarName); } else { // has fallbackmsg children generateMsgGroupVariable(node, tmpVarName); return tmpVarName; } } /** * Generates an initialized variable declaration for an {@link MsgNode} with no fallback messages. * * @return The variable name, which will be the actual MSG_BLAH variable if no temporary variables * are needed for additional formatting. */ private String generateSingleMsgVariable(MsgNode msgNode, String tmpVarName) { String googMsgVarName = buildGoogMsgVarNameHelper(msgNode); // Generate the goog.getMsg call. GoogMsgCodeGenInfo googMsgCodeGenInfo = genGoogGetMsgCallHelper(googMsgVarName, msgNode); if (!msgNode.isPlrselMsg()) { // No postprocessing is needed. Simply use the original goog.getMsg var. return googMsgVarName; } // For plural/select messages, generate the goog.i18n.MessageFormat call. // We don't want to output the result of goog.getMsg() directly. Instead, we send that // string to goog.i18n.MessageFormat for postprocessing. This postprocessing is where we're // handling all placeholder replacements, even ones that have nothing to do with // plural/select. jsCodeBuilder().append(declare(tmpVarName, getMessageFormatCall(googMsgCodeGenInfo))); return tmpVarName; } /** * Generates an initialized variable declaration for an {@link MsgFallbackGroupNode} that contains * fallback(s). */ private void generateMsgGroupVariable(MsgFallbackGroupNode node, String tmpVarName) { List<GoogMsgCodeGenInfo> childGoogMsgCodeGenInfos = new ArrayList<>(node.numChildren()); // Generate the goog.getMsg calls for all children. for (MsgNode msgNode : node.getChildren()) { String googMsgVarName = buildGoogMsgVarNameHelper(msgNode); childGoogMsgCodeGenInfos.add(genGoogGetMsgCallHelper(googMsgVarName, msgNode)); } // Declare a temporary variable to hold the getMsgWithFallback() call so that we can apply any // MessageFormats from any of the fallbacks. This is also the variable name that we return to // the caller. jsCodeBuilder().appendLineStart("var ", tmpVarName, " = goog.getMsgWithFallback("); boolean isFirst = true; for (GoogMsgCodeGenInfo childGoogMsgCodeGenInfo : childGoogMsgCodeGenInfos) { if (isFirst) { isFirst = false; } else { jsCodeBuilder().append(", "); } jsCodeBuilder().append(childGoogMsgCodeGenInfo.googMsgVarName); } jsCodeBuilder().appendLineEnd(");"); // Generate the goog.i18n.MessageFormat calls for child plural/select messages (if any), each // wrapped in an if-block that will only execute if that child is the chosen message. for (GoogMsgCodeGenInfo childGoogMsgCodeGenInfo : childGoogMsgCodeGenInfos) { if (childGoogMsgCodeGenInfo.isPlrselMsg) { CodeChunk.WithValue tmpVar = id(tmpVarName); jsCodeBuilder() .append( ifStatement( tmpVar.doubleEquals(id(childGoogMsgCodeGenInfo.googMsgVarName)), tmpVar.assign(getMessageFormatCall(childGoogMsgCodeGenInfo))) .build()); } } } /** Builds the googMsgVarName for an MsgNode. */ private String buildGoogMsgVarNameHelper(MsgNode msgNode) { // NOTE: MSG_UNNAMED/MSG_EXTERNAL are a special tokens recognized by the jscompiler. MSG_UNNAMED // disables the default logic that requires all messages to be uniquely named. // and MSG_EXTERNAL String desiredName = jsSrcOptions.googMsgsAreExternal() ? "MSG_EXTERNAL_" + MsgUtils.computeMsgIdForDualFormat(msgNode) : "MSG_UNNAMED"; return translationContext.nameGenerator().generateName(desiredName); } /** * Generates the goog.getMsg call for an MsgNode. The goog.getMsg call (including JsDoc) will be * appended to the jsCodeBuilder. * * @return The GoogMsgCodeGenInfo object created in the process, which may be needed for * generating postprocessing code (if the message is plural/select). */ private GoogMsgCodeGenInfo genGoogGetMsgCallHelper(String googMsgVarName, MsgNode msgNode) { // Build the code for the message content. // TODO: We could build the msg parts once and save it as a field on the MsgNode or save it some // other way, but it would increase memory usage a little bit. It's probably not a big deal, // since it's not per-locale, but I'm not going to do this right now since we're trying to // decrease memory usage right now. The same memoization possibility also applies to the msg // parts with embedded ICU syntax (created in helper buildGoogMsgContentStr()). ImmutableList<SoyMsgPart> msgParts = MsgUtils.buildMsgParts(msgNode); CodeChunk.WithValue googMsgContent = stringLiteral(buildGoogMsgContentStr(msgParts, msgNode.isPlrselMsg())); // Build the individual code bits for each placeholder (i.e. "<placeholderName>: <exprCode>") // and each plural/select (i.e. "<varName>: <exprCode>"). GoogMsgCodeGenInfo googMsgCodeGenInfo = new GoogMsgCodeGenInfo(googMsgVarName, msgNode.isPlrselMsg()); genGoogMsgCodeForChildren(msgNode, msgNode, googMsgCodeGenInfo); // Generate JS comment (JSDoc) block for the goog.getMsg() call. jsCodeBuilder().appendLineStart("/** "); if (msgNode.getMeaning() != null) { jsCodeBuilder().appendLineEnd("@meaning ", msgNode.getMeaning()); jsCodeBuilder().appendLineStart(" * "); } jsCodeBuilder().append("@desc ", msgNode.getDesc()); if (msgNode.isHidden()) { jsCodeBuilder().appendLineEnd(); jsCodeBuilder().appendLineStart(" * @hidden"); } jsCodeBuilder().appendLineEnd(" */"); // Generate goog.getMsg() call. if (msgNode.isPlrselMsg() || googMsgCodeGenInfo.placeholders.isEmpty()) { // For plural/select msgs, we're letting goog.i18n.MessageFormat handle all placeholder // replacements, even ones that have nothing to do with plural/select. Therefore, this case // is the same as having no placeholder replacements. jsCodeBuilder() .append(declare(googMsgCodeGenInfo.googMsgVarName, GOOG_GET_MSG.call(googMsgContent))); } else { // If there are placeholders, pass them as an arg to goog.getMsg. jsCodeBuilder() .append( declare( googMsgCodeGenInfo.googMsgVarName, GOOG_GET_MSG.call(googMsgContent, googMsgCodeGenInfo.placeholders.build()))); } return googMsgCodeGenInfo; } /** * Builds the message content string for a goog.getMsg() call. * * @param msgParts The parts of the message. * @param doUseBracedPhs Whether to use braced placeholders. * @return The message content string for a goog.getMsg() call. */ private static String buildGoogMsgContentStr( ImmutableList<SoyMsgPart> msgParts, boolean doUseBracedPhs) { // Note: For source messages, disallow ICU syntax chars that need escaping in raw text. msgParts = IcuSyntaxUtils.convertMsgPartsToEmbeddedIcuSyntax(msgParts, false); StringBuilder msgStrSb = new StringBuilder(); for (SoyMsgPart msgPart : msgParts) { if (msgPart instanceof SoyMsgRawTextPart) { msgStrSb.append(((SoyMsgRawTextPart) msgPart).getRawText()); } else if (msgPart instanceof SoyMsgPlaceholderPart) { String placeholderName = ((SoyMsgPlaceholderPart) msgPart).getPlaceholderName(); if (doUseBracedPhs) { // Add placeholder to message text. msgStrSb.append("{").append(placeholderName).append("}"); } else { // For goog.getMsg(), we must change the placeholder name to lower camel-case format. String googMsgPlaceholderName = genGoogMsgPlaceholderName(placeholderName); // Add placeholder to message text. Note the '$' for goog.getMsg() syntax. msgStrSb.append("{$").append(googMsgPlaceholderName).append("}"); } } else { throw new AssertionError(); } } return msgStrSb.toString(); } /** * Generates the {@code goog.i18n.MessageFormat} postprocessing call for a child plural/select * message. */ private static CodeChunk.WithValue getMessageFormatCall(GoogMsgCodeGenInfo codeGenInfo) { MapLiteralBuilder builder = codeGenInfo.pluralsAndSelects; builder.putAll(codeGenInfo.placeholders); return new_(GOOG_I18N_MESSAGE_FORMAT) .call(id(codeGenInfo.googMsgVarName)) .dotAccess("formatIgnoringPound") .call(builder.build()); } /** Stores the data required for generating {@code goog.getMsg()} calls. */ private static final class GoogMsgCodeGenInfo { /** The name of the {@code goog.getMsg()} msg var, i.e. MSG_EXTERNAL_### or MSG_UNNAMED_###. */ final String googMsgVarName; /** Whether the message is a plural/select message. */ final boolean isPlrselMsg; /** Key-value entries for placeholders. */ final MapLiteralBuilder placeholders = new MapLiteralBuilder(); /** Key-value entries for plural and select variables. */ final MapLiteralBuilder pluralsAndSelects = new MapLiteralBuilder(); GoogMsgCodeGenInfo(String googMsgVarName, boolean isPlrselMsg) { this.googMsgVarName = googMsgVarName; this.isPlrselMsg = isPlrselMsg; } } /** * Generates {@code goog.getMsg()} calls for a given parent node and its children. * * @param parentNode A parent node of one of these types: {@link MsgNode}, {@link * com.google.template.soy.soytree.MsgPluralCaseNode}, {@link * com.google.template.soy.soytree.MsgPluralDefaultNode}, {@link * com.google.template.soy.soytree.MsgSelectCaseNode} {@link * com.google.template.soy.soytree.MsgSelectDefaultNode}. * @param msgNode The enclosing MsgNode. * @param codeGenInfo Data structure holding information on placeholder names, plural variable * names, and select variable names to be used for message code generation. */ private void genGoogMsgCodeForChildren( BlockNode parentNode, MsgNode msgNode, GoogMsgCodeGenInfo codeGenInfo) { for (StandaloneNode child : parentNode.getChildren()) { if (child instanceof RawTextNode) { // nothing to do } else if (child instanceof MsgPlaceholderNode) { genGoogMsgCodeForPlaceholder((MsgPlaceholderNode) child, msgNode, codeGenInfo); } else if (child instanceof MsgPluralNode) { genGoogMsgCodeForPluralNode((MsgPluralNode) child, msgNode, codeGenInfo); } else if (child instanceof MsgSelectNode) { genGoogMsgCodeForSelectNode((MsgSelectNode) child, msgNode, codeGenInfo); } else { String nodeStringForErrorMsg = (child instanceof CommandNode) ? "Tag " + ((CommandNode) child).getTagString() : "Node " + child; throw new AssertionError( nodeStringForErrorMsg + " is not allowed to be a direct child of a 'msg' tag. At :" + child.getSourceLocation()); } } } /** * Generates code bits for a {@code MsgPluralNode} subtree inside a message. * * @param pluralNode A node of type {@code MsgPluralNode}. * @param msgNode The enclosing {@code MsgNode} object. * @param googMsgCodeGenInfo Data structure holding information on placeholder names, plural * variable names, and select variable names to be used for message code generation. */ private void genGoogMsgCodeForPluralNode( MsgPluralNode pluralNode, MsgNode msgNode, GoogMsgCodeGenInfo googMsgCodeGenInfo) { googMsgCodeGenInfo.pluralsAndSelects.put( stringLiteral(msgNode.getPluralVarName(pluralNode)), jsExprTranslator.translateToCodeChunk( pluralNode.getExpr(), translationContext, errorReporter)); for (CaseOrDefaultNode child : pluralNode.getChildren()) { genGoogMsgCodeForChildren(child, msgNode, googMsgCodeGenInfo); } } /** * Generates code bits for a {@code MsgSelectNode} subtree inside a message. * * @param selectNode A node of type {@code MsgSelectNode}. * @param msgNode The enclosing {@code MsgNode} object. * @param codeGenInfo Data structure holding information on placeholder names, plural variable * names, and select variable names to be used for message code generation. */ private void genGoogMsgCodeForSelectNode( MsgSelectNode selectNode, MsgNode msgNode, GoogMsgCodeGenInfo codeGenInfo) { codeGenInfo.pluralsAndSelects.put( stringLiteral(msgNode.getSelectVarName(selectNode)), jsExprTranslator.translateToCodeChunk( selectNode.getExpr(), translationContext, errorReporter)); for (CaseOrDefaultNode child : selectNode.getChildren()) { genGoogMsgCodeForChildren(child, msgNode, codeGenInfo); } } /** * Generates code bits for a normal {@code MsgPlaceholderNode} inside a message. * * @param node A node of type {@code MsgPlaceholderNode}. * @param msgNode The enclosing {@code MsgNode} object. * @param codeGenInfo Data structure holding information on placeholder names, plural variable * names, and select variable names to be used for message code generation. */ private void genGoogMsgCodeForPlaceholder( MsgPlaceholderNode node, MsgNode msgNode, GoogMsgCodeGenInfo codeGenInfo) { String placeholderName = msgNode.getPlaceholderName(node); // For plural/select, the placeholder is an ICU placeholder, i.e. kept in all-caps. But for // goog.getMsg(), we must change the placeholder name to lower camel-case format. String googMsgPlaceholderName = codeGenInfo.isPlrselMsg ? placeholderName : genGoogMsgPlaceholderName(placeholderName); codeGenInfo.placeholders.put( stringLiteral(googMsgPlaceholderName), genGoogMsgPlaceholder(node)); } /** * <p> * Converts a Soy placeholder name (in upper underscore format) into a JS variable name (in lower * camel case format) used by goog.getMsg(). If the original name has a numeric suffix, it will * be preserved with an underscore. * </p> * <p> * For example, the following transformations happen: * <li> N : n * <li> NUM_PEOPLE : numPeople * <li> PERSON_2 : person_2 * <li>GENDER_OF_THE_MAIN_PERSON_3 : genderOfTheMainPerson_3 * </p> * * @param placeholderName The placeholder name to convert. * @return The generated goog.getMsg name for the given (standard) Soy name. */ private static String genGoogMsgPlaceholderName(String placeholderName) { Matcher suffixMatcher = UNDERSCORE_NUMBER_SUFFIX.matcher(placeholderName); if (suffixMatcher.find()) { String base = placeholderName.substring(0, suffixMatcher.start()); String suffix = suffixMatcher.group(); return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, base) + suffix; } else { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, placeholderName); } } /** Returns a code chunk for the given placeholder node. */ protected CodeChunk.WithValue genGoogMsgPlaceholder(MsgPlaceholderNode msgPhNode) { List<CodeChunk.WithValue> contentChunks = new ArrayList<>(); for (StandaloneNode contentNode : msgPhNode.getChildren()) { if (contentNode instanceof MsgHtmlTagNode && !isComputableAsJsExprsVisitor.exec(contentNode)) { // This is a MsgHtmlTagNode that is not computable as JS expressions. Visit it to // generate code to define the 'htmlTag<n>' variable. visit(contentNode); contentChunks.add(id("htmlTag" + contentNode.getId())); } else if (contentNode instanceof CallNode) { // If the CallNode has any CallParamContentNode children that are not computable as JS // expressions, visit them to generate code to define their respective 'param<n>' variables. CallNode callNode = (CallNode) contentNode; for (CallParamNode grandchild : callNode.getChildren()) { if (grandchild instanceof CallParamContentNode && !isComputableAsJsExprsVisitor.exec(grandchild)) { visit(grandchild); } } CodeChunk.WithValue call = genCallCodeUtils.gen(callNode, templateAliases, translationContext, errorReporter); contentChunks.add(call); } else { List<CodeChunk.WithValue> chunks = genJsExprsVisitor.exec(contentNode); contentChunks.add(CodeChunkUtils.concatChunks(chunks)); } } return CodeChunkUtils.concatChunks(contentChunks); } // ----------------------------------------------------------------------------------------------- // Implementations for other specific nodes. /** * Example: * <pre> * <a href="http://www.google.com/search?hl=en * {for $i in range(3)} * &param{$i}={$i} * {/for} * "> * might generate * </pre> * var htmlTag84 = (new soy.StringBuilder()).append('<a href="'); * for (var i80 = 1; i80 < 3; i80++) { * htmlTag84.append('&param', i80, '=', i80); * } * htmlTag84.append('">'); * </pre> */ @Override protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) { // This node should only be visited when it's not computable as JS expressions, because this // method just generates the code to define the temporary 'htmlTag<n>' variable. if (isComputableAsJsExprsVisitor.exec(node)) { throw new AssertionError( "Should only define 'htmlTag<n>' when not computable as JS expressions."); } jsCodeBuilder().pushOutputVar("htmlTag" + node.getId()); visitChildren(node); jsCodeBuilder().popOutputVar(); } // ----------------------------------------------------------------------------------------------- // Fallback implementation. @Override protected void visitSoyNode(SoyNode node) { master.visitForUseByAssistants(node); } /** * Helper class for building up the input to {@link CodeChunk#mapLiteral}. TODO(brndn): consider * making this part of the CodeChunk DSL, since all callers seem to do something similar. */ private static final class MapLiteralBuilder { final ImmutableList.Builder<CodeChunk.WithValue> keys = ImmutableList.builder(); final ImmutableList.Builder<CodeChunk.WithValue> values = ImmutableList.builder(); final Set<CodeChunk.WithValue> knownKeys = new HashSet<>(); MapLiteralBuilder put(CodeChunk.WithValue key, CodeChunk.WithValue value) { // No-op if the key already exists. This happens whenever a placeholder is repeated // in a message (different branches of an {if}, {select}, etc.) if (knownKeys.add(key)) { keys.add(key); values.add(value); } return this; } boolean isEmpty() { return knownKeys.isEmpty(); } MapLiteralBuilder putAll(MapLiteralBuilder other) { ImmutableList<CodeChunk.WithValue> keys = other.keys.build(); ImmutableList<CodeChunk.WithValue> values = other.values.build(); Preconditions.checkState(keys.size() == values.size()); Preconditions.checkState(Sets.intersection(knownKeys, other.knownKeys).isEmpty()); for (int i = 0; i < keys.size(); ++i) { put(keys.get(i), values.get(i)); } return this; } CodeChunk.WithValue build() { return mapLiteral(keys.build(), values.build()); } } }