// Copyright 2014 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;

import static com.google.devtools.build.lib.analysis.DependencyKind.OUTPUT_FILE_RULE_DEPENDENCY;
import static com.google.devtools.build.lib.analysis.DependencyKind.TRANSITIVE_VISIBILITY_DEPENDENCY;
import static com.google.devtools.build.lib.analysis.DependencyKind.VISIBILITY_DEPENDENCY;

import com.google.auto.value.AutoOneOf;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
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.DependencyKind.AttributeDependencyKind;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.analysis.config.ExecutionTransitionFactory;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.transitions.TransitionFactory;
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Aspect;
import com.google.devtools.build.lib.packages.AspectClass;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
import com.google.devtools.build.lib.packages.Attribute.LateBoundDefault;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.AttributeTransitionData;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ConfiguredAttributeMapper;
import com.google.devtools.build.lib.packages.DeclaredExecGroup;
import com.google.devtools.build.lib.packages.EnvironmentGroup;
import com.google.devtools.build.lib.packages.InputFile;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.PackageGroup;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.skyframe.toolchains.UnloadedToolchainContext;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.syntax.Location;

/**
 * Helpers for resolution for dependencies between configured targets.
 *
 * <p>Includes logic to determine all attribute dependencies and their associated labels.
 */
public final class DependencyResolutionHelpers {

  private DependencyResolutionHelpers() {}

  /** The tuple {@link #computeDependencyLabels} outputs. */
  public static final class DependencyLabels {
    private final OrderedSetMultimap<DependencyKind, Label> labels;
    @Nullable private final ConfiguredAttributeMapper attributeMap;

    private DependencyLabels(
        OrderedSetMultimap<DependencyKind, Label> labels,
        @Nullable ConfiguredAttributeMapper attributeMap) {
      this.labels = labels;
      this.attributeMap = attributeMap;
    }

    public OrderedSetMultimap<DependencyKind, Label> labels() {
      return labels;
    }

    @Nullable // Non-null for rules and output files when there are aspects that apply to files.
    public ConfiguredAttributeMapper attributeMap() {
      return attributeMap;
    }
  }

  public static DependencyLabels computeDependencyLabels(
      TargetAndConfiguration node,
      ImmutableList<Aspect> aspects,
      ImmutableMap<Label, ConfigMatchingProvider> configConditions,
      @Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
      @Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts)
      throws Failure, InterruptedException {
    Target target = node.getTarget();
    BuildConfigurationValue config = node.getConfiguration();
    OrderedSetMultimap<DependencyKind, Label> outgoingLabels = OrderedSetMultimap.create();

    // TODO(bazel-team): Figure out a way to implement the below using LabelVisitationUtils.
    Rule fromRule;
    ConfiguredAttributeMapper attributeMap = null;
    if (target instanceof OutputFile) {
      Preconditions.checkNotNull(config);
      addVisibilityDepLabels(target.getVisibilityDependencyLabels(), outgoingLabels);
      addTransitiveVisibilityDepLabel(
          target.getPackageDeclarations().getPackageArgs().transitiveVisibility(), outgoingLabels);
      Rule rule = ((OutputFile) target).getGeneratingRule();
      outgoingLabels.put(OUTPUT_FILE_RULE_DEPENDENCY, rule.getLabel());
      if (Iterables.any(aspects, a -> a.getDefinition().applyToFiles())) {
        attributeMap = ConfiguredAttributeMapper.of(rule, configConditions, config);
        resolveAttributes(getAspectAttributes(aspects), outgoingLabels, rule, attributeMap, config);
      }
      addToolchainDeps(toolchainContexts, outgoingLabels);
    } else if (target instanceof InputFile) {
      addVisibilityDepLabels(target.getVisibilityDependencyLabels(), outgoingLabels);
      addTransitiveVisibilityDepLabel(
          target.getPackageDeclarations().getPackageArgs().transitiveVisibility(), outgoingLabels);
    } else if (target instanceof EnvironmentGroup) {
      addVisibilityDepLabels(target.getVisibilityDependencyLabels(), outgoingLabels);
    } else if (target instanceof Rule rule) {
      fromRule = rule;
      attributeMap = ConfiguredAttributeMapper.of(fromRule, configConditions, config);
      addTransitiveVisibilityDepLabel(
          fromRule.getPackageDeclarations().getPackageArgs().transitiveVisibility(),
          outgoingLabels);
      visitRule(
          node,
          aspects,
          attributeMap,
          toolchainContexts,
          baseTargetUnloadedToolchainContexts,
          outgoingLabels);
    } else if (target instanceof PackageGroup packageGroup) {
      outgoingLabels.putAll(VISIBILITY_DEPENDENCY, packageGroup.getIncludes());
    } else {
      throw new IllegalStateException(target.getLabel().toString());
    }
    return new DependencyLabels(outgoingLabels, attributeMap);
  }

  /** The results of {@link #getExecutionPlatformLabel} as a tagged union. */
  @AutoOneOf(DependencyResolutionHelpers.ExecutionPlatformResult.Kind.class)
  public abstract static class ExecutionPlatformResult {
    /** Tags for the possible results. */
    public enum Kind {
      /** A label was successfully determined. */
      LABEL,
      /**
       * A label was successfully determined to be null.
       *
       * <p>{@link AutoOneOf} does not permit {@code @Nullable} so this is distinct from {@link
       * #LABEL}.
       */
      NULL_LABEL,
      /**
       * The dependency should be skipped.
       *
       * <p>See comments in {@link #getExecutionPlatformLabel} for details.
       */
      SKIP,
      /** An error message. */
      ERROR
    }

    public abstract Kind kind();

    public abstract Label label();

    abstract void nullLabel();

    abstract void skip();

    public abstract String error();

    private static ExecutionPlatformResult ofLabel(Label label) {
      return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.label(label);
    }

    private static ExecutionPlatformResult ofNullLabel() {
      return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.nullLabel();
    }

    private static ExecutionPlatformResult ofSkip() {
      return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.skip();
    }

    private static ExecutionPlatformResult ofError(String message) {
      return AutoOneOf_DependencyResolutionHelpers_ExecutionPlatformResult.error(message);
    }
  }

  public static ExecutionPlatformResult getExecutionPlatformLabel(
      AttributeDependencyKind kind,
      @Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
      @Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts,
      ImmutableList<Aspect> aspectsList) {
    if (aspectsList.isEmpty() || isMainAspect(aspectsList, kind.getOwningAspect())) {
      return getExecutionPlatformLabel(kind, toolchainContexts);
    } else if (kind.getOwningAspect() == null) {
      // During aspect evaluation, use {@code baseTargetUnloadedToolchainContexts} for the base
      // target's dependencies.
      return getExecutionPlatformLabel(kind, baseTargetUnloadedToolchainContexts);
    } else {
      ExecutionPlatformResult executionPlatformResult =
          getExecutionPlatformLabel(kind, toolchainContexts);
      if (executionPlatformResult.kind() == ExecutionPlatformResult.Kind.ERROR) {
        // TODO(b/373963347): Make the toolchain contexts of base aspects available to be used with
        // their corresponding dependencies.
        // Currently dependencies of the base aspects are resolved with the toolchain context of the
        // main aspect, skip errors as actual errors would be reported during the base aspect
        // evaluation.
        return ExecutionPlatformResult.ofSkip();
      } else {
        return executionPlatformResult;
      }
    }
  }

  private static ExecutionPlatformResult getExecutionPlatformLabel(
      AttributeDependencyKind kind,
      @Nullable ToolchainCollection<? extends ToolchainContext> toolchainContexts) {
    if (toolchainContexts == null) {
      return ExecutionPlatformResult.ofNullLabel();
    }

    // The exec transition may be composed with other transitions (via transition.and_then), so
    // look for it anywhere in the (possibly composed) transition factory.
    ExecutionTransitionFactory execTransitionFactory =
        findExecutionTransitionFactory(kind.getAttribute().getTransitionFactory());
    if (execTransitionFactory == null) {
      return ExecutionPlatformResult.ofLabel(
          toolchainContexts
              .getToolchainContext(DeclaredExecGroup.DEFAULT_EXEC_GROUP_NAME)
              .executionPlatform()
              .label());
    }

    String execGroup = execTransitionFactory.getExecGroup();
    if (toolchainContexts.hasToolchainContext(execGroup)) {
      PlatformInfo platform = toolchainContexts.getToolchainContext(execGroup).executionPlatform();
      return platform == null
          ? ExecutionPlatformResult.ofNullLabel()
          : ExecutionPlatformResult.ofLabel(platform.label());
    }

    return ExecutionPlatformResult.ofError(
        String.format(
            "Attr '%s' declares a transition for non-existent exec group '%s'",
            kind.getAttribute().getName(), execGroup));
  }

  /**
   * Returns the {@link ExecutionTransitionFactory} within the given transition factory, or {@code
   * null} if there is none. A composition contains at most one native transition, so there is at
   * most one match.
   */
  @Nullable
  private static ExecutionTransitionFactory findExecutionTransitionFactory(
      TransitionFactory<AttributeTransitionData> transitionFactory) {
    var found = new ExecutionTransitionFactory[1];
    transitionFactory.visit(
        factory -> {
          if (factory instanceof ExecutionTransitionFactory execFactory) {
            found[0] = execFactory;
          }
        });
    return found[0];
  }

  /** True if {@code owningAspect} is the main aspect, the last one in {@code aspectsList}. */
  private static boolean isMainAspect(
      ImmutableList<Aspect> aspectsList, @Nullable AspectClass owningAspect) {
    return Iterables.getLast(aspectsList).getAspectClass().equals(owningAspect);
  }

  /** Indicates a failure during dependency resolution. */
  public static class Failure extends Exception {
    @Nullable private final Location location;

    private Failure(Location location, String message) {
      super(message);
      this.location = location;
    }

    /** Returns the location of the error, if known. */
    @Nullable
    public Location getLocation() {
      return location;
    }
  }

  private static void visitRule(
      TargetAndConfiguration node,
      ImmutableList<Aspect> aspects,
      ConfiguredAttributeMapper attributeMap,
      @Nullable ToolchainCollection<ToolchainContext> toolchainContexts,
      @Nullable ToolchainCollection<UnloadedToolchainContext> baseTargetUnloadedToolchainContexts,
      OrderedSetMultimap<DependencyKind, Label> outgoingLabels)
      throws Failure, InterruptedException {
    Preconditions.checkArgument(node.getTarget() instanceof Rule, node);
    BuildConfigurationValue ruleConfig = Preconditions.checkNotNull(node.getConfiguration(), node);
    Rule rule = (Rule) node.getTarget();

    try {
      attributeMap.validateAttributes();
    } catch (ConfiguredAttributeMapper.ValidationException ex) {
      throw new Failure(rule.getLocation(), ex.getMessage());
    }

    Iterable<Label> visibilityDepLabels = rule.getVisibilityDependencyLabels();
    addVisibilityDepLabels(visibilityDepLabels, outgoingLabels);
    resolveAttributes(getAttributes(rule, aspects), outgoingLabels, rule, attributeMap, ruleConfig);

    // Add the rule's visibility labels (which may come from the rule or from package defaults).
    addExplicitDeps(outgoingLabels, rule, "visibility", visibilityDepLabels);

    // Add package default constraints when the rule doesn't explicitly declare them.
    //
    // Note that this can have subtle implications for constraint semantics. For example: say that
    // package defaults declare compatibility with ':foo' and rule R declares compatibility with
    // ':bar'. Does that mean that R is compatible with [':foo', ':bar'] or just [':bar']? In other
    // words, did R's author intend to add additional compatibility to the package defaults or to
    // override them? More severely, what if package defaults "restrict" support to just [':baz']?
    // Should R's declaration signify [':baz'] + ['bar'], [ORIGINAL_DEFAULTS] + ['bar'], or
    // something else?
    //
    // Rather than try to answer these questions with possibly confusing logic, we take the
    // simple approach of assigning the rule's "restriction" attribute to the rule-declared value if
    // it exists, else the package defaults value (and likewise for "compatibility"). This may not
    // always provide what users want, but it makes it easy for them to understand how rule
    // declarations and package defaults intermix (and how to refactor them to get what they want).
    //
    // An alternative model would be to apply the "rule declaration" / "rule class defaults"
    // relationship, i.e. the rule class' "compatibility" and "restriction" declarations are merged
    // to generate a set of default environments, then the rule's declarations are independently
    // processed on top of that. This protects against obscure coupling behavior between
    // declarations from wildly different places (e.g. it offers clear answers to the examples posed
    // above). But within the scope of a single package it seems better to keep the model simple and
    // make the user responsible for resolving ambiguities.
    if (!rule.isAttributeValueExplicitlySpecified(RuleClass.COMPATIBLE_ENVIRONMENT_ATTR)) {
      addExplicitDeps(
          outgoingLabels,
          rule,
          RuleClass.COMPATIBLE_ENVIRONMENT_ATTR,
          rule.getPackageDeclarations().getPackageArgs().defaultCompatibleWith());
    }
    if (!rule.isAttributeValueExplicitlySpecified(RuleClass.RESTRICTED_ENVIRONMENT_ATTR)) {
      addExplicitDeps(
          outgoingLabels,
          rule,
          RuleClass.RESTRICTED_ENVIRONMENT_ATTR,
          rule.getPackageDeclarations().getPackageArgs().defaultRestrictedTo());
    }

    addToolchainDeps(toolchainContexts, outgoingLabels);
    addBaseTargetToolchainDeps(baseTargetUnloadedToolchainContexts, outgoingLabels);
  }

  private static void addToolchainDeps(
      ToolchainCollection<ToolchainContext> toolchainContexts,
      OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
    if (toolchainContexts != null) {
      for (Map.Entry<String, ToolchainContext> entry : toolchainContexts.contextMap().entrySet()) {
        outgoingLabels.putAll(
            DependencyKind.forExecGroup(entry.getKey()),
            entry.getValue().resolvedToolchainLabels());
      }
    }
  }

  private static void addBaseTargetToolchainDeps(
      @Nullable ToolchainCollection<UnloadedToolchainContext> toolchainContexts,
      OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
    if (toolchainContexts == null) {
      return;
    }
    for (Map.Entry<String, UnloadedToolchainContext> execGroup :
        toolchainContexts.contextMap().entrySet()) {
      for (var toolchainTypeToResolved :
          execGroup.getValue().toolchainTypeToResolved().asMap().entrySet()) {
        // map entries from (exec group, toolchain type) to resolved toolchain labels. We need to
        // distinguish the resolved toolchains per type because aspects propagate on toolchains
        // based on the types specified in `toolchains_aspects`. So even if 2 types resolved to the
        // same toolchain target, their CT will be different if an aspect propagates to one type but
        // not the other.
        outgoingLabels.putAll(
            DependencyKind.forBaseTargetExecGroup(
                execGroup.getKey(), toolchainTypeToResolved.getKey().typeLabel()),
            toolchainTypeToResolved.getValue());
      }
    }
  }

  private static void resolveAttributes(
      Iterable<AttributeDependencyKind> attributeDependencyKinds,
      OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
      Rule rule,
      ConfiguredAttributeMapper attributeMap,
      BuildConfigurationValue ruleConfig)
      throws InterruptedException {
    for (AttributeDependencyKind dependencyKind : attributeDependencyKinds) {
      Attribute attribute = dependencyKind.getAttribute();
      // Not only is resolving CONFIG_SETTING_DEPS_ATTRIBUTE deps here wasteful, since the only
      // place they're used is in ConfiguredTargetFunction.getConfigConditions, but it actually
      // breaks trimming as shown by
      // FeatureFlagManualTrimmingTest#featureFlagInUnusedSelectBranchButNotInTransitiveConfigs_DoesNotError
      // because it resolves a dep that trimming (correctly) doesn't account for because it's part
      // of an unchosen select() branch.
      if (attribute.getName().equals(RuleClass.CONFIG_SETTING_DEPS_ATTRIBUTE)) {
        continue;
      }
      Type<?> type = attribute.getType();
      if (type == BuildType.OUTPUT
          || type == BuildType.OUTPUT_LIST
          || type == BuildType.NODEP_LABEL
          || type == BuildType.NODEP_LABEL_LIST
          || type == BuildType.DORMANT_LABEL
          || type == BuildType.DORMANT_LABEL_LIST
          || type == BuildType.GENQUERY_SCOPE_TYPE
          || type == BuildType.GENQUERY_SCOPE_TYPE_LIST) {
        // These types invoke visitLabels() so that they are reported in "bazel query" but do not
        // create a dependency. Maybe it's better to remove that, but then the labels() query
        // function would need to be rethought.
        continue;
      }

      resolveAttribute(
          attribute, type, dependencyKind, outgoingLabels, rule, attributeMap, ruleConfig);
    }
  }

  private static <T> void resolveAttribute(
      Attribute attribute,
      Type<T> type,
      AttributeDependencyKind dependencyKind,
      OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
      Rule rule,
      ConfiguredAttributeMapper attributeMap,
      BuildConfigurationValue ruleConfig)
      throws InterruptedException {
    T attributeValue = null;
    if (attribute.isImplicit()) {
      // Since the attributes that come from aspects do not appear in attributeMap, we have to get
      // their values from somewhere else. This incidentally means that aspects attributes are not
      // configurable. It would be nice if that wasn't the case, but we'd have to revamp how
      // attribute mapping works, which is a large chunk of work.
      if (dependencyKind.getOwningAspect() == null) {
        attributeValue = attributeMap.get(attribute.getName(), type);
      } else {
        Object defaultValue = attribute.getDefaultValue(rule);
        attributeValue =
            type.cast(
                defaultValue instanceof ComputedDefault computedDefault
                    ? computedDefault.getDefault(attributeMap)
                    : defaultValue);
      }
    } else if (attribute.isMaterializing()) {
      // These attributes are resolved by calling the materializer function in
      // DependencyMapProducer. The reason is that they need the analyzed versions some direct
      // dependencies and we can't do that here.
      outgoingLabels.put(dependencyKind, null);
    } else if (attribute.isLateBound()) {
      attributeValue =
          type.cast(resolveLateBoundDefault(rule, attributeMap, attribute, ruleConfig));
    } else if (dependencyKind.getOwningAspect() == null && attributeMap.has(attribute.getName())) {
      // This condition is false for aspect attributes that do not give rise to dependencies because
      // attributes that come from aspects do not appear in attributeMap (see the comment in the
      // case that handles implicit attributes).
      attributeValue = attributeMap.get(attribute.getName(), type);
    }

    if (attributeValue == null) {
      return;
    }

    type.visitLabels(
        (depLabel, ctx) -> outgoingLabels.put(dependencyKind, depLabel),
        attributeValue,
        /*context=*/ null);
  }

  @Nullable
  @VisibleForTesting(/* used to test LateBoundDefaults' default values */ )
  static <FragmentT> Object resolveLateBoundDefault(
      Rule rule, AttributeMap attributeMap, Attribute attribute, BuildConfigurationValue ruleConfig)
      throws InterruptedException {
    Preconditions.checkState(!attribute.getTransitionFactory().isSplit());
    @SuppressWarnings("unchecked")
    LateBoundDefault<FragmentT, ?> lateBoundDefault =
        (LateBoundDefault<FragmentT, ?>) attribute.getLateBoundDefault();

    Class<FragmentT> fragmentClass = lateBoundDefault.getFragmentClass();
    try {
      // TODO(b/65746853): remove this when nothing uses it anymore
      if (BuildConfigurationValue.class.equals(fragmentClass)
          // noconfig targets can't meaningfully parse late-bound defaults. See NoConfigTransition.
          && !ruleConfig.getOptions().hasNoConfig()) {
        return lateBoundDefault.resolve(rule, attributeMap, fragmentClass.cast(ruleConfig));
      }
      if (Void.class.equals(fragmentClass)) {
        return lateBoundDefault.resolve(
            rule, attributeMap, /* input= */ null
            /* analysisContext= */
            /* eventHandler= */ );
      }
      @SuppressWarnings("unchecked")
      FragmentT fragment =
          fragmentClass.cast(ruleConfig.getFragment((Class<? extends Fragment>) fragmentClass));
      if (fragment == null) {
        return null;
      }
      return lateBoundDefault.resolve(
          rule, attributeMap, fragment
          /* analysisContext= */
          /* eventHandler= */ );
    } catch (EvalException e) {
      // Materializers should not be called here and those are the only kind of late-bound defaults
      // that can throw these exceptions.
      throw new IllegalStateException(e);
    }
  }

  /**
   * Adds new dependencies to the given rule under the given attribute name
   *
   * @param attrName the name of the attribute to add dependency labels to
   * @param labels the dependencies to add
   */
  private static void addExplicitDeps(
      OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
      Rule rule,
      String attrName,
      Iterable<Label> labels) {
    if (!rule.isAttrDefined(attrName, BuildType.LABEL_LIST)
        && !rule.isAttrDefined(attrName, BuildType.NODEP_LABEL_LIST)) {
      return;
    }
    Attribute attribute =
        rule.getRuleClassObject().getAttributeProvider().getAttributeByName(attrName);
    outgoingLabels.putAll(AttributeDependencyKind.forRule(attribute), labels);
  }

  /** Returns the attributes that should be visited for this rule/aspect combination. */
  private static ImmutableList<AttributeDependencyKind> getAttributes(
      Rule rule, ImmutableList<Aspect> aspects) {
    ImmutableList.Builder<AttributeDependencyKind> result = ImmutableList.builder();
    HashSet<String> ruleAndBaseAspectsProcessedAttributes = new HashSet<>();

    // For aspects evaluation, all attributes of the main aspect (last aspect in {@code aspects}
    // should be added, even if they have the same name as an attribute in the rule or a base aspect
    // because main aspect attributes are separated and retrieved from `ctx.attr`.

    // Attributes of the underlying rule and base aspects are merged and retrieved from
    // `ctx.rule.attr` with rule attributes taking precedence then aspects' attributes based on the
    // aspect order in the aspects path (lowest order to highest).

    List<Attribute> ruleAttributes =
        rule.getRuleClassObject().getAttributeProvider().getAttributes();
    for (Attribute attribute : ruleAttributes) {
        result.add(AttributeDependencyKind.forRule(attribute));
      ruleAndBaseAspectsProcessedAttributes.add(attribute.getName());
    }

    addAspectAttributes(aspects, ruleAndBaseAspectsProcessedAttributes, result);

    return result.build();
  }

  private static ImmutableList<AttributeDependencyKind> getAspectAttributes(
      ImmutableList<Aspect> aspects) {
    ImmutableList.Builder<AttributeDependencyKind> result = ImmutableList.builder();
    addAspectAttributes(aspects, new HashSet<>(), result);
    return result.build();
  }

  private static void addAspectAttributes(
      ImmutableList<Aspect> aspects,
      Set<String> processedAttributes,
      ImmutableList.Builder<AttributeDependencyKind> attributes) {

    if (aspects.isEmpty()) {
      return;
    }

    // Add all the main aspect's attributes
    Aspect mainAspect = Iterables.getLast(aspects, null);
    for (Attribute attribute : mainAspect.getDefinition().getAttributes().values()) {
      attributes.add(AttributeDependencyKind.forAspect(attribute, mainAspect.getAspectClass()));
    }

    // For base aspects, if multiple attributes have the same name, take the first encountered in
    // the aspects path.
    for (Aspect aspect : aspects.subList(0, aspects.size() - 1)) {
      for (Attribute attribute : aspect.getDefinition().getAttributes().values()) {
        if (processedAttributes.add(attribute.getName())) {
          attributes.add(AttributeDependencyKind.forAspect(attribute, aspect.getAspectClass()));
        }
      }
    }
  }

  private static void addVisibilityDepLabels(
      Iterable<Label> labels, OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
    outgoingLabels.putAll(VISIBILITY_DEPENDENCY, labels);
  }

  private static void addTransitiveVisibilityDepLabel(
      Label label, OrderedSetMultimap<DependencyKind, Label> outgoingLabels) {
    if (label != null) {
      outgoingLabels.put(TRANSITIVE_VISIBILITY_DEPENDENCY, label);
    }
  }
}
