blob: 1a63f8b47e0ad543a876090311b8bdd9b4027a22 [file] [log] [blame]
/*
* Copyright (C) 2019 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 com.google.common.base.Preconditions.checkState;
import com.google.auto.common.MoreElements;
import com.google.auto.value.AutoValue;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import com.squareup.javapoet.ClassName;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/**
* Implements default configurations for Processors, and provides structure for exception handling.
*
* <p>By default #process() will do the following:
*
* <ol>
* <li> #preRoundProcess()
* <li> foreach element:
* <ul><li> #processEach()</ul>
* </li>
* <li> #postRoundProcess()
* <li> #claimAnnotation()
* </ol>
*
* <p>#processEach() allows each element to be processed, even if exceptions are thrown. Due to the
* non-deterministic ordering of the processed elements, this is needed to ensure a consistent set
* of exceptions are thrown with each build.
*/
public abstract class BaseProcessor extends AbstractProcessor {
/** Stores the state of processing for a given annotation and element. */
@AutoValue
abstract static class ProcessingState {
private static ProcessingState of(TypeElement annotation, Element element) {
// We currently only support TypeElements directly annotated with the annotation.
// TODO(bcorso): Switch to using BasicAnnotationProcessor if we need more than this.
// Note: Switching to BasicAnnotationProcessor is currently not possible because of cyclic
// references to generated types in our API. For example, an @AndroidEntryPoint annotated
// element will indefinitely defer its own processing because it extends a generated type
// that it's responsible for generating.
checkState(MoreElements.isType(element));
checkState(Processors.hasAnnotation(element, ClassName.get(annotation)));
return new AutoValue_BaseProcessor_ProcessingState(
ClassName.get(annotation),
ClassName.get(MoreElements.asType(element)));
}
/** Returns the class name of the annotation. */
abstract ClassName annotationClassName();
/** Returns the type name of the annotated element. */
abstract ClassName elementClassName();
/** Returns the annotation that triggered the processing. */
TypeElement annotation(Elements elements) {
return elements.getTypeElement(elementClassName().toString());
}
/** Returns the annotated element to process. */
TypeElement element(Elements elements) {
return elements.getTypeElement(annotationClassName().toString());
}
}
private final Set<ProcessingState> stateToReprocess = new LinkedHashSet<>();
private Elements elements;
private Types types;
private Messager messager;
private ProcessorErrorHandler errorHandler;
@Override
public final Set<String> getSupportedOptions() {
// This is declared here rather than in the actual processors because KAPT will issue a
// warning if any used option is not unsupported. This can happen when there is a module
// which uses Hilt but lacks any @AndroidEntryPoint annotations.
// See: https://github.com/google/dagger/issues/2040
return HiltCompilerOptions.getProcessorOptions();
}
/** Used to perform initialization before each round of processing. */
protected void preRoundProcess(RoundEnvironment roundEnv) {};
/**
* Called for each element in a round that uses a supported annotation.
*
* Note that an exception can be thrown for each element in the round. This is usually preferred
* over throwing only the first exception in a round. Only throwing the first exception in the
* round can lead to flaky errors that are dependent on the non-deterministic ordering that the
* elements are processed in.
*/
protected void processEach(TypeElement annotation, Element element) throws Exception {};
/**
* Used to perform post processing at the end of a round. This is especially useful for handling
* additional processing that depends on aggregate data, that cannot be handled in #processEach().
*
* <p>Note: this will not be called if an exception is thrown during #processEach() -- if we have
* already detected errors on an annotated element, performing post processing on an aggregate
* will just produce more (perhaps non-deterministic) errors.
*/
protected void postRoundProcess(RoundEnvironment roundEnv) throws Exception {};
/** @return true if you want to claim annotations after processing each round. Default false. */
protected boolean claimAnnotations() {
return false;
}
/**
* @return true if you want to delay errors to the last round. Useful if the processor
* generates code for symbols used a lot in the user code. Delaying allows as much code to
* compile as possible for correctly configured types and reduces error spam.
*/
protected boolean delayErrors() {
return false;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
this.messager = processingEnv.getMessager();
this.elements = processingEnv.getElementUtils();
this.types = processingEnv.getTypeUtils();
this.errorHandler = new ProcessorErrorHandler(processingEnvironment);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* This should not be overridden, as it defines the order of the processing.
*/
@Override
public final boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
preRoundProcess(roundEnv);
boolean roundError = false;
// Gather the set of new and deferred elements to process, grouped by annotation.
SetMultimap<TypeElement, Element> elementMultiMap = LinkedHashMultimap.create();
for (ProcessingState processingState : stateToReprocess) {
elementMultiMap.put(processingState.annotation(elements), processingState.element(elements));
}
for (TypeElement annotation : annotations) {
elementMultiMap.putAll(annotation, roundEnv.getElementsAnnotatedWith(annotation));
}
// Clear the processing state before reprocessing.
stateToReprocess.clear();
for (Map.Entry<TypeElement, Collection<Element>> entry : elementMultiMap.asMap().entrySet()) {
TypeElement annotation = entry.getKey();
for (Element element : entry.getValue()) {
try {
processEach(annotation, element);
} catch (Exception e) {
if (e instanceof ErrorTypeException && !roundEnv.processingOver()) {
// Allow an extra round to reprocess to try to resolve this type.
stateToReprocess.add(ProcessingState.of(annotation, element));
} else {
errorHandler.recordError(e);
roundError = true;
}
}
}
}
if (!roundError) {
try {
postRoundProcess(roundEnv);
} catch (Exception e) {
errorHandler.recordError(e);
}
}
if (!delayErrors() || roundEnv.processingOver()) {
errorHandler.checkErrors();
}
return claimAnnotations();
}
/** @return the error handle for the processor. */
protected final ProcessorErrorHandler getErrorHandler() {
return errorHandler;
}
public final ProcessingEnvironment getProcessingEnv() {
return processingEnv;
}
public final Elements getElementUtils() {
return elements;
}
public final Types getTypeUtils() {
return types;
}
public final Messager getMessager() {
return messager;
}
}