Java tutorial
/* * Copyright 2008 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.parseinfo.passes; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.template.soy.base.SoyBackendKind; import com.google.template.soy.base.SoySyntaxException; import com.google.template.soy.base.internal.BaseUtils; import com.google.template.soy.base.internal.IndentedLinesBuilder; import com.google.template.soy.base.internal.SoyFileKind; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.exprtree.AbstractExprNodeVisitor; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.ExprNode.ParentExprNode; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.exprtree.FieldAccessNode; import com.google.template.soy.internal.base.Pair; import com.google.template.soy.parseinfo.SoyFileInfo.CssTagsPrefixPresence; import com.google.template.soy.sharedpasses.FindIjParamsVisitor; import com.google.template.soy.sharedpasses.FindIjParamsVisitor.IjParamsInfo; import com.google.template.soy.sharedpasses.FindIndirectParamsVisitor; import com.google.template.soy.sharedpasses.FindIndirectParamsVisitor.IndirectParamsInfo; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.CssNode; import com.google.template.soy.soytree.ExprUnion; import com.google.template.soy.soytree.SoyFileNode; import com.google.template.soy.soytree.SoyFileSetNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.ExprHolderNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import com.google.template.soy.soytree.SoySyntaxExceptionUtils; import com.google.template.soy.soytree.TemplateBasicNode; import com.google.template.soy.soytree.TemplateDelegateNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.TemplateRegistry; import com.google.template.soy.soytree.Visibility; import com.google.template.soy.soytree.defn.HeaderParam; import com.google.template.soy.soytree.defn.TemplateParam; import com.google.template.soy.types.SoyObjectType; import com.google.template.soy.types.SoyType; import com.google.template.soy.types.aggregate.ListType; import com.google.template.soy.types.aggregate.MapType; import com.google.template.soy.types.aggregate.RecordType; import com.google.template.soy.types.aggregate.UnionType; import com.google.template.soy.types.proto.SoyProtoType; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Visitor for generating Java classes containing the parse info. * * <p> Important: Do not use outside of Soy code (treat as superpackage-private). * * <p> {@link #exec} should be called on a full parse tree. * * <p> For an example Soy file and its corresponding generated code, see * <pre> * [tests_dir]/com/google/template/soy/test_data/AaaBbbCcc.soy * [tests_dir]/com/google/template/soy/test_data/AaaBbbCccSoyInfo.java * </pre> * */ public final class GenerateParseInfoVisitor extends AbstractSoyNodeVisitor<ImmutableMap<String, String>> { /** * Represents the source of the generated Java class names. */ @VisibleForTesting static enum JavaClassNameSource { /** AaaBbb.soy or aaa_bbb.soy --> AaaBbbSoyInfo. */ SOY_FILE_NAME, /** boo.foo.aaaBbb --> AaaBbbSoyInfo. */ SOY_NAMESPACE_LAST_PART, /** File1SoyInfo, File2SoyInfo, etc. */ GENERIC; /** Pattern for an all-upper-case word in a file name or identifier. */ private static final Pattern ALL_UPPER_WORD = Pattern .compile("(?<= [^A-Za-z] | ^) [A-Z]+ (?= [^A-Za-z] | $)", Pattern.COMMENTS); /** Pattern for an all-lower-case word in a file name or identifier. */ // Note: Char after an all-lower word can be an upper letter (e.g. first word of camel case). private static final Pattern ALL_LOWER_WORD = Pattern .compile("(?<= [^A-Za-z] | ^) [a-z]+ (?= [^a-z] | $)", Pattern.COMMENTS); /** Pattern for a character that's not a letter nor a digit. */ private static final Pattern NON_LETTER_DIGIT = Pattern.compile("[^A-Za-z0-9]"); /** * Generates the base Java class name for the given Soy file. * @param soyFile The Soy file. * @return The generated base Java class name (without any suffixes). */ @VisibleForTesting String generateBaseClassName(SoyFileNode soyFile) { switch (this) { case SOY_FILE_NAME: String fileName = soyFile.getFileName(); if (fileName == null) { throw new IllegalArgumentException( "Trying to generate Java class name based on Soy file name, but Soy file name was" + " not provided."); } if (fileName.toLowerCase().endsWith(".soy")) { fileName = fileName.substring(0, fileName.length() - 4); } return makeUpperCamelCase(fileName); case SOY_NAMESPACE_LAST_PART: String namespace = soyFile.getNamespace(); assert namespace != null; // suppress warnings String namespaceLastPart = namespace.substring(namespace.lastIndexOf('.') + 1); return makeUpperCamelCase(namespaceLastPart); case GENERIC: return "File"; default: throw new AssertionError(); } } /** * Creates the upper camel case version of the given string (can be file name or identifier). * @param str The string to turn into upper camel case. * @return The upper camel case version of the string. */ private static String makeUpperCamelCase(String str) { str = makeWordsCapitalized(str, ALL_UPPER_WORD); str = makeWordsCapitalized(str, ALL_LOWER_WORD); str = NON_LETTER_DIGIT.matcher(str).replaceAll(""); return str; } /** * Makes all the words in the given string into capitalized format (first letter capital, rest * lower case). Words are defined by the given regex pattern. * @param str The string to process. * @param wordPattern The regex pattern for matching a word. * @return The resulting string with all words in capitalized format. */ private static String makeWordsCapitalized(String str, Pattern wordPattern) { StringBuffer sb = new StringBuffer(); Matcher wordMatcher = wordPattern.matcher(str); while (wordMatcher.find()) { String oldWord = wordMatcher.group(); StringBuilder newWord = new StringBuilder(); for (int i = 0, n = oldWord.length(); i < n; i++) { if (i == 0) { newWord.append(Character.toUpperCase(oldWord.charAt(i))); } else { newWord.append(Character.toLowerCase(oldWord.charAt(i))); } } wordMatcher.appendReplacement(sb, Matcher.quoteReplacement(newWord.toString())); } wordMatcher.appendTail(sb); return sb.toString(); } } /** The package name of the generated files. */ private final String javaPackage; /** The source of the generated Java class names. */ private final JavaClassNameSource javaClassNameSource; /** Map from Soy file node to generated Java class name (built at start of pass). */ private Map<SoyFileNode, String> soyFileToJavaClassNameMap; /** Registry of all templates in the Soy tree. */ private TemplateRegistry templateRegistry; /** Cache for results of calls to {@code Utils.convertToUpperUnderscore()}. */ private final Map<String, String> convertedIdents = Maps.newHashMap(); /** The contents of the generated JS files. */ private LinkedHashMap<String, String> generatedFiles; /** Builder for the generated code. */ private IndentedLinesBuilder ilb; /** * @param javaPackage The Java package for the generated classes. * @param javaClassNameSource Source of the generated class names. Must be one of "filename", * "namespace", or "generic". * @param errorReporter For reporting errors. */ public GenerateParseInfoVisitor(String javaPackage, String javaClassNameSource, ErrorReporter errorReporter) { super(errorReporter); this.javaPackage = javaPackage; switch (javaClassNameSource) { case "filename": this.javaClassNameSource = JavaClassNameSource.SOY_FILE_NAME; break; case "namespace": this.javaClassNameSource = JavaClassNameSource.SOY_NAMESPACE_LAST_PART; break; case "generic": this.javaClassNameSource = JavaClassNameSource.GENERIC; break; default: throw new IllegalArgumentException("Invalid value for javaClassNameSource \"" + javaClassNameSource + "\"" + " (valid values are \"filename\", \"namespace\", and \"generic\")."); } } @Override public ImmutableMap<String, String> exec(SoyNode node) { generatedFiles = Maps.newLinkedHashMap(); ilb = null; visit(node); return ImmutableMap.copyOf(generatedFiles); } // ----------------------------------------------------------------------------------------------- // Implementations for specific nodes. @Override protected void visitSoyFileSetNode(SoyFileSetNode node) { // Figure out the generated class name for each Soy file, including adding number suffixes // to resolve collisions, and then adding the common suffix "SoyInfo". Multimap<String, SoyFileNode> baseGeneratedClassNameToSoyFilesMap = HashMultimap.create(); for (SoyFileNode soyFile : node.getChildren()) { if (soyFile.getSoyFileKind() == SoyFileKind.SRC) { baseGeneratedClassNameToSoyFilesMap.put(javaClassNameSource.generateBaseClassName(soyFile), soyFile); } } soyFileToJavaClassNameMap = Maps.newHashMap(); for (String baseClassName : baseGeneratedClassNameToSoyFilesMap.keySet()) { Collection<SoyFileNode> soyFiles = baseGeneratedClassNameToSoyFilesMap.get(baseClassName); if (soyFiles.size() == 1) { for (SoyFileNode soyFile : soyFiles) { soyFileToJavaClassNameMap.put(soyFile, baseClassName + "SoyInfo"); } } else { int numberSuffix = 1; for (SoyFileNode soyFile : soyFiles) { soyFileToJavaClassNameMap.put(soyFile, baseClassName + numberSuffix + "SoyInfo"); numberSuffix++; } } } // Build template registry. templateRegistry = new TemplateRegistry(node, errorReporter); // Run the pass. for (SoyFileNode soyFile : node.getChildren()) { try { visit(soyFile); } catch (SoySyntaxException sse) { throw sse.associateMetaInfo(null, soyFile.getFilePath(), null); } } } @Override protected void visitSoyFileNode(SoyFileNode node) { if (node.getSoyFileKind() != SoyFileKind.SRC) { return; // don't generate code for deps } if (node.getFilePath() == null) { throw SoySyntaxExceptionUtils .createWithNode("In order to generate parse info, all Soy files must have paths (file name is" + " extracted from the path).", node); } String javaClassName = soyFileToJavaClassNameMap.get(node); // Collect the following: // + all the public basic templates (non-private, non-delegate) in a map from the // upper-underscore template name to the template's node, // + all the param keys from all templates (including private), // + for each param key, the list of templates that list it directly. // + for any params whose type is a proto, get the proto name and Java class name. LinkedHashMap<String, TemplateNode> publicBasicTemplateMap = Maps.newLinkedHashMap(); Set<String> allParamKeys = Sets.newHashSet(); LinkedHashMultimap<String, TemplateNode> paramKeyToTemplatesMultimap = LinkedHashMultimap.create(); SortedSet<String> protoTypes = Sets.newTreeSet(); for (TemplateNode template : node.getChildren()) { if (template.getVisibility() == Visibility.PUBLIC && template instanceof TemplateBasicNode) { publicBasicTemplateMap.put(convertToUpperUnderscore(template.getPartialTemplateName().substring(1)), template); } for (TemplateParam param : template.getAllParams()) { if (!param.isInjected()) { allParamKeys.add(param.name()); paramKeyToTemplatesMultimap.put(param.name(), template); } if (param instanceof HeaderParam) { SoyType paramType = ((HeaderParam) param).type(); findProtoTypesRecurse(paramType, protoTypes); } } new FindUsedProtoTypesVisitor(protoTypes, errorReporter).exec(template); } // allParamKeysMap is a map from upper-underscore key to original key. SortedMap<String, String> allParamKeysMap = Maps.newTreeMap(); for (String key : allParamKeys) { String upperUnderscoreKey = convertToUpperUnderscore(key); if (allParamKeysMap.containsKey(upperUnderscoreKey)) { throw SoySyntaxExceptionUtils.createWithNode("Cannot generate parse info because two param keys '" + allParamKeysMap.get(upperUnderscoreKey) + "' and '" + key + "' generate the same upper-underscore name '" + upperUnderscoreKey + "'.", node); } allParamKeysMap.put(upperUnderscoreKey, key); } ilb = new IndentedLinesBuilder(2); // ------ Header. ------ ilb.appendLine("// This file was automatically generated from ", node.getFileName(), "."); ilb.appendLine("// Please don't edit this file by hand."); ilb.appendLine(); ilb.appendLine("package ", javaPackage, ";"); ilb.appendLine(); ilb.appendLine("import com.google.common.collect.ImmutableList;"); ilb.appendLine("import com.google.common.collect.ImmutableMap;"); ilb.appendLine("import com.google.common.collect.ImmutableSortedSet;"); ilb.appendLine("import com.google.template.soy.parseinfo.SoyFileInfo;"); ilb.appendLine("import com.google.template.soy.parseinfo.SoyTemplateInfo;"); // ------ Class start. ------ ilb.appendLine(); ilb.appendLine(); appendJavadoc(ilb, "Soy parse info for " + node.getFileName() + ".", true, false); ilb.appendLine("public final class ", javaClassName, " extends SoyFileInfo {"); ilb.increaseIndent(); // ------ Constant for namespace. ------ ilb.appendLine(); ilb.appendLine(); ilb.appendLine("/** This Soy file's namespace. */"); ilb.appendLine("public static final String __NAMESPACE__ = \"", node.getNamespace(), "\";"); // ------ Proto types map. ------ if (!protoTypes.isEmpty()) { ilb.appendLine(); ilb.appendLine(); ilb.appendLine("/** Protocol buffer types used by these templates. */"); ilb.appendLine("@Override public ImmutableList<Object> getProtoTypes() {"); ilb.increaseIndent(); // Note we use fully-qualified names instead of imports to avoid potential collisions. List<String> defaultInstances = Lists.newArrayList(); defaultInstances.addAll(protoTypes); appendListOrSetHelper(ilb, "return ImmutableList.<Object>of", defaultInstances); ilb.appendLineEnd(";"); ilb.decreaseIndent(); ilb.appendLine("}"); } // ------ Template names. ------ ilb.appendLine(); ilb.appendLine(); ilb.appendLine("public static final class TemplateName {"); ilb.increaseIndent(); ilb.appendLine("private TemplateName() {}"); ilb.appendLine(); for (Entry<String, TemplateNode> templateEntry : publicBasicTemplateMap.entrySet()) { StringBuilder javadocSb = new StringBuilder(); javadocSb.append("The full template name of the ") .append(templateEntry.getValue().getPartialTemplateName()).append(" template."); appendJavadoc(ilb, javadocSb.toString(), false, true); ilb.appendLine("public static final String ", templateEntry.getKey(), " = \"", templateEntry.getValue().getTemplateName(), "\";"); } ilb.decreaseIndent(); ilb.appendLine("}"); // ------ Params. ------ ilb.appendLine(); ilb.appendLine(); ilb.appendLine("/**"); ilb.appendLine(" * Param names from all templates in this Soy file."); ilb.appendLine(" */"); ilb.appendLine("public static final class Param {"); ilb.increaseIndent(); ilb.appendLine("private Param() {}"); ilb.appendLine(); for (Map.Entry<String, String> paramEntry : allParamKeysMap.entrySet()) { String upperUnderscoreKey = paramEntry.getKey(); String key = paramEntry.getValue(); StringBuilder javadocSb = new StringBuilder(); javadocSb.append("Listed by "); boolean isFirst = true; for (TemplateNode template : paramKeyToTemplatesMultimap.get(key)) { if (isFirst) { isFirst = false; } else { javadocSb.append(", "); } javadocSb.append(buildTemplateNameForJavadoc(node, template)); } javadocSb.append('.'); appendJavadoc(ilb, javadocSb.toString(), false, true); ilb.appendLine("public static final String ", upperUnderscoreKey, " = \"", key, "\";"); } ilb.decreaseIndent(); ilb.appendLine("}"); // ------ Templates. ------ for (TemplateNode template : publicBasicTemplateMap.values()) { try { visit(template); } catch (SoySyntaxException sse) { throw sse.associateMetaInfo(null, null, template.getTemplateNameForUserMsgs()); } } // ------ Constructor. ------ ilb.appendLine(); ilb.appendLine(); ilb.appendLine("private ", javaClassName, "() {"); ilb.increaseIndent(); ilb.appendLine("super("); ilb.increaseIndent(2); ilb.appendLine("\"", node.getFileName(), "\","); ilb.appendLine("\"", node.getNamespace(), "\","); // Templates. List<String> itemSnippets = Lists.newArrayList(); itemSnippets.addAll(publicBasicTemplateMap.keySet()); appendImmutableList(ilb, "<SoyTemplateInfo>", itemSnippets); ilb.appendLineEnd(","); // CSS names. SortedMap<String, CssTagsPrefixPresence> cssNameMap = new CollectCssNamesVisitor(errorReporter).exec(node); List<Pair<String, String>> entrySnippetPairs = Lists.newArrayList(); for (Map.Entry<String, CssTagsPrefixPresence> entry : cssNameMap.entrySet()) { entrySnippetPairs .add(Pair.of("\"" + entry.getKey() + "\"", "CssTagsPrefixPresence." + entry.getValue().name())); } appendImmutableMap(ilb, "<String, CssTagsPrefixPresence>", entrySnippetPairs); ilb.appendLineEnd(");"); ilb.decreaseIndent(2); ilb.decreaseIndent(); ilb.appendLine("}"); // ------ Singleton instance and its getter. ------ ilb.appendLine(); ilb.appendLine(); ilb.appendLine("private static final ", javaClassName, " __INSTANCE__ ="); ilb.increaseIndent(2); ilb.appendLine("new ", javaClassName, "();"); ilb.decreaseIndent(2); ilb.appendLine(); ilb.appendLine("public static ", javaClassName, " getInstance() {"); ilb.increaseIndent(); ilb.appendLine("return __INSTANCE__;"); ilb.decreaseIndent(); ilb.appendLine("}"); // ------ Class end. ------ ilb.appendLine(); ilb.decreaseIndent(); ilb.appendLine("}"); generatedFiles.put(javaClassName + ".java", ilb.toString()); ilb = null; } @Override protected void visitTemplateNode(TemplateNode node) { // Don't generate anything for private or delegate templates. if (node.getVisibility() == Visibility.LEGACY_PRIVATE || node instanceof TemplateDelegateNode) { return; } // First build list of all transitive params (direct and indirect). LinkedHashMap<String, TemplateParam> transitiveParamMap = Maps.newLinkedHashMap(); // Direct params. for (TemplateParam param : node.getParams()) { transitiveParamMap.put(param.name(), param); } // Indirect params. IndirectParamsInfo indirectParamsInfo = new FindIndirectParamsVisitor(templateRegistry, errorReporter) .exec(node); for (TemplateParam param : indirectParamsInfo.indirectParams.values()) { TemplateParam existingParam = transitiveParamMap.get(param.name()); if (existingParam == null) { // Note: We don't list the description for indirect params. transitiveParamMap.put(param.name(), param.copyEssential()); } } // Get info on injected params. IjParamsInfo ijParamsInfo = new FindIjParamsVisitor(templateRegistry, errorReporter).exec(node); @SuppressWarnings("ConstantConditions") // for IntelliJ String upperUnderscoreName = convertToUpperUnderscore(node.getPartialTemplateName().substring(1)); String templateInfoClassName = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, upperUnderscoreName) + "SoyTemplateInfo"; // ------ *SoyTemplateInfo class start. ------ ilb.appendLine(); ilb.appendLine(); appendJavadoc(ilb, node.getSoyDocDesc(), true, false); ilb.appendLine("public static final class ", templateInfoClassName, " extends SoyTemplateInfo {"); ilb.increaseIndent(); // ------ Constants for template name. ------ ilb.appendLine(); ilb.appendLine("/** This template's full name. */"); ilb.appendLine("public static final String __NAME__ = \"", node.getTemplateName(), "\";"); ilb.appendLine("/** This template's partial name. */"); ilb.appendLine("public static final String __PARTIAL_NAME__ = \"", node.getPartialTemplateName(), "\";"); // ------ Param constants. ------ boolean hasSeenFirstDirectParam = false; boolean hasSwitchedToIndirectParams = false; for (TemplateParam param : transitiveParamMap.values()) { if (param.desc() != null) { // Direct param. if (!hasSeenFirstDirectParam) { ilb.appendLine(); hasSeenFirstDirectParam = true; } appendJavadoc(ilb, param.desc(), false, false); } else { // Indirect param. if (!hasSwitchedToIndirectParams) { ilb.appendLine(); ilb.appendLine("// Indirect params."); hasSwitchedToIndirectParams = true; } // Get the list of all transitive callee names as they will appear in the generated // Javadoc (possibly containing both partial and full names) and sort them before // generating the Javadoc. SortedSet<String> sortedJavadocCalleeNames = Sets.newTreeSet(); for (TemplateNode transitiveCallee : indirectParamsInfo.paramKeyToCalleesMultimap .get(param.name())) { String javadocCalleeName = buildTemplateNameForJavadoc(node.getParent(), transitiveCallee); sortedJavadocCalleeNames.add(javadocCalleeName); } // Generate the Javadoc. StringBuilder javadocSb = new StringBuilder(); javadocSb.append("Listed by "); boolean isFirst = true; for (String javadocCalleeName : sortedJavadocCalleeNames) { if (isFirst) { isFirst = false; } else { javadocSb.append(", "); } javadocSb.append(javadocCalleeName); } javadocSb.append('.'); appendJavadoc(ilb, javadocSb.toString(), false, true); } // The actual param field. ilb.appendLine("public static final String ", convertToUpperUnderscore(param.name()), " = \"", param.name(), "\";"); } // ------ Constructor. ------ ilb.appendLine(); ilb.appendLine("private ", templateInfoClassName, "() {"); ilb.increaseIndent(); ilb.appendLine("super("); ilb.increaseIndent(2); ilb.appendLine("\"", node.getTemplateName(), "\","); if (!transitiveParamMap.isEmpty()) { List<Pair<String, String>> entrySnippetPairs = Lists.newArrayList(); for (TemplateParam param : transitiveParamMap.values()) { entrySnippetPairs.add(Pair.of("\"" + param.name() + "\"", param.isRequired() ? "ParamRequisiteness.REQUIRED" : "ParamRequisiteness.OPTIONAL")); } appendImmutableMap(ilb, "<String, ParamRequisiteness>", entrySnippetPairs); ilb.appendLineEnd(","); } else { ilb.appendLine("ImmutableMap.<String, ParamRequisiteness>of(),"); } appendIjParamSet(ilb, ijParamsInfo); ilb.appendLineEnd(");"); ilb.decreaseIndent(2); ilb.decreaseIndent(); ilb.appendLine("}"); // ------ Singleton instance and its getter. ------ ilb.appendLine(); ilb.appendLine("private static final ", templateInfoClassName, " __INSTANCE__ ="); ilb.increaseIndent(2); ilb.appendLine("new ", templateInfoClassName, "();"); ilb.decreaseIndent(2); ilb.appendLine(); ilb.appendLine("public static ", templateInfoClassName, " getInstance() {"); ilb.increaseIndent(); ilb.appendLine("return __INSTANCE__;"); ilb.decreaseIndent(); ilb.appendLine("}"); // ------ *SoyTemplateInfo class end. ------ ilb.decreaseIndent(); ilb.appendLine("}"); // ------ Static field with instance of *SoyTemplateInfo class. ------ ilb.appendLine(); ilb.appendLine("/** Same as ", templateInfoClassName, ".getInstance(). */"); ilb.appendLine("public static final ", templateInfoClassName, " ", upperUnderscoreName, " ="); ilb.increaseIndent(2); ilb.appendLine(templateInfoClassName, ".getInstance();"); ilb.decreaseIndent(2); } /** * Private helper for visitSoyFileNode() and visitTemplateNode() to convert an identifier to upper * underscore format. * * <p>We simply dispatch to Utils.convertToUpperUnderscore() to do the actual conversion. * The reason for the existence of this method is that we cache all results of previous * invocations in this pass because this method is expected to be called for the same identifier * multiple times. * * @param ident The identifier to convert. * @return The identifier in upper underscore format. */ private String convertToUpperUnderscore(String ident) { String result = convertedIdents.get(ident); if (result == null) { result = BaseUtils.convertToUpperUnderscore(ident); convertedIdents.put(ident, result); } return result; } /** * Recursively search for protocol buffer types within the given type. * @param type The type to search. * @param protoTypes Output set. */ private static void findProtoTypesRecurse(SoyType type, SortedSet<String> protoTypes) { if (type instanceof SoyProtoType) { protoTypes.add(((SoyProtoType) type).getDescriptorExpression()); } else { switch (type.getKind()) { case UNION: for (SoyType member : ((UnionType) type).getMembers()) { findProtoTypesRecurse(member, protoTypes); } break; case LIST: { ListType listType = (ListType) type; findProtoTypesRecurse(listType.getElementType(), protoTypes); break; } case MAP: { MapType mapType = (MapType) type; findProtoTypesRecurse(mapType.getKeyType(), protoTypes); findProtoTypesRecurse(mapType.getValueType(), protoTypes); break; } case RECORD: { RecordType recordType = (RecordType) type; for (SoyType fieldType : recordType.getMembers().values()) { findProtoTypesRecurse(fieldType, protoTypes); } break; } } } } /** * Private helper for visitSoyFileNode() and visitTemplateNode() to append a Javadoc comment to * the code being built. * * @param ilb The builder for the code. * @param doc The doc string to append as the content of a Javadoc comment. The Javadoc format * will follow the usual conventions. Important: If the doc string is multiple lines, the * line separator must be '\n'. * @param forceMultiline If true, we always generate a multiline Javadoc comment even if the doc * string only has one line. If false, we generate either a single line or multiline Javadoc * comment, depending on the doc string. * @param wrapAt100Chars If true, wrap at 100 chars. */ @VisibleForTesting static void appendJavadoc(IndentedLinesBuilder ilb, String doc, boolean forceMultiline, boolean wrapAt100Chars) { if (wrapAt100Chars) { // Actual wrap length is less because of indent and because of space used by Javadoc chars. int wrapLen = 100 - ilb.getCurrIndentLen() - 7; List<String> wrappedLines = Lists.newArrayList(); for (String line : Splitter.on('\n').split(doc)) { while (line.length() > wrapLen) { int spaceIndex = line.lastIndexOf(' ', wrapLen); if (spaceIndex >= 0) { wrappedLines.add(line.substring(0, spaceIndex)); line = line.substring(spaceIndex + 1); // add 1 to skip the space } else { // No spaces. Just wrap at wrapLen. wrappedLines.add(line.substring(0, wrapLen)); line = line.substring(wrapLen); } } wrappedLines.add(line); } doc = Joiner.on("\n").join(wrappedLines); } if (doc.contains("\n") || forceMultiline) { // Multiline. ilb.appendLine("/**"); for (String line : Splitter.on('\n').split(doc)) { ilb.appendLine(" * ", line); } ilb.appendLine(" */"); } else { // One line. ilb.appendLine("/** ", doc, " */"); } } /** * Private helper for visitTemplateNode() to append the set of injected params. * * @param ilb The builder for the code. * @param ijParamsInfo Info on injected params for the template being processed. */ private void appendIjParamSet(IndentedLinesBuilder ilb, IjParamsInfo ijParamsInfo) { List<String> itemSnippets = Lists.newArrayList(); for (String paramKey : ijParamsInfo.ijParamSet) { itemSnippets.add("\"" + paramKey + "\""); } appendImmutableSortedSet(ilb, "<String>", itemSnippets); } // ----------------------------------------------------------------------------------------------- // General helpers. /** * Private helper to build the human-readable string for referring to a template in the generated * code's javadoc. * @param currSoyFile The current Soy file for which we're generating parse-info code. * @param template The template that we want to refer to in the generated javadoc. Note that this * template may not be in the current Soy file. * @return The human-readable string for referring to the given template in the generated code's * javadoc. */ private static String buildTemplateNameForJavadoc(SoyFileNode currSoyFile, TemplateNode template) { StringBuilder resultSb = new StringBuilder(); if (template.getParent() == currSoyFile && !(template instanceof TemplateDelegateNode)) { resultSb.append(template.getPartialTemplateName()); } else { resultSb.append(template.getTemplateNameForUserMsgs()); } if (template.getVisibility() == Visibility.LEGACY_PRIVATE) { resultSb.append(" (private)"); } if (template instanceof TemplateDelegateNode) { resultSb.append(" (delegate)"); } return resultSb.toString(); } /** * Private helper to append an ImmutableList to the code. * * @param ilb The builder for the code. * @param typeParamSnippet The type parameter for the ImmutableList. * @param itemSnippets Code snippets for the items to put into the ImmutableList. */ private static void appendImmutableList(IndentedLinesBuilder ilb, String typeParamSnippet, Collection<String> itemSnippets) { appendListOrSetHelper(ilb, "ImmutableList." + typeParamSnippet + "of", itemSnippets); } /** * Private helper to append an ImmutableSortedSet to the code. * * @param ilb The builder for the code. * @param typeParamSnippet The type parameter for the ImmutableSortedSet. * @param itemSnippets Code snippets for the items to put into the ImmutableSortedSet. */ private static void appendImmutableSortedSet(IndentedLinesBuilder ilb, String typeParamSnippet, Collection<String> itemSnippets) { appendListOrSetHelper(ilb, "ImmutableSortedSet." + typeParamSnippet + "of", itemSnippets); } /** * Private helper for appendImmutableList() and appendImmutableSortedSet(). * * @param ilb The builder for the code. * @param creationFunctionSnippet Code snippet for the qualified name of the list or set creation * function (without trailing parentheses). * @param itemSnippets Code snippets for the items to put into the list or set. */ private static void appendListOrSetHelper(IndentedLinesBuilder ilb, String creationFunctionSnippet, Collection<String> itemSnippets) { if (itemSnippets.isEmpty()) { ilb.appendLineStart(creationFunctionSnippet, "()"); } else { ilb.appendLine(creationFunctionSnippet, "("); boolean isFirst = true; for (String item : itemSnippets) { if (isFirst) { isFirst = false; } else { ilb.appendLineEnd(","); } ilb.appendLineStart(" ", item); } ilb.append(")"); } } /** * Private helper to append an ImmutableMap to the code. * * @param ilb The builder for the code. * @param typeParamSnippet The type parameter for the ImmutableMap. * @param entrySnippetPairs Pairs of (key, value) code snippets for the entries to put into the * ImmutableMap. */ private static void appendImmutableMap(IndentedLinesBuilder ilb, String typeParamSnippet, Collection<Pair<String, String>> entrySnippetPairs) { if (entrySnippetPairs.isEmpty()) { ilb.appendLineStart("ImmutableMap.", typeParamSnippet, "of()"); } else { ilb.appendLine("ImmutableMap.", typeParamSnippet, "builder()"); for (Pair<String, String> entrySnippetPair : entrySnippetPairs) { ilb.appendLine(" .put(", entrySnippetPair.first, ", ", entrySnippetPair.second, ")"); } ilb.appendLineStart(" .build()"); } } // ----------------------------------------------------------------------------------------------- // Helper visitor to collect CSS names. /** * Private helper class for visitSoyFileNode() to collect all the CSS names appearing in a file. * * <p>The return value of exec() is a map from each CSS name appearing in the given node's subtree * to its CssTagsPrefixPresence state. */ private static class CollectCssNamesVisitor extends AbstractSoyNodeVisitor<SortedMap<String, CssTagsPrefixPresence>> { /** Map from each CSS name to its CssTagsPrefixPresence state. */ private SortedMap<String, CssTagsPrefixPresence> cssNamesMap; private CollectCssNamesVisitor(ErrorReporter errorReporter) { super(errorReporter); cssNamesMap = Maps.newTreeMap(); } @Override public SortedMap<String, CssTagsPrefixPresence> exec(SoyNode node) { visit(node); return cssNamesMap; } @Override protected void visitCssNode(CssNode node) { String cssName = node.getSelectorText(); CssTagsPrefixPresence existingCssTagsPrefixPresence = cssNamesMap.get(cssName); CssTagsPrefixPresence additionalCssTagsPrefixPresence = (node.getComponentNameExpr() == null) ? CssTagsPrefixPresence.NEVER : CssTagsPrefixPresence.ALWAYS; if (existingCssTagsPrefixPresence == null) { cssNamesMap.put(cssName, additionalCssTagsPrefixPresence); } else if (existingCssTagsPrefixPresence != additionalCssTagsPrefixPresence) { cssNamesMap.put(cssName, CssTagsPrefixPresence.SOMETIMES); } else { // Nothing to change. } } @Override protected void visitSoyNode(SoyNode node) { if (node instanceof ParentSoyNode<?>) { visitChildren((ParentSoyNode<?>) node); } } } // ----------------------------------------------------------------------------------------------- // Helper visitor to collect CSS names. /** * Private helper class for visitSoyFileNode() to collect all of the proto * extension types used in the template. */ private static class FindUsedProtoTypesVisitor extends AbstractSoyNodeVisitor<Void> { private final SortedSet<String> protoTypes; private FindUsedProtoTypesVisitor(SortedSet<String> protoTypes, ErrorReporter errorReporter) { super(errorReporter); this.protoTypes = protoTypes; } @Override public Void exec(SoyNode node) { visit(node); return null; } @Override protected void visitSoyNode(SoyNode node) { if (node instanceof ExprHolderNode) { visitExpressions((ExprHolderNode) node); } if (node instanceof ParentSoyNode<?>) { visitChildren((ParentSoyNode<?>) node); } } private void visitExpressions(ExprHolderNode node) { FindUsedProtoTypesExprVisitor exprVisitor = new FindUsedProtoTypesExprVisitor(protoTypes, errorReporter); for (ExprUnion exprUnion : node.getAllExprUnions()) { if (exprUnion.getExpr() != null) { exprVisitor.exec(exprUnion.getExpr()); } } } } /** * Private helper class to collect all of the proto extension types used in * an expression. */ private static final class FindUsedProtoTypesExprVisitor extends AbstractExprNodeVisitor<Void> { private final SortedSet<String> protoTypes; FindUsedProtoTypesExprVisitor(SortedSet<String> protoTypes, ErrorReporter errorReporter) { super(errorReporter); this.protoTypes = protoTypes; } @Override protected void visitExprRootNode(ExprRootNode node) { visitChildren(node); ExprNode expr = node.getRoot(); node.setType(expr.getType()); } @Override protected void visitExprNode(ExprNode node) { if (node instanceof ParentExprNode) { visitChildren((ParentExprNode) node); } } @Override protected void visitFieldAccessNode(FieldAccessNode node) { visit(node.getBaseExprChild()); SoyType baseType = node.getBaseExprChild().getType(); if (baseType instanceof SoyObjectType) { SoyObjectType objectType = (SoyObjectType) baseType; Set<String> importedNames = objectType.getFieldAccessImports(node.getFieldName(), SoyBackendKind.TOFU); protoTypes.addAll(importedNames); } } } }