diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java index 153ef175f342c5..6d0001f3d3db95 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/TemplateDict.java @@ -29,6 +29,7 @@ import javax.annotation.Nullable; import net.starlark.java.eval.EvalException; import net.starlark.java.eval.Mutability; +import net.starlark.java.eval.Sequence; import net.starlark.java.eval.Starlark; import net.starlark.java.eval.StarlarkCallable; import net.starlark.java.eval.StarlarkFunction; @@ -134,11 +135,22 @@ public String getValue() throws EvalException { /*kwargs=*/ ImmutableMap.of()); if (ret instanceof String) { parts.add((String) ret); + } else if (ret instanceof Sequence) { + for (Object v : ((Sequence) ret)) { + if (!(v instanceof String)) { + throw Starlark.errorf( + "Function provided to map_each must return string, None, or list of strings," + + " but returned list containing element '%s' of type %s for key '%s' and" + + " value: %s", + v, Starlark.type(v), getKey(), val); + } + parts.add((String) v); + } } else if (ret != Starlark.NONE) { throw Starlark.errorf( - "Function provided to map_each must return a String or None, but returned type " - + "%s for key '%s' and value: %s", - Starlark.type(ret), getKey(), Starlark.repr(val)); + "Function provided to map_each must return string, None, or list of strings, but " + + "returned type %s for key '%s' and value: %s", + Starlark.type(ret), getKey(), val); } } catch (InterruptedException e) { // Report the error to the user, but the stack trace is not of use to them diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java index e129f5e5fb740c..e7315c4016516c 100644 --- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java +++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/TemplateDictApi.java @@ -71,9 +71,9 @@ public interface TemplateDictApi extends StarlarkValue { named = true, positional = false, doc = - "A Starlark function accepting a single argument and returning either a String or " - + "None. This function is applied to each item of the depset " - + "specified in the values parameter"), + "A Starlark function accepting a single argument and returning either a string, " + + "None, or a list of strings. This function is applied to each " + + "item of the depset specified in the values parameter"), @Param( name = "uniquify", named = true, diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java index c1b785cc055e12..c451fe1c16cc84 100644 --- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java +++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleContextTest.java @@ -3741,6 +3741,68 @@ public void testTemplateExpansionComputedSubstitutionWithUniquify() throws Excep assertThat(substitutionsUnchecked).isEqualTo(ImmutableMap.of("exts", "txt%%log%%exe%%sh")); } + @Test + public void testTemplateExpansionComputedSubstitutionWithUniquifyAndListMapEach() + throws Exception { + setBuildLanguageOptions("--experimental_lazy_template_expansion"); + scratch.file( + "test/rules.bzl", + "def _artifact_to_extension(file):", + " if file.extension == 'sh':", + " return [file.extension]", + " return [file.extension, '.' + file.extension]", + "", + "def _undertest_impl(ctx):", + " template_dict = ctx.actions.template_dict()", + " template_dict.add_joined('exts', depset(ctx.files.srcs),", + " map_each = _artifact_to_extension,", + " uniquify = True,", + " join_with = '%%',", + " )", + " ctx.actions.expand_template(output=ctx.outputs.out,", + " template=ctx.file.template,", + " computed_substitutions=template_dict,", + " )", + "undertest_rule = rule(", + " implementation = _undertest_impl,", + " outputs = {'out': '%{name}.txt'},", + " attrs = {'template': attr.label(allow_single_file=True),", + " 'srcs':attr.label_list(allow_files=True)", + " },", + " _skylark_testable = True,", + ")", + testingRuleDefinition); + scratch.file("test/template.txt", "exts", "exts"); + scratch.file( + "test/BUILD", + "load(':rules.bzl', 'undertest_rule', 'testing_rule')", + "undertest_rule(", + " name = 'undertest',", + " template = ':template.txt',", + " srcs = ['foo.txt', 'bar.log', 'baz.txt', 'bak.exe', 'far.sh', 'boo.sh'],", + ")", + "testing_rule(", + " name = 'testing',", + " dep = ':undertest',", + ")"); + StarlarkRuleContext ruleContext = createRuleContext("//test:testing"); + setRuleContext(ruleContext); + ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]")); + ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]")); + + assertThat(ev.eval("type(action)")).isEqualTo("Action"); + + Object contentUnchecked = ev.eval("action.content"); + assertThat(contentUnchecked).isInstanceOf(String.class); + assertThat(contentUnchecked) + .isEqualTo("txt%%.txt%%log%%.log%%exe%%.exe%%sh\ntxt%%.txt%%log%%.log%%exe%%.exe%%sh\n"); + + Object substitutionsUnchecked = ev.eval("action.substitutions"); + assertThat(substitutionsUnchecked).isInstanceOf(Dict.class); + assertThat(substitutionsUnchecked) + .isEqualTo(ImmutableMap.of("exts", "txt%%.txt%%log%%.log%%exe%%.exe%%sh")); + } + @Test public void testTemplateExpansionComputedSubstitutionDuplicateKeys() throws Exception { setBuildLanguageOptions("--experimental_lazy_template_expansion"); @@ -3913,8 +3975,60 @@ public void testTemplateExpansionComputedSubstitutionMapEachBadReturnType() thro assertThat(evalException) .hasMessageThat() .isEqualTo( - "Function provided to map_each must return a String or None, but returned " - + "type Label for key '%files%' and value: "); + "Function provided to map_each must return string, None, or list of strings, " + + "but returned type Label for key '%files%' and value: " + + "File:[/workspace[source]]test/template.txt"); + } + + @Test + public void testTemplateExpansionComputedSubstitutionMapEachBadListReturnType() throws Exception { + setBuildLanguageOptions("--experimental_lazy_template_expansion"); + scratch.file( + "test/rules.bzl", + "def file_to_owner_label(file):", + " return [file.owner]", + "", + "def _undertest_impl(ctx):", + " template_dict = ctx.actions.template_dict()", + " template_dict.add_joined('%files%', depset(ctx.files.template),", + " map_each = file_to_owner_label,", + " join_with = '')", + " ctx.actions.expand_template(output=ctx.outputs.out,", + " template=ctx.file.template,", + " computed_substitutions=template_dict,", + " )", + "undertest_rule = rule(", + " implementation = _undertest_impl,", + " outputs = {'out': '%{name}.txt'},", + " attrs = {'template': attr.label(allow_single_file=True),},", + " _skylark_testable = True,", + ")", + testingRuleDefinition); + scratch.file("test/template.txt"); + scratch.file( + "test/BUILD", + "load(':rules.bzl', 'undertest_rule', 'testing_rule')", + "undertest_rule(", + " name = 'undertest',", + " template = ':template.txt',", + ")", + "testing_rule(", + " name = 'testing',", + " dep = ':undertest',", + ")"); + StarlarkRuleContext ruleContext = createRuleContext("//test:testing"); + setRuleContext(ruleContext); + ev.update("file", ev.eval("ruleContext.attr.dep.files.to_list()[0]")); + ev.update("action", ev.eval("ruleContext.attr.dep[Actions].by_file[file]")); + + EvalException evalException = + assertThrows(EvalException.class, () -> ev.eval("action.content")); + assertThat(evalException) + .hasMessageThat() + .isEqualTo( + "Function provided to map_each must return string, None, or list of strings, " + + "but returned list containing element '//test:template.txt' of type Label for " + + "key '%files%' and value: File:[/workspace[source]]test/template.txt"); } @Test