Simplify `DependencyResolver` attribute resolution.

 - Move the logic for resolving aspect/rule merged attributes into `AspectAwareAttributeMapper` (this is a dedicated class for this purpose)
 - Make `AspectAwareAttributeMapper` more powerful (it now resolves *values*) with less code
 - Simplify `DependencyResolver`'s code path
 - Eliminate the duplicate logic for excluding "nodep" dependencies
 - Reduce LoC by a nice little amount

Testing: since this is core logic, all changes have test coverage - if not directly then by the build logic that expects the invariants preserved here
PiperOrigin-RevId: 424938613
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapper.java
index 6e5e797..1386282 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapper.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapper.java
@@ -17,80 +17,78 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.packages.AbstractAttributeMapper;
 import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
 import com.google.devtools.build.lib.packages.AttributeMap;
 import com.google.devtools.build.lib.packages.DependencyFilter;
+import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.Type;
+import java.util.Map;
 import java.util.function.BiConsumer;
-import java.util.function.Consumer;
 
 /**
- * An {@link AttributeMap} that supports attribute type queries on both a rule and its aspects and
- * attribute value queries on the rule.
+ * An {@link AttributeMap} that supports queries on both a rule and its aspects.
  *
- * <p>An attribute type query is anything accessible from {@link Attribute} (i.e. anything about how
- * the attribute is integrated into the {@link com.google.devtools.build.lib.packages.RuleClass}).
- * An attribute value query is anything related to the actual value an attribute takes.
+ * <p>When both the rule and aspect declare the same attribute, the aspect's value takes precedence.
+ * This is because aspects expect access to the merged rule/attribute data (including possible
+ * aspect overrides) while rules evaluate before aspects are attached.
  *
- * <p>For example, given {@code deps = [":adep"]}, checking that {@code deps} exists or that it's
- * type is {@link com.google.devtools.build.lib.packages.BuildType#LABEL_LIST} are type queries.
- * Checking that its value is explicitly set in the BUILD File or that its value {@code [":adep"]}
- * are value queries..
- *
- * <p>Value queries on aspect attributes trigger {@link UnsupportedOperationException}.
+ * <p>Note that public aspect attributes must be strings that inherit the values of the equivalent
+ * rule attributes. Only private (implicit) attributes can have different values.
  */
-class AspectAwareAttributeMapper implements AttributeMap {
-  private final AttributeMap ruleAttributes;
+final class AspectAwareAttributeMapper extends AbstractAttributeMapper {
+  private final AbstractAttributeMapper ruleAttributes;
+  // Attribute name -> definition.
   private final ImmutableMap<String, Attribute> aspectAttributes;
+  // Attribute name -> value. null values (which are valid) are excluded from this map. Use
+  // aspectAttributes to check attribute existence.
+  private final ImmutableMap<String, Object> aspectAttributeValues;
 
-  public AspectAwareAttributeMapper(AttributeMap ruleAttributes,
+  public AspectAwareAttributeMapper(
+      Rule rule,
+      AbstractAttributeMapper ruleAttributes,
       ImmutableMap<String, Attribute> aspectAttributes) {
+    super(rule);
     this.ruleAttributes = ruleAttributes;
     this.aspectAttributes = aspectAttributes;
-  }
-
-  @Override
-  public String getName() {
-    return ruleAttributes.getName();
-  }
-
-  @Override
-  public String getRuleClassName() {
-    return ruleAttributes.getRuleClassName();
-  }
-
-  @Override
-  public Label getLabel() {
-    return ruleAttributes.getLabel();
+    ImmutableMap.Builder<String, Object> valueBuilder = new ImmutableMap.Builder<>();
+    for (Map.Entry<String, Attribute> aspectAttribute : aspectAttributes.entrySet()) {
+      Attribute attribute = aspectAttribute.getValue();
+      Object defaultValue = attribute.getDefaultValue(rule);
+      Object attributeValue =
+          attribute
+              .getType()
+              .cast(
+                  defaultValue instanceof ComputedDefault
+                      ? ((ComputedDefault) defaultValue).getDefault(ruleAttributes)
+                      : defaultValue);
+      if (attributeValue != null) {
+        valueBuilder.put(aspectAttribute.getKey(), attributeValue);
+      }
+    }
+    this.aspectAttributeValues = valueBuilder.buildOrThrow();
   }
 
   @Override
   public <T> T get(String attributeName, Type<T> type) {
+    Attribute aspectAttribute = aspectAttributes.get(attributeName);
+    if (aspectAttribute != null) {
+      if (aspectAttribute.getType() != type) {
+        throw new IllegalArgumentException(
+            String.format(
+                "attribute %s has type %s, not expected type %s",
+                attributeName, aspectAttribute.getType(), type));
+      }
+      return type.cast(aspectAttributeValues.get(attributeName));
+    }
     if (ruleAttributes.has(attributeName, type)) {
       return ruleAttributes.get(attributeName, type);
-    } else {
-      Attribute attribute = aspectAttributes.get(attributeName);
-      if (attribute == null) {
-        throw new IllegalArgumentException(String.format(
+    }
+    throw new IllegalArgumentException(
+        String.format(
             "no attribute '%s' in either %s or its aspects",
             attributeName, ruleAttributes.getLabel()));
-      } else if (attribute.getType() != type) {
-        throw new IllegalArgumentException(String.format(
-            "attribute %s has type %s, not expected type %s",
-            attributeName, attribute.getType(), type));
-      } else {
-        throw new UnsupportedOperationException(
-            String.format(
-                "Attribute '%s' comes from an aspect. "
-                    + "Value retrieval for aspect attributes is not supported.",
-                attributeName));
-      }
-    }
-  }
-
-  @Override
-  public boolean isConfigurable(String attributeName) {
-    return ruleAttributes.isConfigurable(attributeName);
   }
 
   @Override
@@ -103,23 +101,20 @@
 
   @Override
   public Type<?> getAttributeType(String attrName) {
-    Type<?> type = ruleAttributes.getAttributeType(attrName);
-    if (type != null) {
-      return type;
-    } else {
-      Attribute attribute = aspectAttributes.get(attrName);
-      return attribute != null ? attribute.getType() : null;
+    Attribute aspectAttribute = aspectAttributes.get(attrName);
+    if (aspectAttribute != null) {
+      return aspectAttribute.getType();
     }
+    return ruleAttributes.getAttributeType(attrName);
   }
 
   @Override
   public Attribute getAttributeDefinition(String attrName) {
-    Attribute attribute = ruleAttributes.getAttributeDefinition(attrName);
-    if (attribute != null) {
-      return attribute;
-    } else {
-      return aspectAttributes.get(attrName);
+    Attribute aspectAttribute = aspectAttributes.get(attrName);
+    if (aspectAttribute != null) {
+      return aspectAttribute;
     }
+    return ruleAttributes.getAttributeDefinition(attrName);
   }
 
   @Override
@@ -128,43 +123,26 @@
   }
 
   @Override
-  public void visitAllLabels(BiConsumer<Attribute, Label> consumer) {
-    throw new UnsupportedOperationException("rule + aspects label visition is not supported");
-  }
-
-  @Override
-  public void visitLabels(Attribute attribute, Consumer<Label> consumer) {
-    throw new UnsupportedOperationException("rule + aspects label visition is not supported");
-  }
-
-  @Override
   public void visitLabels(DependencyFilter filter, BiConsumer<Attribute, Label> consumer) {
-    throw new UnsupportedOperationException("rule + aspects label visition is not supported");
+    ImmutableList.Builder<Attribute> combined = ImmutableList.builder();
+    combined.addAll(rule.getAttributes());
+    aspectAttributes.values().forEach(combined::add);
+    visitLabels(combined.build(), filter, consumer);
   }
 
   @Override
-  public String getPackageDefaultHdrsCheck() {
-    return ruleAttributes.getPackageDefaultHdrsCheck();
-  }
-
-  @Override
-  public boolean isPackageDefaultHdrsCheckSet() {
-    return ruleAttributes.isPackageDefaultHdrsCheckSet();
-  }
-
-  @Override
-  public Boolean getPackageDefaultTestOnly() {
-    return ruleAttributes.getPackageDefaultTestOnly();
-  }
-
-  @Override
-  public String getPackageDefaultDeprecation() {
-    return ruleAttributes.getPackageDefaultDeprecation();
-  }
-
-  @Override
-  public ImmutableList<String> getPackageDefaultCopts() {
-    return ruleAttributes.getPackageDefaultCopts();
+  public <T> void visitLabels(Attribute attribute, Type<T> type, Type.LabelVisitor visitor) {
+    Attribute aspectAttr = aspectAttributes.get(attribute.getName());
+    if (aspectAttr != null) {
+      T value = type.cast(aspectAttributeValues.get(attribute.getName()));
+      if (value != null) { // null values are particularly possible for computed defaults.
+        type.visitLabels(visitor, value, attribute);
+      }
+      return; // If both the aspect and rule have this attribute, the aspect instance overrides.
+    }
+    if (ruleAttributes.has(attribute.getName(), type)) {
+      ruleAttributes.visitLabels(attribute, type, visitor);
+    }
   }
 
   @Override
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 816e8a0..4f0b687 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
@@ -42,7 +42,6 @@
 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;
@@ -59,6 +58,7 @@
 import com.google.devtools.build.lib.packages.RuleTransitionData;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.LabelVisitor;
 import com.google.devtools.build.lib.util.OrderedSetMultimap;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -591,11 +591,11 @@
       Iterable<AttributeDependencyKind> attributeDependencyKinds,
       OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
       Rule rule,
-      ConfiguredAttributeMapper attributeMap,
+      ConfiguredAttributeMapper ruleAttributes,
       BuildConfigurationValue ruleConfig) {
     for (AttributeDependencyKind dependencyKind : attributeDependencyKinds) {
       Attribute attribute = dependencyKind.getAttribute();
-      if (!attribute.getCondition().apply(attributeMap)
+      if (!attribute.getCondition().apply(ruleAttributes)
           // 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
@@ -604,70 +604,36 @@
           // part of an unchosen select() branch.
           || attribute.getName().equals(RuleClass.CONFIG_SETTING_DEPS_ATTRIBUTE)) {
         continue;
+      } else if (attribute.isLateBound()) {
+        // Latebound attributes: compute their values here instead of from the rule / aspect attrs.
+        addLateBoundDefault(
+            resolveLateBoundDefault(rule, ruleAttributes, attribute, ruleConfig),
+            attribute.getType(),
+            (depLabel, ctx) -> outgoingLabels.put(dependencyKind, depLabel));
+      } else {
+        // Everything else: read the rule / aspect attribute settings.
+        AspectAwareAttributeMapper combinedAttributes =
+            new AspectAwareAttributeMapper(
+                rule,
+                ruleAttributes,
+                dependencyKind.getOwningAspect() != null
+                    ? ImmutableMap.of(attribute.getName(), attribute)
+                    : ImmutableMap.of());
+        combinedAttributes.visitLabels(
+            attribute, (depLabel) -> outgoingLabels.put(dependencyKind, depLabel));
       }
-
-      Type<?> type = attribute.getType();
-      if (type == BuildType.OUTPUT
-          || type == BuildType.OUTPUT_LIST
-          || type == BuildType.NODEP_LABEL
-          || type == BuildType.NODEP_LABEL_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 <T> void resolveAttribute(
-      Attribute attribute,
-      Type<T> type,
-      AttributeDependencyKind dependencyKind,
-      OrderedSetMultimap<DependencyKind, Label> outgoingLabels,
-      Rule rule,
-      ConfiguredAttributeMapper attributeMap,
-      BuildConfigurationValue ruleConfig) {
-    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) defaultValue).getDefault(attributeMap)
-                    : defaultValue);
-      }
-    } else if (attribute.isLateBound()) {
-      attributeValue =
-          type.cast(resolveLateBoundDefault(rule, attributeMap, attribute, ruleConfig));
-    } else if (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);
+  private <T> void addLateBoundDefault(
+      @Nullable Object value, Type<T> type, LabelVisitor labelVisitor) {
+    if (value != null) {
+      type.visitLabels(labelVisitor, type.cast(value), /*context=*/ null);
     }
-
-    if (attributeValue == null) {
-      return;
-    }
-
-    type.visitLabels(
-        (depLabel, ctx) -> outgoingLabels.put(dependencyKind, depLabel),
-        attributeValue,
-        /*context=*/ null);
   }
 
   @VisibleForTesting(/* used to test LateBoundDefaults' default values */ )
-  public static <FragmentT> Object resolveLateBoundDefault(
+  static <FragmentT> Object resolveLateBoundDefault(
       Rule rule,
       AttributeMap attributeMap,
       Attribute attribute,
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index 3788fea..d8414f7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -209,7 +209,9 @@
     this.ruleClassProvider = builder.ruleClassProvider;
     this.targetMap = targetMap;
     this.configConditions = builder.configConditions.asProviders();
-    this.attributes = new AspectAwareAttributeMapper(attributes, builder.aspectAttributes);
+    this.attributes =
+        new AspectAwareAttributeMapper(
+            rule, (ConfiguredAttributeMapper) attributes, builder.aspectAttributes);
     Set<String> allEnabledFeatures = new HashSet<>();
     Set<String> allDisabledFeatures = new HashSet<>();
     getAllFeatures(allEnabledFeatures, allDisabledFeatures);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
index 4c34b48..7243807 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/AbstractAttributeMapper.java
@@ -32,7 +32,7 @@
 public abstract class AbstractAttributeMapper implements AttributeMap {
   private final RuleClass ruleClass;
   private final Label ruleLabel;
-  final Rule rule;
+  protected final Rule rule;
 
   protected AbstractAttributeMapper(Rule rule) {
     this.ruleClass = rule.getRuleClassObject();
@@ -181,11 +181,11 @@
   }
 
   @Override
-  public final void visitLabels(DependencyFilter filter, BiConsumer<Attribute, Label> consumer) {
+  public void visitLabels(DependencyFilter filter, BiConsumer<Attribute, Label> consumer) {
     visitLabels(ruleClass.getAttributes(), filter, consumer);
   }
 
-  private void visitLabels(
+  protected void visitLabels(
       List<Attribute> attributes, DependencyFilter filter, BiConsumer<Attribute, Label> consumer) {
     Type.LabelVisitor visitor =
         (label, attribute) -> {
@@ -195,8 +195,12 @@
         };
     for (Attribute attribute : attributes) {
       Type<?> type = attribute.getType();
-      // TODO(bazel-team): clean up the typing / visitation interface so we don't have to
-      // special-case these types.
+      // 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.
+      //
+      // TODO(bazel-team): clarify which variations of visitLabels include these types (for query)
+      //   and which don't (for dependency analysis)
       if (type != BuildType.OUTPUT
           && type != BuildType.OUTPUT_LIST
           && type != BuildType.NODEP_LABEL
@@ -208,7 +212,7 @@
   }
 
   /** Visits all labels reachable from the given attribute. */
-  <T> void visitLabels(Attribute attribute, Type<T> type, Type.LabelVisitor visitor) {
+  public <T> void visitLabels(Attribute attribute, Type<T> type, Type.LabelVisitor visitor) {
     T value = get(attribute.getName(), type);
     if (value != null) { // null values are particularly possible for computed defaults.
       type.visitLabels(visitor, value, attribute);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java b/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java
index 5c8fa74..49d2156 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/AggregatingAttributeMapper.java
@@ -70,7 +70,7 @@
    * {@link #visitAttribute}'s documentation. So we want to avoid that code path when possible.
    */
   @Override
-  <T> void visitLabels(Attribute attribute, Type<T> type, Type.LabelVisitor visitor) {
+  public <T> void visitLabels(Attribute attribute, Type<T> type, Type.LabelVisitor visitor) {
     visitLabels(attribute, type, /*includeSelectKeys=*/ true, visitor);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
index 7b48285..2aa948e 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
@@ -64,7 +64,7 @@
 import net.starlark.java.eval.Structure;
 
 /**
- * Metadata of a rule attribute. Contains the attribute name and type, and an default value to be
+ * Metadata of a rule attribute. Contains the attribute name and type, and a default value to be
  * used if none is provided in a rule declaration in a BUILD file. Attributes are immutable, and may
  * be shared by more than one rule (for example, <code>foo_binary</code> and <code>foo_library
  * </code> may share many attributes in common).
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapperTest.java b/src/test/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapperTest.java
index be08836..dbb8341 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapperTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/AspectAwareAttributeMapperTest.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
 import com.google.devtools.build.lib.util.FileTypeSet;
+import java.util.ArrayList;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,12 +54,15 @@
 
     RuleConfiguredTarget ct = (RuleConfiguredTarget) ctad.getConfiguredTarget();
     rule = (Rule) ctad.getTarget();
-    Attribute aspectAttr = new Attribute.Builder<Label>("fromaspect", BuildType.LABEL)
-        .allowedFileTypes(FileTypeSet.ANY_FILE)
-        .build();
+    Attribute aspectAttr =
+        new Attribute.Builder<Label>("$fromaspect", BuildType.LABEL)
+            .allowedFileTypes(FileTypeSet.ANY_FILE)
+            .defaultValue(Label.parseAbsoluteUnchecked("//:aspect_default_value"))
+            .build();
     aspectAttributes = ImmutableMap.<String, Attribute>of(aspectAttr.getName(), aspectAttr);
     mapper =
         new AspectAwareAttributeMapper(
+            rule,
             ConfiguredAttributeMapper.of(
                 rule,
                 ct.getConfigConditions(),
@@ -85,18 +89,18 @@
 
   @Test
   public void getAspectAttributeValue() throws Exception {
-    assertThrows(
-        UnsupportedOperationException.class, () -> mapper.get("fromaspect", BuildType.LABEL));
+    assertThat(mapper.get("$fromaspect", BuildType.LABEL))
+        .isEqualTo(Label.parseAbsoluteUnchecked("//:aspect_default_value"));
   }
 
   @Test
   public void getAspectValueWrongType() throws Exception {
     IllegalArgumentException e =
         assertThrows(
-            IllegalArgumentException.class, () -> mapper.get("fromaspect", BuildType.LABEL_LIST));
+            IllegalArgumentException.class, () -> mapper.get("$fromaspect", BuildType.LABEL_LIST));
     assertThat(e)
         .hasMessageThat()
-        .isEqualTo("attribute fromaspect has type label, not expected type list(label)");
+        .isEqualTo("attribute $fromaspect has type label, not expected type list(label)");
   }
 
   @Test
@@ -111,31 +115,43 @@
   @Test
   public void isConfigurable() throws Exception {
     assertThat(mapper.isConfigurable("linkstatic")).isTrue();
-    assertThat(mapper.isConfigurable("fromaspect")).isFalse();
+    assertThat(mapper.isConfigurable("$fromaspect")).isFalse();
   }
 
   @Test
   public void getAttributeNames() throws Exception {
-    assertThat(mapper.getAttributeNames()).containsAtLeast("srcs", "linkstatic", "fromaspect");
+    assertThat(mapper.getAttributeNames()).containsAtLeast("srcs", "linkstatic", "$fromaspect");
   }
 
   @Test
   public void getAttributeType() throws Exception {
     assertThat(mapper.getAttributeType("srcs")).isEqualTo(BuildType.LABEL_LIST);
-    assertThat(mapper.getAttributeType("fromaspect")).isEqualTo(BuildType.LABEL);
+    assertThat(mapper.getAttributeType("$fromaspect")).isEqualTo(BuildType.LABEL);
   }
 
   @Test
   public void getAttributeDefinition() throws Exception {
     assertThat(mapper.getAttributeDefinition("srcs").getName()).isEqualTo("srcs");
-    assertThat(mapper.getAttributeDefinition("fromaspect").getName()).isEqualTo("fromaspect");
-
+    assertThat(mapper.getAttributeDefinition("$fromaspect").getName()).isEqualTo("$fromaspect");
   }
 
   @Test
   public void has() throws Exception {
     assertThat(mapper.has("srcs")).isTrue();
-    assertThat(mapper.has("fromaspect")).isTrue();
+    assertThat(mapper.has("$fromaspect")).isTrue();
+    assertThat(mapper.has("noexist")).isFalse();
+  }
+
+  @Test
+  public void visitLabels() throws Exception {
+    ArrayList<Label> actual = new ArrayList<>();
+    mapper.visitAllLabels((attr, label) -> actual.add(label));
+    // From the aspect:
+    assertThat(actual).contains(Label.parseAbsoluteUnchecked("//:aspect_default_value"));
+    // From the rule:
+    assertThat(actual).contains(Label.parseAbsoluteUnchecked("//foo:a.cc"));
+    // The rule also has implicit toolchain targets we don't care about here.
+    // TODO(bazel-team): use a Starlark rule instead of a cc_binary to eliminate that variation.
   }
 }