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