• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Dagger Authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package dagger.internal.codegen.validation;
18 
19 import static java.util.Comparator.comparing;
20 
21 import com.google.common.base.Joiner;
22 import com.google.common.base.Splitter;
23 import com.google.common.base.Strings;
24 import com.google.common.collect.HashMultimap;
25 import com.google.common.collect.ImmutableSet;
26 import com.google.common.collect.Iterables;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.TreeMap;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 
37 /**
38  * Munges an error message to remove/shorten package names and adds a legend at the end.
39  */
40 final class PackageNameCompressor {
41 
42   static final String LEGEND_HEADER =
43       "\n\n======================\nFull classname legend:\n======================\n";
44   static final String LEGEND_FOOTER =
45       "========================\nEnd of classname legend:\n========================\n";
46 
47   private static final ImmutableSet<String> PACKAGES_SKIPPED_IN_LEGEND = ImmutableSet.of(
48       "java.lang.",
49       "java.util.");
50 
51   private static final Splitter PACKAGE_SPLITTER = Splitter.on('.');
52 
53   private static final Joiner PACKAGE_JOINER = Joiner.on('.');
54 
55   // TODO(erichang): Consider validating this regex by also passing in all of the known types from
56   // keys, module names, component names, etc and checking against that list. This may have some
57   // extra complications with taking apart types like List<Foo> to get the inner class names.
58   private static final Pattern CLASSNAME_PATTERN =
59       // Match lowercase package names with trailing dots. Start with a non-word character so we
60       // don't match substrings in like Bar.Foo and match the ar.Foo. Start a group to not include
61       // the non-word character.
62       Pattern.compile("[\\W](([a-z_0-9]++[.])++"
63           // Then match a name starting with an uppercase letter. This is the outer class name.
64           + "[A-Z][\\w$]++)");
65 
66   /**
67    * Compresses an error message by stripping the packages out of class names and adding them
68    * to a legend at the bottom of the error.
69    */
compressPackagesInMessage(String input)70   static String compressPackagesInMessage(String input) {
71     Matcher matcher = CLASSNAME_PATTERN.matcher(input);
72 
73     Set<String> names = new HashSet<>();
74     // Find all classnames in the error. Note that if our regex isn't complete, it just means the
75     // classname is left in the full form, which is a fine fallback.
76     while (matcher.find()) {
77       String name = matcher.group(1);
78       names.add(name);
79     }
80     // Now dedupe any conflicts. Use a TreeMap since we're going to need the legend sorted anyway.
81     // This map is from short name to full name.
82     Map<String, String> replacementMap = shortenNames(names);
83 
84     // If we have nothing to replace, just return the original.
85     if (replacementMap.isEmpty()) {
86       return input;
87     }
88 
89     // Find the longest key for building the legend
90     int longestKey = replacementMap.keySet().stream().max(comparing(String::length)).get().length();
91 
92     String replacedString = input;
93     StringBuilder legendBuilder = new StringBuilder();
94     for (Map.Entry<String, String> entry : replacementMap.entrySet()) {
95       String shortName = entry.getKey();
96       String fullName = entry.getValue();
97       // Do the replacements in the message
98       replacedString = replacedString.replace(fullName, shortName);
99 
100       // Skip certain prefixes. We need to check the shortName for a . though in case
101       // there was some type of conflict like java.util.concurrent.Future and
102       // java.util.foo.Future that got shortened to concurrent.Future and foo.Future.
103       // In those cases we do not want to skip the legend. We only skip if the class
104       // is directly in that package.
105       String prefix = fullName.substring(0, fullName.length() - shortName.length());
106       if (PACKAGES_SKIPPED_IN_LEGEND.contains(prefix) && !shortName.contains(".")) {
107         continue;
108       }
109 
110       // Add to the legend
111       legendBuilder
112           .append(shortName)
113           .append(": ")
114           // Add enough spaces to adjust the columns
115           .append(Strings.repeat(" ", longestKey - shortName.length()))
116           .append(fullName)
117           .append("\n");
118     }
119 
120     return legendBuilder.length() == 0 ? replacedString
121         : replacedString + LEGEND_HEADER + legendBuilder + LEGEND_FOOTER;
122   }
123 
124   /**
125    * Returns a map from short name to full name after resolving conflicts. This resolves conflicts
126    * by adding on segments of the package name until they are unique. For example, com.foo.Baz and
127    * com.bar.Baz will conflict on Baz and then resolve with foo.Baz and bar.Baz as replacements.
128    */
shortenNames(Collection<String> names)129   private static Map<String, String> shortenNames(Collection<String> names) {
130     HashMultimap<String, List<String>> shortNameToPartsMap = HashMultimap.create();
131     for (String name : names) {
132       List<String> parts = new ArrayList<>(PACKAGE_SPLITTER.splitToList(name));
133       // Start with the just the class name as the simple name
134       String className = parts.remove(parts.size() - 1);
135       shortNameToPartsMap.put(className, parts);
136     }
137 
138     // Iterate through looking for conflicts adding the next part of the package until there are no
139     // more conflicts
140     while (true) {
141       // Save the keys with conflicts to avoid concurrent modification issues
142       List<String> conflictingShortNames = new ArrayList<>();
143       for (Map.Entry<String, Collection<List<String>>> entry
144           : shortNameToPartsMap.asMap().entrySet()) {
145         if (entry.getValue().size() > 1) {
146           conflictingShortNames.add(entry.getKey());
147         }
148       }
149 
150       if (conflictingShortNames.isEmpty()) {
151         break;
152       }
153 
154       // For all conflicts, add in the next part of the package
155       for (String conflictingShortName : conflictingShortNames) {
156         Set<List<String>> partsCollection = shortNameToPartsMap.removeAll(conflictingShortName);
157         for (List<String> parts : partsCollection) {
158           String newShortName = parts.remove(parts.size() - 1) + "." + conflictingShortName;
159           // If we've removed the last part of the package, then just skip it entirely because
160           // now we're not shortening it at all.
161           if (!parts.isEmpty()) {
162             shortNameToPartsMap.put(newShortName, parts);
163           }
164         }
165       }
166     }
167 
168     // Turn the multimap into a regular map now that conflicts have been resolved. Use a TreeMap
169     // since we're going to need the legend sorted anyway. This map is from short name to full name.
170     Map<String, String> replacementMap = new TreeMap<>();
171     for (Map.Entry<String, Collection<List<String>>> entry
172         : shortNameToPartsMap.asMap().entrySet()) {
173       replacementMap.put(
174           entry.getKey(),
175           PACKAGE_JOINER.join(Iterables.getOnlyElement(entry.getValue())) + "." + entry.getKey());
176     }
177     return replacementMap;
178   }
179 
PackageNameCompressor()180   private PackageNameCompressor() {}
181 }
182