• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.writing;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.collect.Iterables.getLast;
21 import static com.google.common.collect.Iterables.getOnlyElement;
22 import static com.squareup.javapoet.MethodSpec.methodBuilder;
23 import static com.squareup.javapoet.TypeSpec.classBuilder;
24 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
25 import static dagger.internal.codegen.javapoet.AnnotationSpecs.Suppression.UNCHECKED;
26 import static dagger.internal.codegen.javapoet.AnnotationSpecs.suppressWarnings;
27 import static dagger.internal.codegen.javapoet.TypeNames.daggerProviderOf;
28 import static javax.lang.model.element.Modifier.FINAL;
29 import static javax.lang.model.element.Modifier.PRIVATE;
30 import static javax.lang.model.element.Modifier.PUBLIC;
31 import static javax.lang.model.element.Modifier.STATIC;
32 
33 import androidx.room.compiler.processing.XProcessingEnv;
34 import com.google.common.collect.ImmutableList;
35 import com.google.common.collect.Lists;
36 import com.squareup.javapoet.ClassName;
37 import com.squareup.javapoet.CodeBlock;
38 import com.squareup.javapoet.MethodSpec;
39 import com.squareup.javapoet.TypeName;
40 import com.squareup.javapoet.TypeSpec;
41 import com.squareup.javapoet.TypeVariableName;
42 import dagger.internal.codegen.binding.ContributionBinding;
43 import dagger.internal.codegen.javapoet.CodeBlocks;
44 import dagger.internal.codegen.model.BindingKind;
45 import dagger.internal.codegen.model.Key;
46 import dagger.internal.codegen.writing.ComponentImplementation.ShardImplementation;
47 import dagger.internal.codegen.writing.FrameworkFieldInitializer.FrameworkInstanceCreationExpression;
48 import dagger.internal.codegen.xprocessing.XProcessingEnvs;
49 import java.util.HashMap;
50 import java.util.LinkedHashMap;
51 import java.util.Map;
52 import java.util.TreeMap;
53 
54 /**
55  * Keeps track of all provider expression requests for a component.
56  *
57  * <p>The provider expression request will be satisfied by a single generated {@code Provider} class
58  * that can provide instances for all types by switching on an id.
59  */
60 final class SwitchingProviders {
61   /**
62    * Each switch size is fixed at 100 cases each and put in its own method. This is to limit the
63    * size of the methods so that we don't reach the "huge" method size limit for Android that will
64    * prevent it from being AOT compiled in some versions of Android (b/77652521). This generally
65    * starts to happen around 1500 cases, but we are choosing 100 to be safe.
66    */
67   // TODO(bcorso): Include a proguard_spec in the Dagger library to prevent inlining these methods?
68   // TODO(ronshapiro): Consider making this configurable via a flag.
69   private static final int MAX_CASES_PER_SWITCH = 100;
70 
71   private static final long MAX_CASES_PER_CLASS = MAX_CASES_PER_SWITCH * MAX_CASES_PER_SWITCH;
72   private static final TypeVariableName T = TypeVariableName.get("T");
73 
74   /**
75    * Maps a {@link Key} to an instance of a {@link SwitchingProviderBuilder}. Each group of {@code
76    * MAX_CASES_PER_CLASS} keys will share the same instance.
77    */
78   private final Map<Key, SwitchingProviderBuilder> switchingProviderBuilders =
79       new LinkedHashMap<>();
80 
81   private final ShardImplementation shardImplementation;
82   private final XProcessingEnv processingEnv;
83 
SwitchingProviders(ShardImplementation shardImplementation, XProcessingEnv processingEnv)84   SwitchingProviders(ShardImplementation shardImplementation, XProcessingEnv processingEnv) {
85     this.shardImplementation = checkNotNull(shardImplementation);
86     this.processingEnv = checkNotNull(processingEnv);
87   }
88 
89   /** Returns the framework instance creation expression for an inner switching provider class. */
newFrameworkInstanceCreationExpression( ContributionBinding binding, RequestRepresentation unscopedInstanceRequestRepresentation)90   FrameworkInstanceCreationExpression newFrameworkInstanceCreationExpression(
91       ContributionBinding binding, RequestRepresentation unscopedInstanceRequestRepresentation) {
92     return new FrameworkInstanceCreationExpression() {
93       @Override
94       public CodeBlock creationExpression() {
95         return switchingProviderBuilders
96             .computeIfAbsent(binding.key(), key -> getSwitchingProviderBuilder())
97             .getNewInstanceCodeBlock(binding, unscopedInstanceRequestRepresentation);
98       }
99     };
100   }
101 
102   private SwitchingProviderBuilder getSwitchingProviderBuilder() {
103     if (switchingProviderBuilders.size() % MAX_CASES_PER_CLASS == 0) {
104       String name = shardImplementation.getUniqueClassName("SwitchingProvider");
105       SwitchingProviderBuilder switchingProviderBuilder =
106           new SwitchingProviderBuilder(shardImplementation.name().nestedClass(name));
107       shardImplementation.addTypeSupplier(switchingProviderBuilder::build);
108       return switchingProviderBuilder;
109     }
110     return getLast(switchingProviderBuilders.values());
111   }
112 
113   // TODO(bcorso): Consider just merging this class with SwitchingProviders.
114   private final class SwitchingProviderBuilder {
115     // Keep the switch cases ordered by switch id. The switch Ids are assigned in pre-order
116     // traversal, but the switch cases are assigned in post-order traversal of the binding graph.
117     private final Map<Integer, CodeBlock> switchCases = new TreeMap<>();
118     private final Map<Key, Integer> switchIds = new HashMap<>();
119     private final ClassName switchingProviderType;
120 
121     SwitchingProviderBuilder(ClassName switchingProviderType) {
122       this.switchingProviderType = checkNotNull(switchingProviderType);
123     }
124 
125     private CodeBlock getNewInstanceCodeBlock(
126         ContributionBinding binding, RequestRepresentation unscopedInstanceRequestRepresentation) {
127       Key key = binding.key();
128       if (!switchIds.containsKey(key)) {
129         int switchId = switchIds.size();
130         switchIds.put(key, switchId);
131         switchCases.put(
132             switchId, createSwitchCaseCodeBlock(key, unscopedInstanceRequestRepresentation));
133       }
134       return CodeBlock.of(
135           "new $T<$L>($L, $L)",
136           switchingProviderType,
137           // Add the type parameter explicitly when the binding is scoped because Java can't resolve
138           // the type when wrapped. For example, the following will error:
139           //   fooProvider = DoubleCheck.provider(new SwitchingProvider<>(1));
140           (binding.scope().isPresent()
141               || binding.kind().equals(BindingKind.ASSISTED_FACTORY)
142               || XProcessingEnvs.isPreJava8SourceVersion(processingEnv))
143               ? CodeBlock.of(
144                   "$T", shardImplementation.accessibleTypeName(binding.contributedType()))
145               : "",
146           shardImplementation.componentFieldsByImplementation().values().stream()
147               .map(field -> CodeBlock.of("$N", field))
148               .collect(CodeBlocks.toParametersCodeBlock()),
149           switchIds.get(key));
150     }
151 
152     private CodeBlock createSwitchCaseCodeBlock(
153         Key key, RequestRepresentation unscopedInstanceRequestRepresentation) {
154       // TODO(bcorso): Try to delay calling getDependencyExpression() until we are writing out the
155       // SwitchingProvider because calling it here makes FrameworkFieldInitializer think there's a
156       // cycle when initializing SwitchingProviders which adds an uncessary DelegateFactory.
157       CodeBlock instanceCodeBlock =
158           unscopedInstanceRequestRepresentation
159               .getDependencyExpression(switchingProviderType)
160               .box()
161               .codeBlock();
162 
163       return CodeBlock.builder()
164           // TODO(bcorso): Is there something else more useful than the key?
165           .add("case $L: // $L \n", switchIds.get(key), key)
166           .addStatement("return ($T) $L", T, instanceCodeBlock)
167           .build();
168     }
169 
170     private TypeSpec build() {
171       TypeSpec.Builder builder =
172           classBuilder(switchingProviderType)
173               .addModifiers(PRIVATE, FINAL, STATIC)
174               .addTypeVariable(T)
175               .addSuperinterface(daggerProviderOf(T))
176               .addMethods(getMethods());
177 
178       // The SwitchingProvider constructor lists all component parameters first and switch id last.
179       MethodSpec.Builder constructor = MethodSpec.constructorBuilder();
180       shardImplementation
181           .componentFieldsByImplementation()
182           .values()
183           .forEach(
184               field -> {
185                 builder.addField(field);
186                 constructor.addParameter(field.type, field.name);
187                 constructor.addStatement("this.$1N = $1N", field);
188               });
189       builder.addField(TypeName.INT, "id", PRIVATE, FINAL);
190       constructor.addParameter(TypeName.INT, "id").addStatement("this.id = id");
191 
192       return builder.addMethod(constructor.build()).build();
193     }
194 
195     private ImmutableList<MethodSpec> getMethods() {
196       ImmutableList<CodeBlock> switchCodeBlockPartitions = switchCodeBlockPartitions();
197       if (switchCodeBlockPartitions.size() == 1) {
198         // There are less than MAX_CASES_PER_SWITCH cases, so no need for extra get methods.
199         return ImmutableList.of(
200             methodBuilder("get")
201                 .addModifiers(PUBLIC)
202                 .addAnnotation(suppressWarnings(UNCHECKED))
203                 .addAnnotation(Override.class)
204                 .returns(T)
205                 .addCode(getOnlyElement(switchCodeBlockPartitions))
206                 .build());
207       }
208 
209       // This is the main public "get" method that will route to private getter methods.
210       MethodSpec.Builder routerMethod =
211           methodBuilder("get")
212               .addModifiers(PUBLIC)
213               .addAnnotation(Override.class)
214               .returns(T)
215               .beginControlFlow("switch (id / $L)", MAX_CASES_PER_SWITCH);
216 
217       ImmutableList.Builder<MethodSpec> getMethods = ImmutableList.builder();
218       for (int i = 0; i < switchCodeBlockPartitions.size(); i++) {
219         MethodSpec method =
220             methodBuilder("get" + i)
221                 .addModifiers(PRIVATE)
222                 .addAnnotation(suppressWarnings(UNCHECKED))
223                 .returns(T)
224                 .addCode(switchCodeBlockPartitions.get(i))
225                 .build();
226         getMethods.add(method);
227         routerMethod.addStatement("case $L: return $N()", i, method);
228       }
229 
230       routerMethod.addStatement("default: throw new $T(id)", AssertionError.class).endControlFlow();
231 
232       return getMethods.add(routerMethod.build()).build();
233     }
234 
235     private ImmutableList<CodeBlock> switchCodeBlockPartitions() {
236       return Lists.partition(ImmutableList.copyOf(switchCases.values()), MAX_CASES_PER_SWITCH)
237           .stream()
238           .map(
239               partitionCases ->
240                   CodeBlock.builder()
241                       .beginControlFlow("switch (id)")
242                       .add(CodeBlocks.concat(partitionCases))
243                       .addStatement("default: throw new $T(id)", AssertionError.class)
244                       .endControlFlow()
245                       .build())
246           .collect(toImmutableList());
247     }
248   }
249 }
250