Improved error reporting in RuleContext:

- Unified duplicate code from RuleContext and RuleContext.Builder in a new class, RuleContext.ErrorReporter
- Added the BUILD file location to error/warning messages if the offending rule was created by a macro

--
MOS_MIGRATED_REVID=103934375
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 10395cb..b280086 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
@@ -137,6 +137,7 @@
   private final ImmutableSet<String> features;
   private final Map<String, Attribute> attributeMap;
   private final BuildConfiguration hostConfiguration;
+  private final ErrorReporter reporter;
 
   private ActionOwner actionOwner;
 
@@ -157,6 +158,7 @@
     this.features = getEnabledFeatures();
     this.attributeMap = attributeMap;
     this.hostConfiguration = builder.hostConfiguration;
+    reporter = builder.reporter;
   }
 
   private ImmutableSet<String> getEnabledFeatures() {
@@ -391,7 +393,7 @@
    */
   @Override
   public void ruleError(String message) {
-    reportError(rule.getLocation(), prefixRuleMessage(message));
+    reporter.ruleError(message);
   }
 
   /**
@@ -400,7 +402,7 @@
    */
   @Override
   public void ruleWarning(String message) {
-    reportWarning(rule.getLocation(), prefixRuleMessage(message));
+    reporter.ruleWarning(message);
   }
 
   /**
@@ -412,11 +414,7 @@
    */
   @Override
   public void attributeError(String attrName, String message) {
-    reportError(rule.getAttributeLocation(attrName),
-        prefixAttributeMessage(Attribute.isImplicit(attrName)
-                ? "(an implicit dependency)"
-                : attrName,
-            message));
+    reporter.attributeError(attrName, message);
   }
 
   /**
@@ -427,30 +425,7 @@
    */
   @Override
   public void attributeWarning(String attrName, String message) {
-    reportWarning(rule.getAttributeLocation(attrName),
-        prefixAttributeMessage(Attribute.isImplicit(attrName)
-                ? "(an implicit dependency)"
-                : attrName,
-            message));
-  }
-
-  private String prefixAttributeMessage(String attrName, String message) {
-    return "in " + attrName + " attribute of "
-           + rule.getRuleClass() + " rule "
-           + getLabel() + ": " + message;
-  }
-
-  private String prefixRuleMessage(String message) {
-    return "in " + rule.getRuleClass() + " rule "
-           + getLabel() + ": " + message;
-  }
-
-  private void reportError(Location location, String message) {
-    getAnalysisEnvironment().getEventHandler().handle(Event.error(location, message));
-  }
-
-  private void reportWarning(Location location, String message) {
-    getAnalysisEnvironment().getEventHandler().handle(Event.warn(location, message));
+    reporter.attributeWarning(attrName, message);
   }
 
   /**
@@ -1237,15 +1212,21 @@
     return features;
   }
 
+  @Override
+  public String toString() {
+    return "RuleContext(" + getLabel() + ", " + getConfiguration() + ")";
+  }
+
   /**
    * Builder class for a RuleContext.
    */
-  public static final class Builder {
+  public static final class Builder implements RuleErrorConsumer  {
     private final AnalysisEnvironment env;
     private final Rule rule;
     private final BuildConfiguration configuration;
     private final BuildConfiguration hostConfiguration;
     private final PrerequisiteValidator prerequisiteValidator;
+    private final ErrorReporter reporter;
     private ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap;
     private Set<ConfigMatchingProvider> configConditions;
     private NestedSet<PackageSpecification> visibility;
@@ -1258,6 +1239,7 @@
       this.configuration = Preconditions.checkNotNull(configuration);
       this.hostConfiguration = Preconditions.checkNotNull(hostConfiguration);
       this.prerequisiteValidator = prerequisiteValidator;
+      reporter = new ErrorReporter(env, rule);
     }
 
     RuleContext build() {
@@ -1405,41 +1387,32 @@
       return mapBuilder.build();
     }
 
-    private String prefixRuleMessage(String message) {
-      return String.format("in %s rule %s: %s", rule.getRuleClass(), rule.getLabel(), message);
-    }
-
-    private String maskInternalAttributeNames(String name) {
-      return Attribute.isImplicit(name) ? "(an implicit dependency)" : name;
-    }
-
-    private String prefixAttributeMessage(String attrName, String message) {
-      return String.format("in %s attribute of %s rule %s: %s",
-          maskInternalAttributeNames(attrName), rule.getRuleClass(), rule.getLabel(), message);
-    }
-
     public void reportError(Location location, String message) {
-      env.getEventHandler().handle(Event.error(location, message));
+      reporter.reportError(location, message);
     }
 
+    @Override
     public void ruleError(String message) {
-      reportError(rule.getLocation(), prefixRuleMessage(message));
+      reporter.ruleError(message);
     }
 
+    @Override
     public void attributeError(String attrName, String message) {
-      reportError(rule.getAttributeLocation(attrName), prefixAttributeMessage(attrName, message));
+      reporter.attributeError(attrName, message);
     }
 
     public void reportWarning(Location location, String message) {
-      env.getEventHandler().handle(Event.warn(location, message));
+      reporter.reportWarning(location, message);
     }
 
+    @Override
     public void ruleWarning(String message) {
-      env.getEventHandler().handle(Event.warn(rule.getLocation(), prefixRuleMessage(message)));
+      reporter.ruleWarning(message);
     }
 
+    @Override
     public void attributeWarning(String attrName, String message) {
-      reportWarning(rule.getAttributeLocation(attrName), prefixAttributeMessage(attrName, message));
+      reporter.attributeWarning(attrName, message);
     }
 
     private void reportBadPrerequisite(Attribute attribute, String targetKind,
@@ -1584,8 +1557,92 @@
     }
   }
 
-  @Override
-  public String toString() {
-    return "RuleContext(" + getLabel() + ", " + getConfiguration() + ")";
+  /**
+   * Helper class for reporting errors and warnings.
+   */
+  public static final class ErrorReporter implements RuleErrorConsumer {
+    private final AnalysisEnvironment env;
+    private final Rule rule;
+
+    ErrorReporter(AnalysisEnvironment env, Rule rule) {
+      this.env = env;
+      this.rule = rule;
+    }
+
+    public void reportError(Location location, String message) {
+      env.getEventHandler().handle(Event.error(location, message));
+    }
+
+    @Override
+    public void ruleError(String message) {
+      reportError(rule.getLocation(), prefixRuleMessage(message));
+    }
+
+    @Override
+    public void attributeError(String attrName, String message) {
+      reportError(getAttributeLocation(attrName), completeAttributeMessage(attrName, message));
+    }
+
+    public void reportWarning(Location location, String message) {
+      env.getEventHandler().handle(Event.warn(location, message));
+    }
+
+    @Override
+    public void ruleWarning(String message) {
+      env.getEventHandler().handle(Event.warn(rule.getLocation(), prefixRuleMessage(message)));
+    }
+
+    @Override
+    public void attributeWarning(String attrName, String message) {
+      reportWarning(getAttributeLocation(attrName), completeAttributeMessage(attrName, message));
+    }
+
+    private String prefixRuleMessage(String message) {
+      return String.format("in %s rule %s: %s", rule.getRuleClass(), rule.getLabel(), message);
+    }
+
+    private String maskInternalAttributeNames(String name) {
+      return Attribute.isImplicit(name) ? "(an implicit dependency)" : name;
+    }
+
+    /**
+     * Prefixes the given message with details about the rule and appends details about the macro
+     * that created this rule, if applicable.
+     */
+    private String completeAttributeMessage(String attrName, String message) {
+      // Appends a note to the given message if the offending rule was created by a macro.
+      String macroMessageAppendix =
+          wasCreatedByMacro()
+              ? String.format(
+                  ". Since this rule was created by the macro '%s', the error might have been "
+                  + "caused by the macro implementation in %s",
+                  getGeneratorFunction(), rule.getAttributeLocation(attrName))
+              : "";
+
+      return String.format("in %s attribute of %s rule %s: %s%s",
+          maskInternalAttributeNames(attrName), rule.getRuleClass(), rule.getLabel(), message,
+          macroMessageAppendix);
+    }
+
+    /**
+     * Returns the location of the specified attribute.
+     *
+     * <p>If the rule was created by a macro, we return the location from the BUILD file instead.
+     */
+    private Location getAttributeLocation(String attrName) {
+      // TODO(bazel-team): We assume that macros have set the rule location to the generator
+      // location. It would be better to read the "generator_location" attribute (which is currently
+      // not implemented).
+      return wasCreatedByMacro() ? rule.getLocation() : rule.getAttributeLocation(attrName);
+    }
+
+    private boolean wasCreatedByMacro() {
+      String generator = getGeneratorFunction();
+      return generator != null && !generator.isEmpty();
+    }
+
+    private String getGeneratorFunction() {
+      return (String) rule.getAttributeContainer().getAttr("generator_function");
+    }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index b7b778a..42e12bd 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -71,6 +71,7 @@
         "exports_files(['JavaBuilder_deploy.jar','SingleJar_deploy.jar',",
         "               'JavaBuilderCanary_deploy.jar', 'ijar', 'GenClass_deploy.jar'])");
     config.create("tools/cpp/BUILD",
+        "cc_library(name = 'stl')",
         "filegroup(name = 'toolchain', ",
         "    srcs = [':cc-compiler-local', ':cc-compiler-darwin', ':cc-compiler-piii',",
         "            ':cc-compiler-armeabi-v7a', ':empty'],",
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
index 9259ad7..b12dd9f 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
@@ -72,6 +72,99 @@
         ")");
   }
 
+  private void setUpAttributeErrorTest() throws Exception   {
+    scratch.file("test/BUILD",
+        "load('/test/macros', 'macro_native_rule', 'macro_skylark_rule', 'skylark_rule')",
+        "macro_native_rule(name = 'm_native',",
+        "  deps = [':jlib'])",
+        "macro_skylark_rule(name = 'm_skylark',",
+        "  deps = [':jlib'])",
+        "java_library(name = 'jlib',",
+        "  srcs = ['bla.java'])",
+        "cc_library(name = 'cclib',",
+        "  deps = [':jlib'])",
+        "skylark_rule(name = 'skyrule',",
+        "  deps = [':jlib'])");
+    scratch.file("test/macros.bzl",
+        "def _impl(ctx):",
+        "  return",
+        "skylark_rule = rule(",
+        "  implementation = _impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(providers = ['some_provider'], allow_files=True)",
+        "  }",
+        ")",
+        "def macro_native_rule(name, deps): ",
+        "  native.cc_library(name = name, deps = deps)",
+        "def macro_skylark_rule(name, deps):",
+        "  skylark_rule(name = name, deps = deps)");
+    reporter.removeHandler(failFastHandler);
+  }
+
+  public void testNativeRuleAttributeErrorWithMacro() throws Exception {
+    setUpAttributeErrorTest();
+    try {
+      createRuleContext("//test:m_native");
+      fail("Should have failed because of invalid dependency");
+    } catch (Exception ex) {
+      // Macro creates native rule -> location points to the rule and the message contains details
+      // about the macro.
+      assertContainsEvent(
+          "ERROR /workspace/test/BUILD:2:1: in deps attribute of cc_library rule //test:m_native: "
+          + "java_library rule '//test:jlib' is misplaced here (expected ");
+      // Skip the part of the error message that has details about the allowed deps since the mocks
+      // for the mac tests might have different values for them.
+      assertContainsEvent(". Since this "
+          + "rule was created by the macro 'macro_native_rule', the error might have been caused "
+          + "by the macro implementation in /workspace/test/macros.bzl:10:41");
+    }
+  }
+  public void testSkylarkRuleAttributeErrorWithMacro() throws Exception {
+    setUpAttributeErrorTest();
+    try {
+      createRuleContext("//test:m_skylark");
+      fail("Should have failed because of invalid attribute value");
+    } catch (Exception ex) {
+      // Macro creates Skylark rule -> location points to the rule and the message contains details
+      // about the macro.
+      assertContainsEvent(
+          "ERROR /workspace/test/BUILD:4:1: in deps attribute of skylark_rule rule "
+          + "//test:m_skylark: '//test:jlib' does not have mandatory provider 'some_provider'. "
+          + "Since this rule was created by the macro 'macro_skylark_rule', the error might have "
+          + "been caused by the macro implementation in /workspace/test/macros.bzl:12:36");
+    }
+  }
+  public void testNativeRuleAttributeErrorWithoutMacro() throws Exception {
+    setUpAttributeErrorTest();
+    try {
+      createRuleContext("//test:cclib");
+      fail("Should have failed because of invalid dependency");
+    } catch (Exception ex) {
+      // Native rule WITHOUT macro -> location points to the attribute and there is no mention of
+      // 'macro' at all.
+      assertContainsEvent("ERROR /workspace/test/BUILD:9:10: in deps attribute of "
+          + "cc_library rule //test:cclib: java_library rule '//test:jlib' is misplaced here "
+          + "(expected ");
+      // Skip the part of the error message that has details about the allowed deps since the mocks
+      // for the mac tests might have different values for them.
+      assertDoesNotContainEvent("Since this rule was created by the macro");
+    }
+  }
+
+  public void testSkylarkRuleAttributeErrorWithoutMacro() throws Exception {
+    setUpAttributeErrorTest();
+    try {
+      createRuleContext("//test:skyrule");
+      fail("Should have failed because of invalid dependency");
+    } catch (Exception ex) {
+      // Skylark rule WITHOUT macro -> location points to the attribute and there is no mention of
+      // 'macro' at all.
+      assertContainsEvent("ERROR /workspace/test/BUILD:11:10: in deps attribute of "
+          + "skylark_rule rule //test:skyrule: '//test:jlib' does not have mandatory provider "
+          + "'some_provider'");
+    }
+  }
+
   public void testGetPrerequisiteArtifacts() throws Exception {
     SkylarkRuleContext ruleContext = createRuleContext("//foo:foo");
     Object result = evalRuleContextCode(ruleContext, "ruleContext.files.srcs");
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java b/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
index c2c01d2..a864694 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
+++ b/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
@@ -359,7 +359,9 @@
                                           String expectedEvent,
                                           Set<EventKind> kinds) {
     for (Event event : eventCollector) {
-      if (event.getMessage().contains(expectedEvent) && kinds.contains(event.getKind())) {
+      // We want to be able to check for the location and the message type (error / warning).
+      // Consequently, we use toString() instead of getMessage().
+      if (event.toString().contains(expectedEvent) && kinds.contains(event.getKind())) {
         return event;
       }
     }