diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModule.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModule.java index ff374a5730ce08..27b6cf88f4f52c 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModule.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/StarlarkBazelModule.java @@ -51,7 +51,12 @@ public class StarlarkBazelModule implements StarlarkValue { "Contains the tags in a module for the module extension currently being processed. This" + " object has a field for each tag class of the extension, and the value of the" + " field is a list containing an object for each tag instance. This \"tag instance\"" - + " object in turn has a field for each attribute of the tag class.") + + " object in turn has a field for each attribute of the tag class.\n\n" + + "When passed as positional arguments to print() or fail()" + + ", tag instance objects turn into a meaningful string representation of the" + + " form \"'install' tag at /home/user/workspace/MODULE.bazel:3:4\". This can be used" + + " to construct error messages that point to the location of the tag in the module" + + " file, e.g. fail(\"Conflict between\", tag1, \"and\", tag2).") static class Tags implements Structure { private final ImmutableMap> typeCheckedTags; diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java index 443321b711604e..0ad0ab203acf2b 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/TypeCheckedTag.java @@ -23,8 +23,11 @@ import javax.annotation.Nullable; import net.starlark.java.annot.StarlarkBuiltin; import net.starlark.java.eval.EvalException; +import net.starlark.java.eval.Printer; +import net.starlark.java.eval.StarlarkSemantics; import net.starlark.java.eval.Structure; import net.starlark.java.spelling.SpellChecker; +import net.starlark.java.syntax.Location; /** * A {@link Tag} whose attribute values have been type-checked against the attribute schema define @@ -36,10 +39,21 @@ public class TypeCheckedTag implements Structure { private final Object[] attrValues; private final boolean devDependency; - private TypeCheckedTag(TagClass tagClass, Object[] attrValues, boolean devDependency) { + // The properties below are only used for error reporting. + private final Location location; + private final String tagClassName; + + private TypeCheckedTag( + TagClass tagClass, + Object[] attrValues, + boolean devDependency, + Location location, + String tagClassName) { this.tagClass = tagClass; this.attrValues = attrValues; this.devDependency = devDependency; + this.location = location; + this.tagClassName = tagClassName; } /** Creates a {@link TypeCheckedTag}. */ @@ -97,7 +111,8 @@ public static TypeCheckedTag create(TagClass tagClass, Tag tag, LabelConverter l attrValues[i] = Attribute.valueToStarlark(attr.getDefaultValueUnchecked()); } } - return new TypeCheckedTag(tagClass, attrValues, tag.isDevDependency()); + return new TypeCheckedTag( + tagClass, attrValues, tag.isDevDependency(), tag.getLocation(), tag.getTagName()); } /** @@ -133,4 +148,9 @@ public ImmutableCollection getFieldNames() { public String getErrorMessageForUnknownField(String field) { return "unknown attribute " + field; } + + @Override + public void debugPrint(Printer printer, StarlarkSemantics semantics) { + printer.append(String.format("'%s' tag at %s", tagClassName, location)); + } } diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java index e1e10dfbd92069..9d9faa84b4e904 100644 --- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java +++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleExtensionResolutionTest.java @@ -2348,4 +2348,40 @@ public void isDevDependency_usages() throws Exception { assertThat(result.get(skyKey).getModule().getGlobal("ext2_data")).isEqualTo("ext2: False"); assertThat(result.get(skyKey).getModule().getGlobal("ext3_data")).isEqualTo("ext3: True"); } + + @Test + public void printAndFailOnTag() throws Exception { + scratch.file( + workspaceRoot.getRelative("MODULE.bazel").getPathString(), + "ext = use_extension('//:defs.bzl', 'ext')", + "ext.foo()", + "ext.foo()"); + scratch.file( + workspaceRoot.getRelative("defs.bzl").getPathString(), + "repo = repository_rule(lambda ctx: True)", + "def _ext_impl(ctx):", + " tag1 = ctx.modules[0].tags.foo[0]", + " tag2 = ctx.modules[0].tags.foo[1]", + " print('Conflict between', tag1, 'and', tag2)", + " fail('Fatal conflict between', tag1, 'and', tag2)", + "foo = tag_class()", + "ext = module_extension(implementation=_ext_impl,tag_classes={'foo':foo})"); + scratch.file(workspaceRoot.getRelative("BUILD").getPathString()); + + ModuleExtensionId extensionId = + ModuleExtensionId.create(Label.parseCanonical("//:defs.bzl"), "ext", Optional.empty()); + reporter.removeHandler(failFastHandler); + var result = + evaluator.evaluate( + ImmutableList.of(SingleExtensionEvalValue.key(extensionId)), evaluationContext); + + assertThat(result.hasError()).isTrue(); + assertContainsEvent( + "Fatal conflict between 'foo' tag at /ws/MODULE.bazel:2:8 and 'foo' tag at " + + "/ws/MODULE.bazel:3:8", + ImmutableSet.of(EventKind.ERROR)); + assertContainsEvent( + "Conflict between 'foo' tag at /ws/MODULE.bazel:2:8 and 'foo' tag at /ws/MODULE.bazel:3:8", + ImmutableSet.of(EventKind.DEBUG)); + } }