Replace TransitionApplier interface with a dedicated class.

This accomplishes a few goals:

 1. Removes the outdated BuildConfiguration.StaticConfigurationApplier code.
 2. Removes the TransitionApplier abstraction completely. This was an awkward
    bridge meant to support both static and dynamic implementations.
 3. Moves transition logic to its own dedicated class: ConfigurationResolver.
    This no longer belongs in BuildConfiguration, which we ultimately want to
    become a simple <key, value> map.

Part of the static config cleanup effort.

PiperOrigin-RevId: 165736955
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
index 2644780..cb4f086 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
@@ -1061,6 +1061,10 @@
     }
 
     class SilentDependencyResolver extends DependencyResolver {
+      private SilentDependencyResolver() {
+        super(ruleClassProvider.getDynamicTransitionMapper());
+      }
+
       @Override
       protected void invalidVisibilityReferenceHook(TargetAndConfiguration node, Label label) {
         throw new RuntimeException("bad visibility on " + label + " during testing unexpected");
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
index ee6e256..e238026 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java
@@ -22,6 +22,8 @@
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.analysis.config.ConfigurationResolver;
+import com.google.devtools.build.lib.analysis.config.DynamicTransitionMapper;
 import com.google.devtools.build.lib.analysis.config.HostTransition;
 import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.analysis.config.PatchTransition;
@@ -60,7 +62,10 @@
  * <p>Includes logic to derive the right configurations depending on transition type.
  */
 public abstract class DependencyResolver {
-  protected DependencyResolver() {
+  private final ConfigurationResolver configResolver;
+
+  protected DependencyResolver(DynamicTransitionMapper transitionMapper) {
+    this.configResolver = new ConfigurationResolver(transitionMapper);
   }
 
   /**
@@ -562,38 +567,6 @@
     }
   }
 
-
-  private AspectCollection requiredAspects(
-      Iterable<Aspect> aspectPath,
-      AttributeAndOwner attributeAndOwner,
-      final Target target,
-      Rule originalRule) throws InconsistentAspectOrderException {
-    if (!(target instanceof Rule)) {
-      return AspectCollection.EMPTY;
-    }
-
-    if (attributeAndOwner.ownerAspect != null) {
-      // Do not propagate aspects along aspect attributes.
-      return AspectCollection.EMPTY;
-    }
-
-    ImmutableList.Builder<Aspect> filteredAspectPath = ImmutableList.builder();
-    ImmutableSet.Builder<AspectDescriptor> visibleAspects = ImmutableSet.builder();
-
-    Attribute attribute = attributeAndOwner.attribute;
-    collectOriginatingAspects(originalRule, attribute, (Rule) target,
-        filteredAspectPath, visibleAspects);
-
-    collectPropagatingAspects(aspectPath,
-        attribute,
-        (Rule) target, filteredAspectPath, visibleAspects);
-    try {
-      return AspectCollection.create(filteredAspectPath.build(), visibleAspects.build());
-    } catch (AspectCycleOnPathException e) {
-      throw  new InconsistentAspectOrderException(originalRule, attribute, target, e);
-    }
-  }
-
   /**
    * Collects into {@code filteredAspectPath}
    * aspects from {@code aspectPath} that propagate along {@code attribute}
@@ -734,22 +707,22 @@
       if (toTarget == null) {
         return; // Skip this round: we still need to Skyframe-evaluate the dep's target.
       }
-      BuildConfiguration.TransitionApplier resolver = ruleConfig.getTransitionApplier();
-      ruleConfig.evaluateTransition(rule, attributeAndOwner.attribute, toTarget, resolver);
-      // An <Attribute, Label> pair can resolve to multiple deps because of split transitions.
-      for (Dependency dependency :
-          resolver.getDependencies(depLabel,
-              requiredAspects(aspects, attributeAndOwner, toTarget, rule))) {
-        outgoingEdges.put(attributeAndOwner.attribute, dependency);
-      }
+      Attribute.Transition transition = configResolver.evaluateTransition(
+          ruleConfig, rule, attributeAndOwner.attribute, toTarget);
+      outgoingEdges.put(
+          attributeAndOwner.attribute,
+          transition == Attribute.ConfigurationTransition.NULL
+              ? Dependency.withNullConfiguration(depLabel)
+              : Dependency.withTransitionAndAspects(depLabel, transition,
+                    requiredAspects(attributeAndOwner, toTarget)));
     }
 
     /**
      * Resolves the given dep for the given attribute using a pre-prepared configuration.
      *
      * <p>Use this method with care: it skips Bazel's standard config transition semantics ({@link
-     * BuildConfiguration#evaluateTransition}). That means attributes passed through here won't obey
-     * standard rules on which configurations apply to their deps. This should only be done for
+     * ConfigurationResolver#evaluateTransition}). That means attributes passed through here won't
+     * obey standard rules on which configurations apply to their deps. This should only be done for
      * special circumstances that really justify the difference. When in doubt, use {@link
      * #resolveDep(AttributeAndOwner, Label)}.
      */
@@ -759,25 +732,40 @@
       if (toTarget == null) {
         return; // Skip this round: this is either a loading error or unevaluated Skyframe dep.
       }
-      BuildConfiguration.TransitionApplier transitionApplier = config.getTransitionApplier();
-      boolean applyNullTransition = false;
-      if (BuildConfiguration.usesNullConfiguration(toTarget)) {
-        transitionApplier.applyTransition(Attribute.ConfigurationTransition.NULL);
-        applyNullTransition = true;
+      outgoingEdges.put(
+          attributeAndOwner.attribute,
+          configResolver.usesNullConfiguration(toTarget)
+              ? Dependency.withNullConfiguration(depLabel)
+              : Dependency.withTransitionAndAspects(depLabel, new FixedTransition(
+                    config.getOptions()), requiredAspects(attributeAndOwner, toTarget)));
+    }
+
+    private AspectCollection requiredAspects(AttributeAndOwner attributeAndOwner,
+        final Target target) throws InconsistentAspectOrderException {
+      if (!(target instanceof Rule)) {
+        return AspectCollection.EMPTY;
       }
 
-      AspectCollection aspects =
-          requiredAspects(this.aspects, attributeAndOwner, toTarget, rule);
-      Dependency dep;
-      if (!applyNullTransition) {
-        // Pass a transition rather than directly feeding the configuration so deps get trimmed.
-        dep = Dependency.withTransitionAndAspects(
-            depLabel, new FixedTransition(config.getOptions()), aspects);
-      } else {
-        dep = Iterables.getOnlyElement(transitionApplier.getDependencies(depLabel, aspects));
+      if (attributeAndOwner.ownerAspect != null) {
+        // Do not propagate aspects along aspect attributes.
+        return AspectCollection.EMPTY;
       }
 
-      outgoingEdges.put(attributeAndOwner.attribute, dep);
+      ImmutableList.Builder<Aspect> filteredAspectPath = ImmutableList.builder();
+      ImmutableSet.Builder<AspectDescriptor> visibleAspects = ImmutableSet.builder();
+
+      Attribute attribute = attributeAndOwner.attribute;
+      collectOriginatingAspects(rule, attribute, (Rule) target,
+          filteredAspectPath, visibleAspects);
+
+      collectPropagatingAspects(aspects,
+          attribute,
+          (Rule) target, filteredAspectPath, visibleAspects);
+      try {
+        return AspectCollection.create(filteredAspectPath.build(), visibleAspects.build());
+      } catch (AspectCycleOnPathException e) {
+        throw new InconsistentAspectOrderException(rule, attribute, target, e);
+      }
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index 5165403..ccb3ea6 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -34,10 +34,8 @@
 import com.google.common.collect.MutableClassToInstanceMap;
 import com.google.devtools.build.lib.actions.ActionEnvironment;
 import com.google.devtools.build.lib.actions.Root;
-import com.google.devtools.build.lib.analysis.AspectCollection;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
-import com.google.devtools.build.lib.analysis.Dependency;
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
 import com.google.devtools.build.lib.buildeventstream.BuildEvent;
@@ -50,23 +48,13 @@
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
-import com.google.devtools.build.lib.packages.Attribute;
-import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
-import com.google.devtools.build.lib.packages.Attribute.Configurator;
-import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
-import com.google.devtools.build.lib.packages.Attribute.Transition;
-import com.google.devtools.build.lib.packages.InputFile;
-import com.google.devtools.build.lib.packages.PackageGroup;
-import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.RuleClassProvider;
-import com.google.devtools.build.lib.packages.RuleTransitionFactory;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.util.OS;
-import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.util.RegexFilter;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -1094,7 +1082,6 @@
   private final ImmutableMap<Class<? extends Fragment>, Fragment> fragments;
   private final ImmutableMap<String, Class<? extends Fragment>> skylarkVisibleFragments;
   private final RepositoryName mainRepositoryName;
-  private final DynamicTransitionMapper dynamicTransitionMapper;
   private final ImmutableSet<String> reservedActionMnemonics;
 
   /**
@@ -1386,8 +1373,7 @@
   public BuildConfiguration(BlazeDirectories directories,
       Map<Class<? extends Fragment>, Fragment> fragmentsMap,
       BuildOptions buildOptions,
-      String repositoryName,
-      @Nullable DynamicTransitionMapper dynamicTransitionMapper) {
+      String repositoryName) {
     this.directories = directories;
     this.fragments = ImmutableSortedMap.copyOf(fragmentsMap, lexicalFragmentSorter);
 
@@ -1398,7 +1384,6 @@
     this.options = buildOptions.get(Options.class);
     this.separateGenfilesDirectory = options.separateGenfilesDirectory;
     this.mainRepositoryName = RepositoryName.createFromValidStrippedName(repositoryName);
-    this.dynamicTransitionMapper = dynamicTransitionMapper;
 
     // We can't use an ImmutableMap.Builder here; we need the ability to add entries with keys that
     // are already in the map so that the same define can be specified on the command line twice,
@@ -1492,8 +1477,7 @@
             directories,
             fragmentsMap,
             options,
-            mainRepositoryName.strippedName(),
-            dynamicTransitionMapper);
+            mainRepositoryName.strippedName());
     return newConfig;
   }
 
@@ -1574,363 +1558,6 @@
   }
 
   /**
-   * A common interface for static vs. dynamic configuration implementations that allows
-   * common configuration and transition-selection logic to seamlessly work with either.
-   *
-   * <p>The basic role of this interface is to "accept" a desired transition and produce
-   * an actual configuration change from it in an implementation-appropriate way.
-   */
-  public interface TransitionApplier {
-    /**
-     * Accepts the given configuration transition. The implementation decides how to turn
-     * this into an actual configuration. This may be called multiple times (representing a
-     * request for a sequence of transitions).
-     */
-    void applyTransition(Transition transition);
-
-    /**
-     * Accepts the given split transition. The implementation decides how to turn this into
-     * actual configurations.
-     */
-    void split(SplitTransition<BuildOptions> splitTransition);
-
-    /**
-     * Returns whether or not all configuration(s) represented by the current state of this
-     * instance are null.
-     */
-    boolean isNull();
-
-    /**
-     * Applies the given attribute configurator to the current configuration(s).
-     */
-    void applyAttributeConfigurator(Configurator<BuildOptions> configurator);
-
-    /**
-     * Applies a custom configuration hook for the given rule.
-     */
-    void applyConfigurationHook(Rule fromRule, Attribute attribute, Target toTarget);
-
-    /**
-     * Populates a {@link com.google.devtools.build.lib.analysis.Dependency}
-     * for each configuration represented by this instance.
-     * TODO(bazel-team): this is a really ugly reverse dependency: factor this away.
-     */
-    Iterable<Dependency> getDependencies(Label label, AspectCollection aspects);
-  }
-
-  /**
-   * Transition applier for static configurations. This implementation populates
-   * {@link com.google.devtools.build.lib.analysis.Dependency} objects with
-   * actual configurations.
-   *
-   * TODO(bazel-team): remove this when dynamic configurations are fully production-ready.
-   */
-  private static class StaticTransitionApplier implements TransitionApplier {
-    // The configuration(s) this applier applies to dep rules. Plural because of split transitions.
-    // May change multiple times: the ultimate transition might be a sequence of intermediate
-    // transitions.
-    List<BuildConfiguration> toConfigurations;
-
-    private StaticTransitionApplier(BuildConfiguration originalConfiguration) {
-      this.toConfigurations = ImmutableList.<BuildConfiguration>of(originalConfiguration);
-    }
-
-    @Override
-    public void applyTransition(Transition transition) {
-      throw new UnsupportedOperationException("dead static config code being removed");
-    }
-
-    @Override
-    public void split(SplitTransition<BuildOptions> splitTransition) {
-      throw new UnsupportedOperationException("dead static config code being removed");
-    }
-
-    @Override
-    public boolean isNull() {
-      return toConfigurations.size() == 1
-          ? Iterables.getOnlyElement(toConfigurations) == null
-          : false;
-    }
-
-    @Override
-    public void applyAttributeConfigurator(Configurator<BuildOptions> configurator) {
-      // There should only be one output configuration at this point: splits don't occur down
-      // attributes with attribute configurators. We can lift this restriction later if desired.
-      BuildOptions toOptions = Iterables.getOnlyElement(toConfigurations).getOptions();
-      applyTransition(configurator.apply(toOptions));
-    }
-
-    @Override
-    public void applyConfigurationHook(Rule fromRule, Attribute attribute, Target toTarget) {
-      throw new UnsupportedOperationException("dead static config code being removed");
-    }
-
-    @Override
-    public Iterable<Dependency> getDependencies(
-        Label label, AspectCollection aspects) {
-      ImmutableList.Builder<Dependency> deps = ImmutableList.builder();
-      for (BuildConfiguration config : toConfigurations) {
-        deps.add(config != null
-            ? Dependency.withConfigurationAndAspects(label, config, aspects)
-            : Dependency.withNullConfiguration(label));
-      }
-      return deps.build();
-    }
-  }
-
-  /**
-   * Transition applier for dynamic configurations. This implementation populates
-   * {@link com.google.devtools.build.lib.analysis.Dependency} objects with
-   * transitions that the caller subsequently creates configurations from.
-   */
-  private static class DynamicTransitionApplier implements TransitionApplier {
-    private final DynamicTransitionMapper dynamicTransitionMapper;
-    private boolean splitApplied = false;
-
-    // The transition this applier applies to dep rules. When multiple transitions are requested,
-    // this is a ComposingSplitTransition, which encapsulates the sequence into a single instance
-    // so calling code doesn't need special logic to support combinations.
-    private Transition currentTransition = Attribute.ConfigurationTransition.NONE;
-
-    private DynamicTransitionApplier(DynamicTransitionMapper dynamicTransitionMapper) {
-      this.dynamicTransitionMapper = dynamicTransitionMapper;
-    }
-
-    /**
-     * Returns true if the given transition should not be modifiable by subsequent ones, i.e.
-     * once this transition is applied it's the final word on the output configuration.
-     */
-    private static boolean isFinal(Transition transition) {
-      return (transition == Attribute.ConfigurationTransition.NULL
-          || transition == HostTransition.INSTANCE);
-    }
-
-    @Override
-    public void applyTransition(Transition transitionToApply) {
-      currentTransition = composeTransitions(currentTransition, transitionToApply);
-    }
-
-    /**
-     * Composes two transitions together efficiently.
-     */
-    private Transition composeTransitions(Transition transition1, Transition transition2) {
-      if (isFinal(transition1)) {
-        return transition1;
-      } else if (transition2 == Attribute.ConfigurationTransition.NONE) {
-        return transition1;
-      } else if (transition2 == Attribute.ConfigurationTransition.NULL) {
-        // A NULL transition can just replace earlier transitions: no need to cfpose them.
-        return Attribute.ConfigurationTransition.NULL;
-      } else if (transition2 == Attribute.ConfigurationTransition.HOST) {
-        // A HOST transition can just replace earlier transitions: no need to compose them.
-        // But it also improves performance: host transitions are common, and
-        // ConfiguredTargetFunction has special optimized logic to handle them. If they were buried
-        // in the last segment of a ComposingSplitTransition, those optimizations wouldn't trigger.
-        return HostTransition.INSTANCE;
-      }
-
-      // TODO(gregce): remove this dynamic transition mapping when static configs are removed.
-      Transition dynamicTransition = dynamicTransitionMapper.map(transition2);
-      return transition1 == Attribute.ConfigurationTransition.NONE
-          ? dynamicTransition
-          : new ComposingSplitTransition(transition1, dynamicTransition);
-    }
-
-    @Override
-    // TODO(gregce): fold this into applyTransition during the static config code removal cleanup
-    public void split(SplitTransition<BuildOptions> splitTransition) {
-      // This "single split" check doesn't come from any design restriction. Its purpose is to
-      // protect against runaway graph explosion, e.g. applying split[1,2,3] -> split[4,5,6] -> ...
-      // and getting 3^n versions of a dep. So it's fine to loosen or lift this restriction
-      // for a principled use case.
-      Preconditions.checkState(!splitApplied,
-          "dependency edges may apply at most one split transition");
-      Preconditions.checkState(currentTransition != Attribute.ConfigurationTransition.NULL,
-          "cannot apply splits after null transitions (null transitions are expected to be final)");
-      Preconditions.checkState(currentTransition != HostTransition.INSTANCE,
-          "cannot apply splits after host transitions (host transitions are expected to be final)");
-      currentTransition = currentTransition == Attribute.ConfigurationTransition.NONE
-          ? splitTransition
-          : new ComposingSplitTransition(currentTransition, splitTransition);
-      splitApplied = true;
-    }
-
-    @Override
-    public boolean isNull() {
-      return currentTransition == Attribute.ConfigurationTransition.NULL;
-    }
-
-    /**
-     * A {@link PatchTransition} that applies an attribute configurator over some input options
-     * to determine which transition to use, then applies that transition over those options
-     * for the final output.
-     */
-    private static final class AttributeConfiguratorTransition implements PatchTransition {
-      private final Configurator<BuildOptions> configurator;
-
-      AttributeConfiguratorTransition(Configurator<BuildOptions> configurator) {
-        this.configurator = configurator;
-      }
-
-      @Override
-      public BuildOptions apply(BuildOptions options) {
-        return Iterables.getOnlyElement(
-            ComposingSplitTransition.apply(options, configurator.apply(options)));
-      }
-
-      @Override
-      public boolean defaultsToSelf() {
-        return false;
-      }
-    }
-    /**
-     * Unlike the static config version, this one can be composed with arbitrary transitions
-     * (including splits).
-     */
-    @Override
-    public void applyAttributeConfigurator(Configurator<BuildOptions> configurator) {
-      if (isFinal(currentTransition)) {
-        return;
-      }
-      currentTransition =
-          composeTransitions(currentTransition, new AttributeConfiguratorTransition(configurator));
-    }
-
-    @Override
-    public void applyConfigurationHook(Rule fromRule, Attribute attribute, Target toTarget) {
-      if (isFinal(currentTransition)) {
-        return;
-      }
-      Rule associatedRule = toTarget.getAssociatedRule();
-      RuleTransitionFactory transitionFactory =
-          associatedRule.getRuleClassObject().getTransitionFactory();
-      if (transitionFactory != null) {
-        // dynamicTransitionMapper is only needed because of Attribute.ConfigurationTransition.DATA:
-        // this is C++-specific but non-C++ rules declare it. So they can't directly provide the
-        // C++-specific patch transition that implements it.
-        PatchTransition ruleClassTransition = (PatchTransition)
-            dynamicTransitionMapper.map(transitionFactory.buildTransitionFor(associatedRule));
-        if (ruleClassTransition != null) {
-          if (currentTransition == ConfigurationTransition.NONE) {
-            currentTransition = ruleClassTransition;
-          } else {
-            currentTransition = new ComposingSplitTransition(currentTransition,
-                ruleClassTransition);
-          }
-        }
-      }
-    }
-
-    @Override
-    public Iterable<Dependency> getDependencies(
-        Label label, AspectCollection aspects) {
-      return ImmutableList.of(
-          isNull()
-              // We can trivially set the final value for null-configured targets now. This saves
-              // us from having to recreate a new Dependency object for the final value later. Since
-              // there are lots of null-configured targets (e.g. all source files), this can add up
-              // over the course of a build.
-              ? Dependency.withNullConfiguration(label)
-              : Dependency.withTransitionAndAspects(label, currentTransition, aspects));
-    }
-  }
-
-  /**
-   * Returns the {@link TransitionApplier} that should be passed to {#evaluateTransition} calls.
-   */
-  public TransitionApplier getTransitionApplier() {
-    return new DynamicTransitionApplier(dynamicTransitionMapper);
-  }
-
-  /**
-   * Returns true if the given target uses a null configuration, false otherwise. Consider
-   * this method the "source of truth" for determining this.
-   */
-  public static boolean usesNullConfiguration(Target target) {
-    return target instanceof InputFile || target instanceof PackageGroup;
-  }
-
-  /**
-   * Calculates the configurations of a direct dependency. If a rule in some BUILD file refers
-   * to a target (like another rule or a source file) using a label attribute, that target needs
-   * to have a configuration, too. This method figures out the proper configuration for the
-   * dependency.
-   *
-   * @param fromRule the rule that's depending on some target
-   * @param attribute the attribute using which the rule depends on that target (eg. "srcs")
-   * @param toTarget the target that's dependeded on
-   * @param transitionApplier the transition applier to accept transitions requests
-   */
-  public void evaluateTransition(final Rule fromRule, final Attribute attribute,
-      final Target toTarget, TransitionApplier transitionApplier) {
-    // Fantastic configurations and where to find them:
-
-    // I. Input files and package groups have no configurations. We don't want to duplicate them.
-    if (usesNullConfiguration(toTarget)) {
-      transitionApplier.applyTransition(Attribute.ConfigurationTransition.NULL);
-      return;
-    }
-
-    // II. Host configurations never switch to another. All prerequisites of host targets have the
-    // same host configuration.
-    if (isHostConfiguration()) {
-      transitionApplier.applyTransition(Attribute.ConfigurationTransition.NONE);
-      return;
-    }
-
-    // Make sure config_setting dependencies are resolved in the referencing rule's configuration,
-    // unconditionally. For example, given:
-    //
-    // genrule(
-    //     name = 'myrule',
-    //     tools = select({ '//a:condition': [':sometool'] })
-    //
-    // all labels in "tools" get resolved in the host configuration (since the "tools" attribute
-    // declares a host configuration transition). We want to explicitly exclude configuration labels
-    // from these transitions, since their *purpose* is to do computation on the owning
-    // rule's configuration.
-    // TODO(bazel-team): don't require special casing here. This is far too hackish.
-    if (toTarget instanceof Rule && ((Rule) toTarget).getRuleClassObject().isConfigMatcher()) {
-      transitionApplier.applyTransition(Attribute.ConfigurationTransition.NONE); // Unnecessary.
-      return;
-    }
-
-    // Apply the parent rule's outgoing transition if it has one.
-    RuleTransitionFactory transitionFactory =
-        fromRule.getRuleClassObject().getOutgoingTransitionFactory();
-    if (transitionFactory != null) {
-      Transition transition = transitionFactory.buildTransitionFor(toTarget.getAssociatedRule());
-      if (transition != null) {
-        transitionApplier.applyTransition(transition);
-      }
-    }
-
-    // TODO(gregce): make the below transitions composable (i.e. take away the "else" clauses) once
-    // the static config code path is removed. They can be mixed freely with dynamic configurations.
-    if (attribute.hasSplitConfigurationTransition()) {
-      Preconditions.checkState(attribute.getConfigurator() == null);
-      transitionApplier.split(
-          (SplitTransition<BuildOptions>) attribute.getSplitTransition(fromRule));
-    } else {
-      // III. Attributes determine configurations. The configuration of a prerequisite is determined
-      // by the attribute.
-      @SuppressWarnings("unchecked")
-      Configurator<BuildOptions> configurator =
-          (Configurator<BuildOptions>) attribute.getConfigurator();
-      if (configurator != null) {
-        // TODO(gregce): remove this branch when static config logic is removed. Attribute
-        // configurators can just be implemented as standard attribute transitions, via
-        // applyTransition.
-        transitionApplier.applyAttributeConfigurator(configurator);
-      } else {
-        transitionApplier.applyTransition(attribute.getConfigurationTransition());
-      }
-    }
-
-    transitionApplier.applyConfigurationHook(fromRule, attribute, toTarget);
-  }
-
-  /**
    * The platform string, suitable for use as a key into a MakeEnvironment.
    */
   public String getPlatformName() {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java
new file mode 100644
index 0000000..5432b7c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/ConfigurationResolver.java
@@ -0,0 +1,255 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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 com.google.devtools.build.lib.analysis.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
+import com.google.devtools.build.lib.packages.Attribute.Configurator;
+import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
+import com.google.devtools.build.lib.packages.Attribute.Transition;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.RuleTransitionFactory;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.util.Preconditions;
+
+/**
+ * Determines which configurations targets should take.
+ *
+ * <p>This is the "generic engine" for configuration selection. It doesn't know anything about
+ * specific rules or their requirements. Rule writers decide those with appropriately placed
+ * {@link PatchTransition} declarations. This class then processes those declarations to determine
+ * final configurations.
+ */
+public final class ConfigurationResolver {
+  private final DynamicTransitionMapper transitionMapper;
+
+  /**
+   * Instantiates this resolver with a helper class that maps non-{@link PatchTransition}s to
+   * {@link PatchTransition}s.
+   */
+  public ConfigurationResolver(DynamicTransitionMapper transitionMapper) {
+    this.transitionMapper = transitionMapper;
+  }
+
+  /**
+   * Given a parent rule and configuration depending on a child through an attribute, determines
+   * the configuration the child should take.
+   *
+   * @param fromConfig the parent rule's configuration
+   * @param fromRule the parent rule
+   * @param attribute the attribute creating the dependency (e.g. "srcs")
+   * @param toTarget the child target (which may or may not be a rule)
+   *
+   * @return the child's configuration, expressed as a diff from the parent's configuration. This
+   *     is usually a {@PatchTransition} but exceptions apply (e.g.
+   *     {@link Attribute.ConfigurationTransition}).
+   */
+  public Transition evaluateTransition(BuildConfiguration fromConfig, final Rule fromRule,
+      final Attribute attribute, final Target toTarget) {
+
+    // I. Input files and package groups have no configurations. We don't want to duplicate them.
+    if (usesNullConfiguration(toTarget)) {
+      return Attribute.ConfigurationTransition.NULL;
+    }
+
+    // II. Host configurations never switch to another. All prerequisites of host targets have the
+    // same host configuration.
+    if (fromConfig.isHostConfiguration()) {
+      return Attribute.ConfigurationTransition.NONE;
+    }
+
+    // Make sure config_setting dependencies are resolved in the referencing rule's configuration,
+    // unconditionally. For example, given:
+    //
+    // genrule(
+    //     name = 'myrule',
+    //     tools = select({ '//a:condition': [':sometool'] })
+    //
+    // all labels in "tools" get resolved in the host configuration (since the "tools" attribute
+    // declares a host configuration transition). We want to explicitly exclude configuration labels
+    // from these transitions, since their *purpose* is to do computation on the owning
+    // rule's configuration.
+    // TODO(bazel-team): don't require special casing here. This is far too hackish.
+    if (toTarget instanceof Rule && ((Rule) toTarget).getRuleClassObject().isConfigMatcher()) {
+      // TODO(gregce): see if this actually gets called
+      return Attribute.ConfigurationTransition.NONE;
+    }
+
+    // The current transition to apply. When multiple transitions are requested, this is a
+    // ComposingSplitTransition, which encapsulates them into a single object so calling code
+    // doesn't need special logic for combinations.
+    Transition currentTransition = Attribute.ConfigurationTransition.NONE;
+
+    // Apply the parent rule's outgoing transition if it has one.
+    RuleTransitionFactory transitionFactory =
+        fromRule.getRuleClassObject().getOutgoingTransitionFactory();
+    if (transitionFactory != null) {
+      Transition transition = transitionFactory.buildTransitionFor(toTarget.getAssociatedRule());
+      if (transition != null) {
+        currentTransition = composeTransitions(currentTransition, transition);
+      }
+    }
+
+    // TODO(gregce): make the below transitions composable (i.e. take away the "else" clauses).
+    // The "else" is a legacy restriction from static configurations.
+    if (attribute.hasSplitConfigurationTransition()) {
+      Preconditions.checkState(attribute.getConfigurator() == null);
+      currentTransition = split(currentTransition,
+          (SplitTransition<BuildOptions>) attribute.getSplitTransition(fromRule));
+    } else {
+      // III. Attributes determine configurations. The configuration of a prerequisite is determined
+      // by the attribute.
+      @SuppressWarnings("unchecked")
+      Configurator<BuildOptions> configurator =
+          (Configurator<BuildOptions>) attribute.getConfigurator();
+      if (configurator != null) {
+        // TODO(gregce): attribute configurators are a holdover from static configurations. Remove
+        // them and remove this branch.
+        currentTransition = applyAttributeConfigurator(currentTransition, configurator);
+      } else {
+        currentTransition = composeTransitions(currentTransition,
+            attribute.getConfigurationTransition());
+      }
+    }
+
+    return applyConfigurationHook(currentTransition, toTarget);
+  }
+
+  /**
+   * Returns true if the given target should have a null configuration. This method is the
+   * "source of truth" for this determination.
+   */
+  public static boolean usesNullConfiguration(Target target) {
+    return target instanceof InputFile || target instanceof PackageGroup;
+  }
+
+  /**
+   * Composes two transitions together efficiently.
+   */
+  @VisibleForTesting
+  public Transition composeTransitions(Transition transition1, Transition transition2) {
+    if (isFinal(transition1)) {
+      return transition1;
+    } else if (transition2 == Attribute.ConfigurationTransition.NONE) {
+      return transition1;
+    } else if (transition2 == Attribute.ConfigurationTransition.NULL) {
+      // A NULL transition can just replace earlier transitions: no need to cfpose them.
+      return Attribute.ConfigurationTransition.NULL;
+    } else if (transition2 == Attribute.ConfigurationTransition.HOST) {
+      // A HOST transition can just replace earlier transitions: no need to compose them.
+      // But it also improves performance: host transitions are common, and
+      // ConfiguredTargetFunction has special optimized logic to handle them. If they were buried
+      // in the last segment of a ComposingSplitTransition, those optimizations wouldn't trigger.
+      return HostTransition.INSTANCE;
+    }
+
+    // TODO(gregce): remove the below conversion when all transitions are patch transitions.
+    Transition dynamicTransition = transitionMapper.map(transition2);
+    return transition1 == Attribute.ConfigurationTransition.NONE
+        ? dynamicTransition
+        : new ComposingSplitTransition(transition1, dynamicTransition);
+  }
+
+  /**
+   * Returns true if once the given transition is applied to a dep no followup transitions should
+   * be composed after it.
+   */
+  private static boolean isFinal(Transition transition) {
+    return (transition == Attribute.ConfigurationTransition.NULL
+        || transition == HostTransition.INSTANCE);
+  }
+
+  /**
+   * Applies the given split and composes it after an existing transition.
+   */
+  private static Transition split(Transition currentTransition,
+      SplitTransition<BuildOptions> split) {
+    Preconditions.checkState(currentTransition != Attribute.ConfigurationTransition.NULL,
+        "cannot apply splits after null transitions (null transitions are expected to be final)");
+    Preconditions.checkState(currentTransition != HostTransition.INSTANCE,
+        "cannot apply splits after host transitions (host transitions are expected to be final)");
+    return currentTransition == Attribute.ConfigurationTransition.NONE
+        ? split
+        : new ComposingSplitTransition(currentTransition, split);
+  }
+
+  /**
+   * Applies the given attribute configurator and composes it after an existing transition.
+   */
+  @VisibleForTesting
+  public Transition applyAttributeConfigurator(Transition currentTransition,
+      Configurator<BuildOptions> configurator) {
+    if (isFinal(currentTransition)) {
+      return currentTransition;
+    }
+    return composeTransitions(currentTransition, new AttributeConfiguratorTransition(configurator));
+  }
+
+  /**
+   * A {@link PatchTransition} that applies an attribute configurator over some input options
+   * to determine which transition to use, then applies that transition over those options
+   * for the final output.
+   */
+  private static final class AttributeConfiguratorTransition implements PatchTransition {
+    private final Configurator<BuildOptions> configurator;
+
+    AttributeConfiguratorTransition(Configurator<BuildOptions> configurator) {
+      this.configurator = configurator;
+    }
+
+    @Override
+    public BuildOptions apply(BuildOptions options) {
+      return Iterables.getOnlyElement(
+          ComposingSplitTransition.apply(options, configurator.apply(options)));
+    }
+
+    @Override
+    public boolean defaultsToSelf() {
+      return false;
+    }
+  }
+
+  /**
+   * Applies any configuration hooks associated with the dep target, composes their transitions
+   * after an existing transition, and returns the composed result.
+   */
+  private Transition applyConfigurationHook(Transition currentTransition, Target toTarget) {
+    if (isFinal(currentTransition)) {
+      return currentTransition;
+    }
+    Rule associatedRule = toTarget.getAssociatedRule();
+    RuleTransitionFactory transitionFactory =
+        associatedRule.getRuleClassObject().getTransitionFactory();
+    if (transitionFactory != null) {
+      // transitionMapper is only needed because of Attribute.ConfigurationTransition.DATA: this is
+      // C++-specific but non-C++ rules declare it. So they can't directly provide the C++-specific
+      // patch transition that implements it.
+      PatchTransition ruleClassTransition = (PatchTransition)
+          transitionMapper.map(transitionFactory.buildTransitionFor(associatedRule));
+      if (ruleClassTransition != null) {
+        if (currentTransition == ConfigurationTransition.NONE) {
+          return ruleClassTransition;
+        } else {
+          return new ComposingSplitTransition(currentTransition, ruleClassTransition);
+        }
+      }
+    }
+    return currentTransition;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/DynamicTransitionMapper.java b/src/main/java/com/google/devtools/build/lib/analysis/config/DynamicTransitionMapper.java
index 9cd4077..66d0cf9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/DynamicTransitionMapper.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/DynamicTransitionMapper.java
@@ -74,7 +74,9 @@
    * {@link IllegalArgumentException}.
    */
   public Transition map(Transition fromTransition) {
-    if (fromTransition instanceof PatchTransition || fromTransition == null) {
+    if (fromTransition instanceof PatchTransition
+        || fromTransition instanceof Attribute.SplitTransition<?>
+        || fromTransition == null) {
       return fromTransition;
     }
     Transition toTransition = map.get(fromTransition);
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
index 5f10973..06e4c9b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
@@ -101,7 +101,6 @@
       ImmutableMap.of(
           Attribute.ConfigurationTransition.DATA, DisableLipoTransition.INSTANCE,
           LipoTransition.LIPO_COLLECTOR, LipoContextCollectorTransition.INSTANCE
-          // TARGET_CONFIG_FOR_LIPO has no entry because only static configurations use it.
       );
 
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BuildConfigurationFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BuildConfigurationFunction.java
index 266b320..e335cea 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BuildConfigurationFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BuildConfigurationFunction.java
@@ -76,8 +76,7 @@
             directories,
             fragmentsMap,
             key.getBuildOptions(),
-            workspaceNameValue.getName(),
-            ruleClassProvider.getDynamicTransitionMapper());
+            workspaceNameValue.getName());
     return new BuildConfigurationValue(config);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
index 90773dc..f05c5a4 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
@@ -54,7 +54,6 @@
 import com.google.devtools.build.lib.packages.NoSuchPackageException;
 import com.google.devtools.build.lib.packages.NoSuchTargetException;
 import com.google.devtools.build.lib.packages.Package;
-import com.google.devtools.build.lib.packages.RuleClassProvider;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.pkgcache.LoadingFailureEvent;
 import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner;
@@ -108,7 +107,7 @@
   private Set<SkyKey> dirtiedConfiguredTargetKeys = Sets.newConcurrentHashSet();
   private volatile boolean anyConfiguredTargetDeleted = false;
 
-  private final RuleClassProvider ruleClassProvider;
+  private final ConfiguredRuleClassProvider ruleClassProvider;
 
   // The host configuration containing all fragments used by this build's transitive closure.
   private BuildConfiguration topLevelHostConfiguration;
@@ -554,7 +553,7 @@
   }
 
   SkyframeDependencyResolver createDependencyResolver(Environment env) {
-    return new SkyframeDependencyResolver(env);
+    return new SkyframeDependencyResolver(env, ruleClassProvider.getDynamicTransitionMapper());
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
index 965c3fb..0a6e9a7 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.DynamicTransitionMapper;
 import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
@@ -44,7 +45,8 @@
 
   private final Environment env;
 
-  public SkyframeDependencyResolver(Environment env) {
+  public SkyframeDependencyResolver(Environment env, DynamicTransitionMapper transitionMapper) {
+    super(transitionMapper);
     this.env = env;
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java b/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java
index d137a83..3d29927 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/DependencyResolverTest.java
@@ -60,7 +60,7 @@
 
   @Before
   public final void createResolver() throws Exception {
-    dependencyResolver = new DependencyResolver() {
+    dependencyResolver = new DependencyResolver(ruleClassProvider.getDynamicTransitionMapper()) {
 
       @Override
       protected void invalidVisibilityReferenceHook(TargetAndConfiguration node, Label label) {
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
index 83c0891..2aac18d 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BuildEventStreamerTest.java
@@ -614,8 +614,7 @@
                           BuildConfiguration.Fragment>of(),
             BuildOptions.of(ImmutableList.<Class<? extends FragmentOptions>>of(
               BuildConfiguration.Options.class)),
-            "workspace",
-            null);
+            "workspace");
     BuildEvent firstWithConfiguration =
         new GenericConfigurationEvent(testId("first"), configuration);
     BuildEvent secondWithConfiguration =
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java
index 0ea4339..10ad309 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsTest.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.SetMultimap;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.Dependency;
 import com.google.devtools.build.lib.analysis.DependencyResolver;
@@ -120,7 +121,8 @@
         OrderedSetMultimap<Attribute, ConfiguredTarget> depMap =
             ConfiguredTargetFunction.computeDependencies(
                 env,
-                new SkyframeDependencyResolver(env),
+                new SkyframeDependencyResolver(env, ((ConfiguredRuleClassProvider) stateProvider
+                    .lateBoundRuleClassProvider()).getDynamicTransitionMapper()),
                 (TargetAndConfiguration) skyKey.argument(),
                 ImmutableList.<Aspect>of(),
                 ImmutableMap.<Label, ConfigMatchingProvider>of(),
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java
index 9137268..04180a8 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ConfigurationsForTargetsWithDynamicConfigurationsTest.java
@@ -20,19 +20,17 @@
 import static com.google.devtools.build.lib.packages.Attribute.attr;
 import static com.google.devtools.build.lib.packages.BuildType.LABEL;
 import static com.google.devtools.build.lib.syntax.Type.STRING;
-import static org.junit.Assert.fail;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
-import com.google.devtools.build.lib.analysis.AspectCollection;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
-import com.google.devtools.build.lib.analysis.Dependency;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationResolver;
 import com.google.devtools.build.lib.analysis.config.PatchTransition;
 import com.google.devtools.build.lib.analysis.test.TestConfiguration;
 import com.google.devtools.build.lib.analysis.util.MockRule;
@@ -50,6 +48,7 @@
 import com.google.devtools.build.lib.testutil.TestSpec;
 import com.google.devtools.build.lib.util.FileTypeSet;
 import java.util.List;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -59,6 +58,14 @@
 @RunWith(JUnit4.class)
 public class ConfigurationsForTargetsWithDynamicConfigurationsTest
     extends ConfigurationsForTargetsTest {
+
+  private ConfigurationResolver configResolver;
+
+  @Before
+  public void createConfigResolver() {
+    configResolver = new ConfigurationResolver(ruleClassProvider.getDynamicTransitionMapper());
+  }
+
   @Override
   protected FlagBuilder defaultFlags() {
     return super.defaultFlags().with(Flag.DYNAMIC_CONFIGURATIONS);
@@ -505,16 +512,14 @@
   }
 
   /**
-   * Returns the value of {@link TestConfiguration.TestOptions#testFilter} in the output {@link
-   * BuildOptions} the given transition applier returns in its current state.
+   * Returns the value of {@link TestConfiguration.TestOptions#testFilter} for a transition
+   * applied over the target configuration.
    */
-  private List<String> getTestFilterOptionValue(BuildConfiguration.TransitionApplier applier)
+  private List<String> getTestFilterOptionValue(Transition transition)
       throws Exception {
-    Dependency dep = Iterables.getOnlyElement(
-        applier.getDependencies(Label.create("some", "target"), AspectCollection.EMPTY));
     ImmutableList.Builder<String> outValues = ImmutableList.builder();
     for (BuildOptions toOptions : ConfiguredTargetFunction.getDynamicTransitionOptions(
-        getTargetConfiguration().getOptions(), dep.getTransition(),
+        getTargetConfiguration().getOptions(), transition,
         ruleClassProvider.getAllFragments(), ruleClassProvider, false)) {
       outValues.add(toOptions.get(TestConfiguration.TestOptions.class).testFilter);
     }
@@ -524,41 +529,41 @@
   @Test
   public void composedStraightTransitions() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.applyTransition(newPatchTransition("foo"));
-    applier.applyTransition(newPatchTransition("bar"));
-    assertThat(getTestFilterOptionValue(applier)).containsExactly("foobar");
+    assertThat(getTestFilterOptionValue(
+        configResolver.composeTransitions(
+            newPatchTransition("foo"),
+            newPatchTransition("bar"))))
+        .containsExactly("foobar");
   }
 
   @Test
   public void composedStraightTransitionThenSplitTransition() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.applyTransition(newPatchTransition("foo"));
-    applier.split(newSplitTransition("split"));
-    assertThat(getTestFilterOptionValue(applier)).containsExactly("foosplit1", "foosplit2");
+    assertThat(getTestFilterOptionValue(
+        configResolver.composeTransitions(
+            newPatchTransition("foo"),
+            newSplitTransition("split"))))
+        .containsExactly("foosplit1", "foosplit2");
   }
 
   @Test
   public void composedSplitTransitionThenStraightTransition() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.split(newSplitTransition("split"));
-    applier.applyTransition(newPatchTransition("foo"));
-    assertThat(getTestFilterOptionValue(applier)).containsExactly("split1foo", "split2foo");
+    assertThat(getTestFilterOptionValue(
+        configResolver.composeTransitions(
+            newSplitTransition("split"),
+            newPatchTransition("foo"))))
+        .containsExactly("split1foo", "split2foo");
   }
 
   @Test
   public void composedSplitTransitions() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.split(newSplitTransition("split"));
-    try {
-      applier.split(newSplitTransition("disallowed second split"));
-      fail("expected failure: deps cannot apply more than one split transition each");
-    } catch (IllegalStateException e) {
-      assertThat(e).hasMessageThat().contains("dependency edges may apply at most one split");
-    }
+    assertThat(getTestFilterOptionValue(
+        configResolver.composeTransitions(
+            newSplitTransition("s"),
+            newSplitTransition("t"))))
+        .containsExactly("s1t1", "s1t2", "s2t1", "s2t2");
   }
 
   /**
@@ -591,18 +596,21 @@
   @Test
   public void attributeConfigurator() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.applyAttributeConfigurator(newAttributeWithStaticConfigurator("from attr"));
-    assertThat(getTestFilterOptionValue(applier)).containsExactly("from attr");
+    assertThat(getTestFilterOptionValue(
+        configResolver.applyAttributeConfigurator(
+            Attribute.ConfigurationTransition.NONE,
+            newAttributeWithStaticConfigurator("from attr"))))
+        .containsExactly("from attr");
   }
 
   @Test
   public void straightTransitionThenAttributeConfigurator() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.applyTransition(newPatchTransition("from patch "));
-    applier.applyAttributeConfigurator(newAttributeWithStaticConfigurator("from attr"));
-    assertThat(getTestFilterOptionValue(applier)).containsExactly("from patch from attr");
+    assertThat(getTestFilterOptionValue(
+        configResolver.applyAttributeConfigurator(
+            newPatchTransition("from patch "),
+            newAttributeWithStaticConfigurator("from attr"))))
+        .containsExactly("from patch from attr");
   }
 
   /**
@@ -625,20 +633,23 @@
   @Test
   public void splitTransitionThenAttributeConfigurator() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.split(newSplitTransition(" split"));
-    applier.applyAttributeConfigurator(ATTRIBUTE_WITH_REPEATING_CONFIGURATOR);
-    assertThat(getTestFilterOptionValue(applier))
+    assertThat(getTestFilterOptionValue(
+        configResolver.applyAttributeConfigurator(
+            newSplitTransition(" split"),
+            ATTRIBUTE_WITH_REPEATING_CONFIGURATOR)))
         .containsExactly(" split1 split1 (attr)", " split2 split2 (attr)");
   }
 
   @Test
   public void composedAttributeConfigurators() throws Exception {
     update(); // Creates the target configuration.
-    BuildConfiguration.TransitionApplier applier = getTargetConfiguration().getTransitionApplier();
-    applier.applyAttributeConfigurator(newAttributeWithStaticConfigurator("from attr 1 "));
-    applier.applyAttributeConfigurator(newAttributeWithStaticConfigurator("from attr 2"));
-    assertThat(getTestFilterOptionValue(applier)).containsExactly("from attr 1 from attr 2");
+    assertThat(getTestFilterOptionValue(
+        configResolver.applyAttributeConfigurator(
+            configResolver.applyAttributeConfigurator(
+                Attribute.ConfigurationTransition.NONE,
+                newAttributeWithStaticConfigurator("from attr 1 ")),
+            ATTRIBUTE_WITH_REPEATING_CONFIGURATOR)))
+        .containsExactly("from attr 1 from attr 1  (attr)");
   }
 
   /** Sets {@link TestConfiguration.TestOptions#testFilter} to the rule class of the given rule. */