Java tutorial
/* * Copyright 2013 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.soytree; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.template.soy.base.internal.BaseUtils; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.SoyError; import com.google.template.soy.exprtree.DataAccessNode; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.FieldAccessNode; import com.google.template.soy.exprtree.GlobalNode; import com.google.template.soy.exprtree.IntegerNode; import com.google.template.soy.exprtree.ItemAccessNode; import com.google.template.soy.exprtree.VarRefNode; import java.util.List; /** * Static helpers for generating base names for msg substitution units (i.e. placeholder names and * plural/select vars). * * <p> Important: Do not use outside of Soy code (treat as superpackage-private). * */ public final class MsgSubstUnitBaseVarNameUtils { private static final SoyError COLLIDING_EXPRESSIONS = SoyError.of( "Cannot generate noncolliding base names for vars. " + "Colliding expressions: ''{0}'' and ''{1}''. " + "Add explicit base names with the ''phname'' attribute."); // Disallow instantiation. private MsgSubstUnitBaseVarNameUtils() { } /** * Helper function to generate a base placeholder (or plural/select var) name from an expression, * using the naive algorithm. * * If the expression is a data ref or global, then the last key (if any) is used as the base * placeholder name. Otherwise, the fallback name is used. * * Examples: $aaaBbb -> AAA_BBB; $aaa_bbb -> AAA_BBB; $aaa.bbb -> BBB; $ij.aaa -> AAA; * aaa.BBB -> BBB; $aaa.0 -> fallback; $aaa[0] -> fallback; $aaa[0].bbb -> BBB; * length($aaa) -> fallback; $aaa + 1 -> fallback * * @param exprNode The root node for an expression. * @param fallbackBaseName The fallback base name. * * @return The base placeholder (or plural/select var) name for the given expression. */ public static String genNaiveBaseNameForExpr(ExprNode exprNode, String fallbackBaseName) { if (exprNode instanceof VarRefNode) { return BaseUtils.convertToUpperUnderscore(((VarRefNode) exprNode).getName()); } else if (exprNode instanceof FieldAccessNode) { return BaseUtils.convertToUpperUnderscore(((FieldAccessNode) exprNode).getFieldName()); } else if (exprNode instanceof GlobalNode) { String globalName = ((GlobalNode) exprNode).getName(); return BaseUtils.convertToUpperUnderscore(BaseUtils.extractPartAfterLastDot(globalName)); } return fallbackBaseName; } /** * The equivalent of {@code genNaiveBaseNameForExpr()} in our new algorithm for generating base * name. * * Examples: $aaaBbb -> AAA_BBB; $aaa_bbb -> AAA_BBB; $aaa.bbb -> BBB; $ij.aaa -> AAA; * aaa.BBB -> BBB; $aaa.0 -> AAA_0; $aaa[0] -> AAA_0; $aaa[0].bbb -> BBB; length($aaa) -> * fallback; $aaa + 1 -> fallback * * @param exprNode The expr root of the expression to generate the shortest base name for. * @param fallbackBaseName The fallback base name to use if the given expression doesn't generate * any base names. * @return The generated base name. */ public static String genShortestBaseNameForExpr(ExprNode exprNode, String fallbackBaseName) { List<String> candidateBaseNames = genCandidateBaseNamesForExpr(exprNode); return Iterables.getFirst(candidateBaseNames, fallbackBaseName); } /** * Generates base names for all the expressions in a list, where for each expression, we use the * shortest candidate base name that does not collide with any of the candidate base names * generated from other expressions in the list. Two candidate base names are considered to * collide if they are identical or if one is a suffix of the other beginning after an underscore * character. * * For example, given the expressions $userGender and $target.gender, the generated base names * would be USER_GENDER and TARGET_GENDER. (Even though the shortest candidate base names are * USER_GENDER and GENDER, the latter one is not used since it collides with the former one.) * * Note: We prefer the shorter candidate base names when possible, because the translator usually * doesn't care about all the names. E.g. $data.actionTargets[0].personInfo.gender turns into * GENDER as opposed to DATA_ACTION_TARGETS_0_PERSON_INFO_GENDER, which is overkill and probably * more confusing. Another reason is that refactorings that change higher-level names should not * change messages unnecessarily. E.g. a refactoring that changes * $data.actionTargets[0].personInfo.gender -> $userData.actionTargets[0].personInfo.gender * should not change the placeholder name. * * @param exprNodes The expr nodes of the expressions to generate noncolliding base names for. * @param fallbackBaseName The fallback base name. * @param errorReporter For reporting collision errors. * @return The list of generated noncolliding base names. */ public static List<String> genNoncollidingBaseNamesForExprs(List<ExprNode> exprNodes, String fallbackBaseName, ErrorReporter errorReporter) { int numExprs = exprNodes.size(); // --- Compute candidate base names for each expression. --- List<List<String>> candidateBaseNameLists = Lists.newArrayListWithCapacity(numExprs); for (ExprNode exprRoot : exprNodes) { candidateBaseNameLists.add(genCandidateBaseNamesForExpr(exprRoot)); } // --- Build a multiset of collision strings (if key has > 1 values, then it's a collision). --- // Note: We could combine this loop with the previous loop, but it's more readable this way. Multimap<String, ExprNode> collisionStrToLongestCandidatesMultimap = HashMultimap.create(); for (int i = 0; i < numExprs; i++) { ExprNode exprRoot = exprNodes.get(i); List<String> candidateBaseNameList = candidateBaseNameLists.get(i); if (candidateBaseNameList.isEmpty()) { continue; } String longestCandidate = candidateBaseNameList.get(candidateBaseNameList.size() - 1); // Add longest candidate as a collision string. collisionStrToLongestCandidatesMultimap.put(longestCandidate, exprRoot); // Add all suffixes that begin after an underscore char as collision strings. for (int j = 0, n = longestCandidate.length(); j < n; j++) { if (longestCandidate.charAt(j) == '_') { collisionStrToLongestCandidatesMultimap.put(longestCandidate.substring(j + 1), exprRoot); } } } // --- Find the shortest noncolliding candidate base name for each expression. --- List<String> noncollidingBaseNames = Lists.newArrayListWithCapacity(numExprs); OUTER: for (int i = 0; i < numExprs; i++) { List<String> candidateBaseNameList = candidateBaseNameLists.get(i); if (!candidateBaseNameList.isEmpty()) { // Has candidates: Use the shortest candidate that doesn't collide. for (String candidateBaseName : candidateBaseNameList) { if (collisionStrToLongestCandidatesMultimap.get(candidateBaseName).size() == 1) { // Found candidate with 1 value in multimap, which means no collision. noncollidingBaseNames.add(candidateBaseName); continue OUTER; } } // Did not find any candidate with no collision. ExprNode exprRoot = exprNodes.get(i); String longestCandidate = candidateBaseNameList.get(candidateBaseNameList.size() - 1); ExprNode collidingExprRoot = null; for (ExprNode er : collisionStrToLongestCandidatesMultimap.get(longestCandidate)) { if (er != exprRoot) { collidingExprRoot = er; break; } } assert collidingExprRoot != null; errorReporter.report(collidingExprRoot.getSourceLocation(), COLLIDING_EXPRESSIONS, exprRoot.toSourceString(), collidingExprRoot.toSourceString()); return noncollidingBaseNames; } else { // No candidates: Use fallback. noncollidingBaseNames.add(fallbackBaseName); } } return noncollidingBaseNames; } /** * Private helper for {@code genShortestBaseNameForExpr()} and {@code * genNoncollidingBaseNamesForExprs()}. * * Given an expression that's a data ref or a global, generates the list of all possible base * names, from short to long. Shortest contains only the last key. Longest contains up to the * first key (unless there are accesses using expressions to compute non-static keys, in which * case we cannot generate any more base names). If no base names can be generated for the given * expression (i.e. if the expression is not a data ref or global, or the last key is non-static), * then returns empty list. * * For example, given $aaa[0].bbb.cccDdd, generates the list ["CCC_DDD", "BBB_CCC_DDD", * "AAA_0_BBB_CCC_DDD"]. One the other hand, given $aaa['xxx'], generates the empty list (because * ['xxx'] parses to a DataRefAccessExprNode). * * @param exprNode The expr root of the expression to generate all candidate base names for. * @return The list of all candidate base names, from shortest to longest. */ @VisibleForTesting static List<String> genCandidateBaseNamesForExpr(ExprNode exprNode) { if (exprNode instanceof VarRefNode || exprNode instanceof DataAccessNode) { List<String> baseNames = Lists.newArrayList(); String baseName = null; while (exprNode != null) { String nameSegment = null; if (exprNode instanceof VarRefNode) { nameSegment = ((VarRefNode) exprNode).getName(); exprNode = null; } else if (exprNode instanceof FieldAccessNode) { FieldAccessNode fieldAccess = (FieldAccessNode) exprNode; nameSegment = fieldAccess.getFieldName(); exprNode = fieldAccess.getBaseExprChild(); } else if (exprNode instanceof ItemAccessNode) { ItemAccessNode itemAccess = (ItemAccessNode) exprNode; exprNode = itemAccess.getBaseExprChild(); if (itemAccess.getKeyExprChild() instanceof IntegerNode) { // Prefix with index, but don't add to baseNames list since it's not a valid ident. IntegerNode keyValue = (IntegerNode) itemAccess.getKeyExprChild(); if (keyValue.getValue() < 0) { // Stop if we encounter an invalid key. break; } nameSegment = Integer.toString(keyValue.getValue()); baseName = BaseUtils.convertToUpperUnderscore(nameSegment) + ((baseName != null) ? "_" + baseName : ""); continue; } else { break; // Stop if we encounter a non-static key } } else { break; // Stop if we encounter an expression that is not representable as a name. } baseName = BaseUtils.convertToUpperUnderscore(nameSegment) + ((baseName != null) ? "_" + baseName : ""); baseNames.add(baseName); // new candidate base name whenever we encounter a key } return baseNames; } else if (exprNode instanceof GlobalNode) { String[] globalNameParts = ((GlobalNode) exprNode).getName().split("\\."); List<String> baseNames = Lists.newArrayList(); String baseName = null; for (int i = globalNameParts.length - 1; i >= 0; i--) { baseName = BaseUtils.convertToUpperUnderscore(globalNameParts[i]) + ((baseName != null) ? "_" + baseName : ""); baseNames.add(baseName); } return baseNames; } else { // We don't handle expressions other than data refs and globals. return ImmutableList.of(); } } }