aboutsummaryrefslogtreecommitdiff
path: root/java/dagger/hilt/processor/internal/ComponentNames.java
blob: eeaa5f44b5cc6261958fe18d852b1842ccd0c7b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/*
 * Copyright (C) 2020 The Dagger 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 dagger.hilt.processor.internal;

import static java.lang.Character.isUpperCase;
import static java.lang.String.format;
import static java.util.Comparator.comparing;

import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimaps;
import com.squareup.javapoet.ClassName;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;

/**
 * Utility class for getting the generated component name.
 *
 * <p>This should not be used externally.
 */
public final class ComponentNames {
  private static final Splitter QUALIFIED_NAME_SPLITTER = Splitter.on('.');

  private final boolean renameTestComponents;
  private final String destinationPackage;
  private final ImmutableMap<ClassName, String> simpleNameByClassName;

  public static ComponentNames withoutRenaming() {
    return new ComponentNames(
        /*renameTestComponents=*/ false, /*destinationPackage=*/ null, ImmutableMap.of());
  }

  public static ComponentNames withRenamingIntoPackage(
      String destinationPackage, ImmutableList<TypeElement> roots) {
    ImmutableMap.Builder<ClassName, String> builder = ImmutableMap.builder();
    ImmutableListMultimap<String, TypeElement> rootsBySimpleName =
        Multimaps.index(roots, typeElement -> typeElement.getSimpleName().toString());
    rootsBySimpleName.asMap().values().stream()
        .map(ComponentNames::disambiguateConflictingSimpleNames)
        .forEach(builder::putAll);
    return new ComponentNames(/*renameTestComponents=*/ true, destinationPackage, builder.build());
  }

  private ComponentNames(
      boolean renameTestComponents,
      String destinationPackage,
      ImmutableMap<ClassName, String> simpleNameByClassName) {
    this.renameTestComponents = renameTestComponents;
    this.destinationPackage = destinationPackage;
    this.simpleNameByClassName = simpleNameByClassName;
  }

  /** Returns the name of the generated component wrapper. */
  public ClassName generatedComponentsWrapper(ClassName root) {
    return Processors.append(
        Processors.getEnclosedClassName(maybeRenameComponent(root)), "_HiltComponents");
  }

  /** Returns the name of the generated component. */
  public ClassName generatedComponent(ClassName root, ClassName component) {
    return generatedComponentsWrapper(root).nestedClass(componentName(component));
  }

  /**
   * Returns the shortened component name by replacing the ending "Component" with "C" if it exists.
   *
   * <p>This is a hack because nested subcomponents in Dagger generate extremely long class names
   * that hit the 256 character limit.
   */
  // TODO(bcorso): See if this issue can be fixed in Dagger, e.g. by using static subcomponents.
  private static String componentName(ClassName component) {
    // TODO(bcorso): How do we want to handle collisions across packages? Currently, we only handle
    // collisions across enclosing elements since namespacing by package would likely lead to too
    // long of class names.
    // Note: This uses regex matching so we only match if the name ends in "Component"
    return Processors.getEnclosedName(component).replaceAll("Component$", "C");
  }

  /**
   * Rewrites the provided HiltAndroidTest-annotated class name using the shared component
   * directory.
   */
  private ClassName maybeRenameComponent(ClassName className) {
    return (renameTestComponents && !className.equals(ClassNames.DEFAULT_ROOT))
        ? ClassName.get(destinationPackage, dedupeSimpleName(className))
        : className;
  }

  /**
   * Derives a new generated component base name, should the simple names of two roots have
   * conflicting simple names.
   *
   * <p>This is lifted nearly verbatim (albeit with new different struct types) from {@link
   * dagger.internal.codegen.writing.SubcomponentNames}.
   */
  private String dedupeSimpleName(ClassName className) {
    Preconditions.checkState(
        simpleNameByClassName.containsKey(className),
        "Class name %s not found in simple name map",
        className.canonicalName());
    return simpleNameByClassName.get(className);
  }

  private static ImmutableMap<ClassName, String> disambiguateConflictingSimpleNames(
      Collection<TypeElement> rootsWithConflictingNames) {
    // If there's only 1 root there's nothing to disambiguate so return the simple name.
    if (rootsWithConflictingNames.size() == 1) {
      TypeElement root = Iterables.getOnlyElement(rootsWithConflictingNames);
      return ImmutableMap.of(ClassName.get(root), root.getSimpleName().toString());
    }

    // There are conflicting simple names, so disambiguate them with a unique prefix.
    // We keep them small to fix https://github.com/google/dagger/issues/421.
    // Sorted in order to guarantee determinism if this is invoked by different processors.
    ImmutableList<TypeElement> sortedRootsWithConflictingNames =
        ImmutableList.sortedCopyOf(
            comparing(typeElement -> typeElement.getQualifiedName().toString()),
            rootsWithConflictingNames);
    Set<String> usedNames = new HashSet<>();
    ImmutableMap.Builder<ClassName, String> uniqueNames = ImmutableMap.builder();
    for (TypeElement root : sortedRootsWithConflictingNames) {
      String basePrefix = uniquingPrefix(root);
      String uniqueName = basePrefix;
      for (int differentiator = 2; !usedNames.add(uniqueName); differentiator++) {
        uniqueName = basePrefix + differentiator;
      }
      uniqueNames.put(ClassName.get(root), format("%s_%s", uniqueName, root.getSimpleName()));
    }
    return uniqueNames.build();
  }

  /** Returns a prefix that could make the component's simple name more unique. */
  private static String uniquingPrefix(TypeElement typeElement) {
    String containerName = typeElement.getEnclosingElement().getSimpleName().toString();

    // If parent element looks like a class, use its initials as a prefix.
    if (!containerName.isEmpty() && isUpperCase(containerName.charAt(0))) {
      return CharMatcher.javaLowerCase().removeFrom(containerName);
    }

    // Not in a normally named class. Prefix with the initials of the elements leading here.
    Name qualifiedName = typeElement.getQualifiedName();
    Iterator<String> pieces = QUALIFIED_NAME_SPLITTER.split(qualifiedName).iterator();
    StringBuilder b = new StringBuilder();

    while (pieces.hasNext()) {
      String next = pieces.next();
      if (pieces.hasNext()) {
        b.append(next.charAt(0));
      }
    }

    // Note that a top level class in the root package will be prefixed "$_".
    return b.length() > 0 ? b.toString() : "$";
  }
}