/* * 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.internal.codegen.compileroption; import static com.google.common.base.CaseFormat.LOWER_CAMEL; import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Sets.immutableEnumSet; import static dagger.internal.codegen.compileroption.FeatureStatus.DISABLED; import static dagger.internal.codegen.compileroption.FeatureStatus.ENABLED; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.EXPERIMENTAL_AHEAD_OF_TIME_SUBCOMPONENTS; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.EXPERIMENTAL_ANDROID_MODE; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.EXPERIMENTAL_DAGGER_ERROR_MESSAGES; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.FAST_INIT; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.FLOATING_BINDS_METHODS; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.FORMAT_GENERATED_SOURCE; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.IGNORE_PRIVATE_AND_STATIC_INJECTION_FOR_COMPONENT; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.PLUGINS_VISIT_FULL_BINDING_GRAPHS; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.STRICT_MULTIBINDING_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.VALIDATE_TRANSITIVE_COMPONENT_DEPENDENCIES; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.WARN_IF_INJECTION_FACTORY_NOT_GENERATED_UPSTREAM; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Feature.WRITE_PRODUCER_NAME_IN_TOKEN; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.KeyOnlyOption.HEADER_COMPILATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.KeyOnlyOption.USE_GRADLE_INCREMENTAL_PROCESSING; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.DISABLE_INTER_COMPONENT_SCOPE_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.EXPLICIT_BINDING_CONFLICTS_WITH_INJECT; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.FULL_BINDING_GRAPH_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.MODULE_HAS_DIFFERENT_SCOPES_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.NULLABLE_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.PRIVATE_MEMBER_VALIDATION; import static dagger.internal.codegen.compileroption.ProcessingEnvironmentCompilerOptions.Validation.STATIC_MEMBER_VALIDATION; import static dagger.internal.codegen.compileroption.ValidationType.ERROR; import static dagger.internal.codegen.compileroption.ValidationType.NONE; import static dagger.internal.codegen.compileroption.ValidationType.WARNING; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static java.util.stream.Collectors.joining; import static java.util.stream.Stream.concat; import com.google.auto.common.MoreElements; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import dagger.internal.codegen.langmodel.DaggerElements; import dagger.producers.Produces; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.annotation.processing.ProcessingEnvironment; import javax.inject.Inject; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; /** {@link CompilerOptions} for the given {@link ProcessingEnvironment}. */ public final class ProcessingEnvironmentCompilerOptions extends CompilerOptions { // EnumOption doesn't support integer inputs so just doing this as a 1-off for now. private static final String KEYS_PER_COMPONENT_SHARD = "dagger.keysPerComponentShard"; private final ProcessingEnvironment processingEnvironment; private final DaggerElements daggerElements; private final Map, Object> enumOptions = new HashMap<>(); private final Map, ImmutableMap>> allCommandLineOptions = new HashMap<>(); @Inject ProcessingEnvironmentCompilerOptions( ProcessingEnvironment processingEnvironment, DaggerElements daggerElements) { this.processingEnvironment = processingEnvironment; this.daggerElements = daggerElements; checkValid(); } @Override public boolean usesProducers() { return processingEnvironment.getElementUtils().getTypeElement(Produces.class.getCanonicalName()) != null; } @Override public boolean headerCompilation() { return isEnabled(HEADER_COMPILATION); } @Override public boolean fastInit(TypeElement component) { return isEnabled(FAST_INIT); } @Override public boolean formatGeneratedSource() { return isEnabled(FORMAT_GENERATED_SOURCE); } @Override public boolean writeProducerNameInToken() { return isEnabled(WRITE_PRODUCER_NAME_IN_TOKEN); } @Override public Diagnostic.Kind nullableValidationKind() { return diagnosticKind(NULLABLE_VALIDATION); } @Override public Diagnostic.Kind privateMemberValidationKind() { return diagnosticKind(PRIVATE_MEMBER_VALIDATION); } @Override public Diagnostic.Kind staticMemberValidationKind() { return diagnosticKind(STATIC_MEMBER_VALIDATION); } @Override public boolean ignorePrivateAndStaticInjectionForComponent() { return isEnabled(IGNORE_PRIVATE_AND_STATIC_INJECTION_FOR_COMPONENT); } @Override public ValidationType scopeCycleValidationType() { return parseOption(DISABLE_INTER_COMPONENT_SCOPE_VALIDATION); } @Override public boolean validateTransitiveComponentDependencies() { return isEnabled(VALIDATE_TRANSITIVE_COMPONENT_DEPENDENCIES); } @Override public boolean warnIfInjectionFactoryNotGeneratedUpstream() { return isEnabled(WARN_IF_INJECTION_FACTORY_NOT_GENERATED_UPSTREAM); } @Override public ValidationType fullBindingGraphValidationType() { return parseOption(FULL_BINDING_GRAPH_VALIDATION); } @Override public boolean pluginsVisitFullBindingGraphs(TypeElement component) { return isEnabled(PLUGINS_VISIT_FULL_BINDING_GRAPHS); } @Override public Diagnostic.Kind moduleHasDifferentScopesDiagnosticKind() { return diagnosticKind(MODULE_HAS_DIFFERENT_SCOPES_VALIDATION); } @Override public ValidationType explicitBindingConflictsWithInjectValidationType() { return parseOption(EXPLICIT_BINDING_CONFLICTS_WITH_INJECT); } @Override public boolean experimentalDaggerErrorMessages() { return isEnabled(EXPERIMENTAL_DAGGER_ERROR_MESSAGES); } @Override public boolean strictMultibindingValidation() { return isEnabled(STRICT_MULTIBINDING_VALIDATION); } @Override public int keysPerComponentShard(TypeElement component) { if (processingEnvironment.getOptions().containsKey(KEYS_PER_COMPONENT_SHARD)) { checkArgument( "dagger.internal.codegen".contentEquals( MoreElements.getPackage(component).getQualifiedName()), "Cannot set %s. It is only meant for internal testing.", KEYS_PER_COMPONENT_SHARD); return Integer.parseInt( processingEnvironment.getOptions().get(KEYS_PER_COMPONENT_SHARD)); } return super.keysPerComponentShard(component); } private boolean isEnabled(KeyOnlyOption keyOnlyOption) { return processingEnvironment.getOptions().containsKey(keyOnlyOption.toString()); } private boolean isEnabled(Feature feature) { return parseOption(feature).equals(ENABLED); } private Diagnostic.Kind diagnosticKind(Validation validation) { return parseOption(validation).diagnosticKind().get(); } @SuppressWarnings("CheckReturnValue") private ProcessingEnvironmentCompilerOptions checkValid() { for (KeyOnlyOption keyOnlyOption : KeyOnlyOption.values()) { isEnabled(keyOnlyOption); } for (Feature feature : Feature.values()) { parseOption(feature); } for (Validation validation : Validation.values()) { parseOption(validation); } noLongerRecognized(EXPERIMENTAL_ANDROID_MODE); noLongerRecognized(FLOATING_BINDS_METHODS); noLongerRecognized(EXPERIMENTAL_AHEAD_OF_TIME_SUBCOMPONENTS); noLongerRecognized(USE_GRADLE_INCREMENTAL_PROCESSING); return this; } private void noLongerRecognized(CommandLineOption commandLineOption) { if (processingEnvironment.getOptions().containsKey(commandLineOption.toString())) { processingEnvironment .getMessager() .printMessage( Diagnostic.Kind.WARNING, commandLineOption + " is no longer recognized by Dagger"); } } private interface CommandLineOption { /** The key of the option (appears after "-A"). */ @Override String toString(); /** * Returns all aliases besides {@link #toString()}, such as old names for an option, in order of * precedence. */ default ImmutableList aliases() { return ImmutableList.of(); } /** All the command-line names for this option, in order of precedence. */ default Stream allNames() { return concat(Stream.of(toString()), aliases().stream()); } } /** An option that can be set on the command line. */ private interface EnumOption> extends CommandLineOption { /** The default value for this option. */ E defaultValue(); /** The valid values for this option. */ Set validValues(); } enum KeyOnlyOption implements CommandLineOption { HEADER_COMPILATION { @Override public String toString() { return "experimental_turbine_hjar"; } }, USE_GRADLE_INCREMENTAL_PROCESSING { @Override public String toString() { return "dagger.gradle.incremental"; } }, } /** * A feature that can be enabled or disabled on the command line by setting {@code -Akey=ENABLED} * or {@code -Akey=DISABLED}. */ enum Feature implements EnumOption { FAST_INIT, EXPERIMENTAL_ANDROID_MODE, FORMAT_GENERATED_SOURCE, WRITE_PRODUCER_NAME_IN_TOKEN, WARN_IF_INJECTION_FACTORY_NOT_GENERATED_UPSTREAM, IGNORE_PRIVATE_AND_STATIC_INJECTION_FOR_COMPONENT, EXPERIMENTAL_AHEAD_OF_TIME_SUBCOMPONENTS, FORCE_USE_SERIALIZED_COMPONENT_IMPLEMENTATIONS, EMIT_MODIFIABLE_METADATA_ANNOTATIONS(ENABLED), PLUGINS_VISIT_FULL_BINDING_GRAPHS, FLOATING_BINDS_METHODS, EXPERIMENTAL_DAGGER_ERROR_MESSAGES, STRICT_MULTIBINDING_VALIDATION, VALIDATE_TRANSITIVE_COMPONENT_DEPENDENCIES(ENABLED) ; final FeatureStatus defaultValue; Feature() { this(DISABLED); } Feature(FeatureStatus defaultValue) { this.defaultValue = defaultValue; } @Override public FeatureStatus defaultValue() { return defaultValue; } @Override public Set validValues() { return EnumSet.allOf(FeatureStatus.class); } @Override public String toString() { return optionName(this); } } /** The diagnostic kind or validation type for a kind of validation. */ enum Validation implements EnumOption { DISABLE_INTER_COMPONENT_SCOPE_VALIDATION(), NULLABLE_VALIDATION(ERROR, WARNING), PRIVATE_MEMBER_VALIDATION(ERROR, WARNING), STATIC_MEMBER_VALIDATION(ERROR, WARNING), /** Whether to validate full binding graphs for components, subcomponents, and modules. */ FULL_BINDING_GRAPH_VALIDATION(NONE, ERROR, WARNING) { @Override public ImmutableList aliases() { return ImmutableList.of("dagger.moduleBindingValidation"); } }, /** * How to report conflicting scoped bindings when validating partial binding graphs associated * with modules. */ MODULE_HAS_DIFFERENT_SCOPES_VALIDATION(ERROR, WARNING), /** * How to report that an explicit binding in a subcomponent conflicts with an {@code @Inject} * constructor used in an ancestor component. */ EXPLICIT_BINDING_CONFLICTS_WITH_INJECT(WARNING, ERROR, NONE), ; final ValidationType defaultType; final ImmutableSet validTypes; Validation() { this(ERROR, WARNING, NONE); } Validation(ValidationType defaultType, ValidationType... moreValidTypes) { this.defaultType = defaultType; this.validTypes = immutableEnumSet(defaultType, moreValidTypes); } @Override public ValidationType defaultValue() { return defaultType; } @Override public Set validValues() { return validTypes; } @Override public String toString() { return optionName(this); } } private static String optionName(Enum> option) { return "dagger." + UPPER_UNDERSCORE.to(LOWER_CAMEL, option.name()); } /** The supported command-line options. */ public static ImmutableSet supportedOptions() { // need explicit type parameter to avoid a runtime stream error return ImmutableSet.builder() .addAll( Stream.of( KeyOnlyOption.values(), Feature.values(), Validation.values()) .flatMap(Arrays::stream) .flatMap(CommandLineOption::allNames) .collect(toImmutableSet())) .add(KEYS_PER_COMPONENT_SHARD) .build(); } /** * Returns the value for the option as set on the command line by any name, or the default value * if not set. * *

If more than one name is used to set the value, but all names specify the same value, * reports a warning and returns that value. * *

If more than one name is used to set the value, and not all names specify the same value, * reports an error and returns the default value. */ private > T parseOption(EnumOption option) { @SuppressWarnings("unchecked") // we only put covariant values into the map T value = (T) enumOptions.computeIfAbsent(option, this::parseOptionUncached); return value; } private boolean isSetOnCommandLine(Feature feature) { return getUsedNames(feature).count() > 0; } private > T parseOptionUncached(EnumOption option) { ImmutableMap values = parseOptionWithAllNames(option); // If no value is specified, return the default value. if (values.isEmpty()) { return option.defaultValue(); } // If all names have the same value, return that. if (values.asMultimap().inverse().keySet().size() == 1) { // Warn if an option was set with more than one name. That would be an error if the values // differed. if (values.size() > 1) { reportUseOfDifferentNamesForOption(Diagnostic.Kind.WARNING, option, values.keySet()); } return values.values().asList().get(0); } // If different names have different values, report an error and return the default // value. reportUseOfDifferentNamesForOption(Diagnostic.Kind.ERROR, option, values.keySet()); return option.defaultValue(); } private void reportUseOfDifferentNamesForOption( Diagnostic.Kind diagnosticKind, EnumOption option, ImmutableSet usedNames) { processingEnvironment .getMessager() .printMessage( diagnosticKind, String.format( "Only one of the equivalent options (%s) should be used; prefer -A%s", usedNames.stream().map(name -> "-A" + name).collect(joining(", ")), option)); } private > ImmutableMap parseOptionWithAllNames( EnumOption option) { @SuppressWarnings("unchecked") // map is covariant ImmutableMap aliasValues = (ImmutableMap) allCommandLineOptions.computeIfAbsent(option, this::parseOptionWithAllNamesUncached); return aliasValues; } private > ImmutableMap parseOptionWithAllNamesUncached( EnumOption option) { ImmutableMap.Builder values = ImmutableMap.builder(); getUsedNames(option) .forEach( name -> parseOptionWithName(option, name).ifPresent(value -> values.put(name, value))); return values.build(); } private > Optional parseOptionWithName(EnumOption option, String key) { checkArgument(processingEnvironment.getOptions().containsKey(key), "key %s not found", key); String stringValue = processingEnvironment.getOptions().get(key); if (stringValue == null) { processingEnvironment .getMessager() .printMessage(Diagnostic.Kind.ERROR, "Processor option -A" + key + " needs a value"); } else { try { T value = Enum.valueOf(option.defaultValue().getDeclaringClass(), Ascii.toUpperCase(stringValue)); if (option.validValues().contains(value)) { return Optional.of(value); } } catch (IllegalArgumentException e) { // handled below } processingEnvironment .getMessager() .printMessage( Diagnostic.Kind.ERROR, String.format( "Processor option -A%s may only have the values %s " + "(case insensitive), found: %s", key, option.validValues(), stringValue)); } return Optional.empty(); } private Stream getUsedNames(CommandLineOption option) { return option.allNames().filter(name -> processingEnvironment.getOptions().containsKey(name)); } }