blob: 9f2b0f5dfe1e8ec27a7f8e94fa099ad4486c7f0c [file] [log] [blame]
// Copyright 2024 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.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.analysis.test.AnalysisFailure;
import com.google.devtools.build.lib.analysis.test.AnalysisFailureInfo;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.MacroInstance;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.Package;
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.packages.Types;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import com.google.testing.junit.testparameterinjector.TestParameters;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.syntax.Location;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests the execution of symbolic macro implementations. */
@RunWith(TestParameterInjector.class)
public final class SymbolicMacroTest extends BuildViewTestCase {
/**
* Returns a package by the given name (no leading "//"), or null upon {@link
* NoSuchPackageException}.
*/
@CanIgnoreReturnValue
@Nullable
private Package getPackage(String pkgName) throws InterruptedException {
try {
return getPackageManager().getPackage(reporter, PackageIdentifier.createInMainRepo(pkgName));
} catch (NoSuchPackageException unused) {
return null;
}
}
private void assertPackageNotInError(@Nullable Package pkg) {
assertThat(pkg).isNotNull();
assertThat(pkg.containsErrors()).isFalse();
}
/** Retrieves the macro with the given id, which must exist in the package. */
private static MacroInstance getMacroById(Package pkg, String id) {
MacroInstance macro = pkg.getMacrosById().get(id);
assertThat(macro).isNotNull();
return macro;
}
/** Maps a list of labels to a more convenient list of strings. */
private static ImmutableList<String> asStringList(List<Label> labelList) {
return labelList.stream().map(Label::getCanonicalForm).collect(toImmutableList());
}
/**
* Retrieves the visibility labels of the target with the given name, which must exist in the
* package.
*/
private static ImmutableList<String> getTargetVisibility(Package pkg, String name)
throws Exception {
Target target = pkg.getTarget(name);
assertThat(target).isNotNull();
return asStringList(target.getActualVisibility().getDeclaredLabels());
}
/**
* Retrieves the visibility labels of the macro with the given id, which must exist in the
* package.
*/
private static ImmutableList<String> getMacroVisibility(Package pkg, String id) throws Exception {
return asStringList(getMacroById(pkg, id).getActualVisibility());
}
/**
* Convenience method for asserting that a package evaluates in error and produces an event
* containing the given substring.
*
* <p>Note that this is not suitable for errors that occur during top-level .bzl evaluation (i.e.,
* triggered by load() rather than during BUILD evaluation), since our test framework fails to
* produce a result in that case (b/26382502).
*/
private void assertGetPackageFailsWithEvent(String pkgName, String msg) throws Exception {
reporter.removeHandler(failFastHandler);
Package pkg = getPackage(pkgName);
assertThat(pkg).isNotNull();
assertThat(pkg.containsErrors()).isTrue();
assertContainsEvent(msg);
}
/**
* Convenience method for asserting that a package evaluates without error, but that the given
* target cannot be configured due to violating macro naming rules.
*/
private void assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck(
String pkgName, String macroName, String targetName) throws Exception {
Package pkg = getPackage(pkgName);
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey(targetName);
String labelString = String.format("//%s:%s", pkgName, targetName);
AssertionError error =
Assert.assertThrows(AssertionError.class, () -> getConfiguredTarget(labelString));
assertThat(error)
.hasMessageThat()
.contains(
String.format(
"Target %s declared in symbolic macro '%s' violates macro naming rules",
labelString, macroName));
}
@Test
public void macroCanBeDefinedUsingFactory() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
pass
def macro_factory():
return macro(implementation=_impl)
my_macro = macro_factory()
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name = "abc")
""");
assertPackageNotInError(getPackage("pkg"));
}
// Regression test for b/409532322
@Test
public void macroCannotBeDefinedInBuildFileThread() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
pass
def macro_factory():
return macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "macro_factory")
my_macro = macro_factory()
""");
assertGetPackageFailsWithEvent("pkg", "macro() can only be used during .bzl initialization");
}
@Test
public void implementationIsInvokedWithNameParam() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
print("my_macro called with name = %s" % name)
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("called with name = abc");
}
@Test
public void implementationFailsDueToBadSignature() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl():
pass
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent(
"pkg", "_impl() got unexpected keyword arguments: name, visibility");
}
@Test
public void implementationMustNotReturnAValue() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
return True
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent("pkg", "macro 'abc' may not return a non-None value (got True)");
}
/**
* Writes source files for package with a given name such that there is a macro by the given name
* declaring a target by the given name.
*/
private void setupForMacroWithSingleTarget(String pkgName, String macroName, String targetName)
throws Exception {
scratch.file(
String.format("%s/foo.bzl", pkgName),
String.format(
"""
def _impl(name, visibility):
native.cc_library(name="%s")
my_macro = macro(implementation=_impl)
""",
targetName));
scratch.file(
String.format("%s/BUILD", pkgName),
String.format(
"""
load(":foo.bzl", "my_macro")
my_macro(name="%s")
""",
macroName));
}
@Test
@TestParameters({"{separator: '_'}", "{separator: '-'}", "{separator: '.'}"})
public void macroTargetName_canBeNamePlusSeparatorPlusSomething(String separator)
throws Exception {
String targetName = String.format("abc%slib", separator);
setupForMacroWithSingleTarget("pkg", "abc", targetName);
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey(targetName);
assertThat(getConfiguredTarget(String.format("//pkg:%s", targetName))).isNotNull();
}
@Test
public void macroTargetName_canBeJustNameForMainTarget() throws Exception {
setupForMacroWithSingleTarget("pkg", "abc", "abc");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey("abc");
assertThat(getConfiguredTarget("//pkg:abc")).isNotNull();
assertThat(pkg.getMacrosById()).containsKey("abc:1");
}
@Test
public void macroTargetName_cannotBeNonSuffixOfName() throws Exception {
setupForMacroWithSingleTarget("pkg", "abc", "xyz");
assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck("pkg", "abc", "xyz");
}
@Test
public void macroTargetName_cannotBeLibPrefixOfName() throws Exception {
setupForMacroWithSingleTarget("pkg", "abc", "libabc.so");
assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck("pkg", "abc", "libabc.so");
}
@Test
@TestParameters({"{separator: ''}", "{separator: '@'}"})
public void macroTargetName_cannotBeInvalidSeparatorPlusSomething(String separator)
throws Exception {
String targetName = String.format("abc%sxyz", separator);
setupForMacroWithSingleTarget("pkg", "abc", targetName);
assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck("pkg", "abc", targetName);
}
@Test
@TestParameters({"{separator: '_'}", "{separator: '-'}", "{separator: '.'}"})
public void macroTargetName_cannotBeNamePlusSeparatorPlusNothing(String separator)
throws Exception {
String targetName = "abc" + separator;
setupForMacroWithSingleTarget("pkg", "abc", targetName);
assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck("pkg", "abc", targetName);
}
@Test
public void illegallyNamedExportsFilesDoNotBreakPackageLoadingButCannotBeConfigured()
throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
# valid names
native.exports_files(srcs=["abc_txt"])
native.exports_files(srcs=["abc-txt"])
native.exports_files(srcs=["abc.txt"])
# allowed during package loading, but cannot be configured
native.exports_files(srcs=["xyz.txt"])
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck("pkg", "abc", "xyz.txt");
assertThat(getConfiguredTarget("//pkg:abc_txt")).isNotNull();
assertThat(getConfiguredTarget("//pkg:abc-txt")).isNotNull();
assertThat(getConfiguredTarget("//pkg:abc.txt")).isNotNull();
}
@Test
public void illegallyNamedOutputsDoNotBreakPackageLoadingButCannotBeConfigured()
throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _my_rule_impl(ctx):
ctx.actions.write(ctx.outputs.out1, "")
ctx.actions.write(ctx.outputs.out2, "")
ctx.actions.write(ctx.outputs.out3, "")
ctx.actions.write(ctx.outputs.out4, "")
return []
my_rule = rule(
implementation = _my_rule_impl,
outputs = {
# valid names
"out1": "%{name}_out1",
"out2": "%{name}-out2",
"out3": "%{name}.out3",
# allowed during package loading, but cannot be configured
"out4": "lib%{name}.so",
},
)
def _my_macro_impl(name, visibility):
my_rule(name=name)
my_macro = macro(implementation=_my_macro_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertPackageLoadsButGetConfiguredTargetFailsMacroNamingCheck("pkg", "abc", "libabc.so");
assertThat(getConfiguredTarget("//pkg:abc")).isNotNull();
assertThat(getConfiguredTarget("//pkg:abc_out1")).isNotNull();
assertThat(getConfiguredTarget("//pkg:abc-out2")).isNotNull();
assertThat(getConfiguredTarget("//pkg:abc.out3")).isNotNull();
}
@Test
public void illegallyNamedTargetsProvideAnalysisFailureInfo() throws Exception {
useConfiguration("--allow_analysis_failures=true");
setupForMacroWithSingleTarget("pkg", "abc", "libabc.so");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
ConfiguredTarget target = getConfiguredTarget("//pkg:libabc.so");
AnalysisFailureInfo info =
(AnalysisFailureInfo) target.get(AnalysisFailureInfo.STARLARK_CONSTRUCTOR.getKey());
AnalysisFailure failure = info.getCauses().getSet(AnalysisFailure.class).toList().get(0);
assertThat(failure.getMessage())
.contains(
"Target //pkg:libabc.so declared in symbolic macro 'abc' violates macro naming rules"
+ " and cannot be built.");
}
@Test
public void targetOutsideMacroMayInvadeMacroNamespace() throws Exception {
// Targets outside a macro may have names that would be valid for targets inside the macro.
// This is not an error so long as no actual target inside the macro clashes on that name.
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name + "_inside_macro")
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
cc_library(name = "abc_outside_macro")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(pkg.getTargets().keySet()).containsAtLeast("abc_inside_macro", "abc_outside_macro");
assertThat(pkg.getMacrosById()).containsKey("abc:1");
}
@Test
public void targetOutsideMacroMayNotClashWithTargetInsideMacro() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name + "_target")
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
cc_library(name = "abc_target")
""");
assertGetPackageFailsWithEvent(
"pkg", "cc_library rule 'abc_target' conflicts with existing cc_library rule");
}
@Test
public void macroCanReferToInputFile() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
native.cc_library(
name = name,
srcs = [
"explicit_input.cc",
# This usage does not cause implicit_input.cc to be created since we're inside
# a symbolic macro. We force the input's creation by referring to it from bar
# in the BUILD file.
"implicit_input.cc",
],
)
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
exports_files(["explicit_input.cc"])
cc_library(name = "bar", srcs = ["implicit_input.cc"])
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey("abc");
assertThat(pkg.getTargets()).containsKey("implicit_input.cc");
assertThat(pkg.getTargets()).containsKey("explicit_input.cc");
}
@Test
public void macroCannotForceCreationOfImplicitInputFileOnItsOwn() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _sub_impl(name, visibility):
native.cc_library(
name = name + "_target",
srcs = ["implicit_input.cc"],
)
my_submacro = macro(implementation=_sub_impl)
def _impl(name, visibility):
native.cc_library(
name = name + "_target",
srcs = ["implicit_input.cc"],
)
my_submacro(name = name + "_submacro")
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
// Confirm that implicit_input.cc is not a target of the package, despite being used inside a
// symbolic macro (and not by anything at the top level) for both a target and a submacro.
//
// It'd be an execution time error to attempt to build the declared rule targets, but the
// package still loads just fine.
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey("abc_target");
assertThat(pkg.getTargets()).containsKey("abc_submacro_target");
assertThat(pkg.getTargets()).doesNotContainKey("implicit_input.cc");
}
@Test
public void macroCanDeclareSubmacros() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _inner_impl(name, visibility):
native.cc_library(name = name + "_lib")
inner_macro = macro(implementation=_inner_impl)
def _impl(name, visibility):
inner_macro(name = name + "_inner")
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey("abc_inner_lib");
}
@Test
public void submacroNameMustFollowPrefixNamingConvention() throws Exception {
scratch.file("pkg/BUILD");
scratch.file(
"pkg/foo.bzl",
"""
def _inner_impl(name, visibility):
pass
inner_macro = macro(implementation=_inner_impl)
def _impl(name, visibility, sep):
inner_macro(name = name + sep + "inner")
my_macro = macro(implementation=_impl, attrs={"sep": attr.string(configurable=False)})
""");
scratch.file(
"good/BUILD",
"""
load("//pkg:foo.bzl", "my_macro")
my_macro(name="abc", sep = "_") # ok
my_macro(name="def", sep = "-") # ok
my_macro(name="ghi", sep = ".") # ok
""");
scratch.file(
"bad/BUILD",
"""
load("//pkg:foo.bzl", "my_macro")
my_macro(name="jkl", sep = "$") # bad
""");
Package good = getPackage("good");
assertPackageNotInError(good);
assertGetPackageFailsWithEvent("bad", "macro 'jkl' cannot declare submacro named 'jkl$inner'");
}
@Test
public void submacroMayHaveSameNameAsAncestorMacros() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _inner_impl(name, visibility):
native.cc_library(name = name)
inner_macro = macro(implementation=_inner_impl)
def _middle_impl(name, visibility):
inner_macro(name = name)
middle_macro = macro(implementation=_middle_impl)
def _outer_impl(name, visibility):
middle_macro(name = name)
outer_macro = macro(implementation = _outer_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "outer_macro")
outer_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(pkg.getTargets()).containsKey("abc");
assertThat(pkg.getMacrosById()).containsKey("abc:1");
assertThat(pkg.getMacrosById()).containsKey("abc:2");
assertThat(pkg.getMacrosById()).containsKey("abc:3");
}
@Test
public void cannotHaveTwoMainTargets() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name)
native.cc_library(name = name)
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent(
"pkg", "cc_library rule 'abc' conflicts with existing cc_library rule");
}
@Test
public void cannotHaveTwoMainSubmacros() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _inner_impl(name, visibility):
pass
inner_macro = macro(implementation=_inner_impl)
def _impl(name, visibility):
inner_macro(name = name)
inner_macro(name = name)
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent(
"pkg", "macro 'abc' conflicts with an existing macro (and was not created by it)");
}
@Test
public void cannotHaveBothMainTargetAndMainSubmacro_submacroDeclaredFirst() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _inner_impl(name, visibility):
# Don't define a main target; we don't want to trigger a name conflict between this and
# the outer target.
pass
inner_macro = macro(implementation=_inner_impl)
def _impl(name, visibility):
inner_macro(name = name)
native.cc_library(name = name)
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent(
"pkg", "target 'abc' conflicts with an existing macro (and was not created by it)");
}
@Test
public void cannotHaveBothMainTargetAndMainSubmacro_targetDeclaredFirst() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _inner_impl(name, visibility):
# Don't define a main target; we don't want to trigger a name conflict between this and
# the outer target.
pass
inner_macro = macro(implementation=_inner_impl)
def _impl(name, visibility):
native.cc_library(name = name)
inner_macro(name = name)
my_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent("pkg", "macro 'abc' conflicts with an existing target");
}
/**
* Implementation of a test that ensures a given API cannot be called from inside a symbolic
* macro.
*/
private void doCannotCallApiTest(String apiName, String usageLine) throws Exception {
doCannotCallApiTest(apiName, usageLine, "used");
}
private void doCannotCallApiTest(String apiName, String usageLine, String errorMessageParticiple)
throws Exception {
scratch.file(
"pkg/foo.bzl",
String.format(
"""
def _impl(name, visibility):
%s
my_macro = macro(implementation=_impl)
""",
usageLine));
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
assertGetPackageFailsWithEvent(
"pkg",
String.format(
// The error also has one of the following suffixes:
// - " or a legacy macro"
// - ", a rule finalizer, a legacy macro, or a WORKSPACE file"
"%s can only be %s while evaluating a BUILD file", apiName, errorMessageParticiple));
}
@Test
public void macroCannotCallPackage() throws Exception {
doCannotCallApiTest(
"package()", "native.package(default_visibility = ['//visibility:public'])");
}
@Test
public void macroCannotCallGlob() throws Exception {
doCannotCallApiTest("glob()", "native.glob(['foo*'])");
}
@Test
public void macroCannotCallSubpackages() throws Exception {
doCannotCallApiTest("subpackages()", "native.subpackages(include = ['*'])");
}
@Test
public void macroCannotCallExistingRule() throws Exception {
doCannotCallApiTest("existing_rule()", "native.existing_rule('foo')");
}
@Test
public void macroCannotCallExistingRules() throws Exception {
doCannotCallApiTest("existing_rules()", "native.existing_rules()");
}
@Test
public void macroCannotCallEnvironmentRuleFunction() throws Exception {
doCannotCallApiTest("environment rule", "native.environment(name = 'foo')", "instantiated");
}
// There are other symbols that must not be called from within symbolic macros, but we don't test
// them because they can't be obtained from a symbolic macro implementation anyway, since they are
// not under `native` (at least, for BUILD-loaded .bzl files) and because symbolic macros can't
// take arbitrary parameter types from their caller. These untested symbols include:
//
// - For BUILD threads: licenses(), environment_group()
// - For WORKSPACE threads: workspace(), register_toolchains(), register_execution_platforms(),
// bind(), and repository rules.
//
// Starlark-defined repository rules might technically be callable but we skip over that edge
// case here.
@Test
public void existingRules_canSeeTargetsCreatedByOrdinaryMacros() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
native.cc_binary(name = name + "_lib")
my_macro = macro(implementation=_impl)
def query():
print("existing_rules() keys: %s" % native.existing_rules().keys())
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro", "query")
cc_library(name = "outer_target")
my_macro(name="abc")
query()
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("existing_rules() keys: [\"outer_target\", \"abc_lib\"]");
}
@Test
public void existingRules_cannotSeeTargetsCreatedByFinalizers() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
native.cc_binary(name = name + "_lib")
my_macro = macro(implementation=_impl, finalizer=True)
def query():
print("existing_rules() keys: %s" % native.existing_rules().keys())
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro", "query")
cc_library(name = "outer_target")
my_macro(name="abc")
query()
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("existing_rules() keys: [\"outer_target\"]");
}
@Test
public void hardcodedDefaultAttrValue_isUsedWhenNotOverriddenAndAttrHasNoUserSpecifiedDefault()
throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, dep_nonconfigurable, dep_configurable, xyz_configurable):
print("dep_nonconfigurable is %s" % dep_nonconfigurable)
print("dep_configurable is %s" % dep_configurable)
print("xyz_configurable is %s" % xyz_configurable)
my_macro = macro(
implementation = _impl,
attrs = {
# Test label type, since LabelType#getDefaultValue returns null.
"dep_nonconfigurable": attr.label(configurable=False),
# Try it again, this time configurable. Select()-promotion doesn't apply to None.
"dep_configurable": attr.label(),
# Now do it for a value besides None. Select()-promotion applies.
"xyz_configurable": attr.string(),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("dep_nonconfigurable is None");
assertContainsEvent("dep_configurable is None");
assertContainsEvent("xyz_configurable is select({\"//conditions:default\": \"\"})");
}
@Test
public void defaultAttrValue_isUsedWhenNotOverridden() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
print("xyz is %s" % xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(default="DEFAULT", configurable=False)
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("xyz is DEFAULT");
}
@Test
public void defaultAttrValue_canBeOverridden() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
print("xyz is %s" % xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(default="DEFAULT", configurable=False)
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = "OVERRIDDEN",
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("xyz is OVERRIDDEN");
}
@Test
public void defaultAttrValue_isUsed_whenAttrIsImplicit() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, _xyz):
print("xyz is %s" % _xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"_xyz": attr.string(default="IMPLICIT", configurable=False)
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("xyz is IMPLICIT");
}
@Test
public void defaultAttrValue_wrappingMacroTakesPrecedenceOverWrappedRule() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _rule_impl(ctx):
pass
my_rule = rule(
implementation = _rule_impl,
attrs = {"dep": attr.label(default="//common:rule_default")},
)
def _macro_impl(name, visibility, dep):
my_rule(name = name, dep = dep)
my_macro = macro(
implementation = _macro_impl,
attrs = {"dep": attr.label(default="//common:macro_default")},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
Rule rule = pkg.getRule("abc");
assertThat(rule).isNotNull();
assertThat(rule.getAttr("dep"))
.isEqualTo(Label.parseCanonicalUnchecked("//common:macro_default"));
}
@Test
public void noneAttrValue_doesNotOverrideDefault() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
print("xyz is %s" % xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(default="DEFAULT", configurable=False)
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = None,
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("xyz is DEFAULT");
}
@Test
public void noneAttrValue_doesNotSatisfyMandatoryRequirement() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
pass
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(mandatory=True),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = None,
)
""");
assertGetPackageFailsWithEvent(
"pkg", "missing value for mandatory attribute 'xyz' in 'my_macro' macro");
}
@Test
public void noneAttrValue_disallowedWhenAttrDoesNotExist() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility):
pass
my_macro = macro(
implementation = _impl,
attrs = {
"xzz": attr.string(doc="This attr is public"),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = None,
)
""");
assertGetPackageFailsWithEvent(
"pkg", "no such attribute 'xyz' in 'my_macro' macro (did you mean 'xzz'?)");
}
@Test
public void stringAttrsAreConvertedToLabelsAndInRightContext() throws Exception {
scratch.file("lib/BUILD");
scratch.file(
"lib/foo.bzl",
"""
def _impl(name, visibility, xyz, _xyz):
print("xyz is %s" % xyz)
print("_xyz is %s" % _xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.label(configurable = False),
"_xyz": attr.label(default=":BUILD", configurable=False)
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load("//lib:foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = ":BUILD", # Should be parsed relative to //pkg, not //lib
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("xyz is @@//pkg:BUILD");
assertContainsEvent("_xyz is @@//lib:BUILD");
}
@Test
public void cannotMutateAttrValues() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
xyz.append(4)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.int_list(configurable=False),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = [1, 2, 3],
)
""");
assertGetPackageFailsWithEvent("pkg", "Error in append: trying to mutate a frozen list value");
}
@Test
public void attrsAllowSelectsByDefault() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
print("xyz is %s" % xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = select({"//some:condition": ":target1", "//some:other_condition": ":target2"}),
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent(
"xyz is select({Label(\"//some:condition\"): \":target1\","
+ " Label(\"//some:other_condition\"): \":target2\"})");
}
@Test
public void noneAttrValue_canAppearInSelects() throws Exception {
// None can appear as a value in a select() entry, and its meaning is "in this case, use
// whatever default would've been chosen if this attribute weren't specified" -- i.e. the same
// behavior as when None is used as the whole attribute value.
//
// The three cases for using a default value are 1) the attribute schema specifies a default, or
// else 2) the attribute type specifies a hardcoded default that is a valid value for the type
// (e.g. the empty string for StringType), or 3) the attribute type uses null as its hardcoded
// default, which we represent in Starlark as None at rule analysis time. We exercise all three
// cases here.
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, attr_using_schema_default, attr_using_hardcoded_nonnull_default,
attr_using_hardcoded_null_default):
print("attr_using_schema_default is %s" % attr_using_schema_default)
print("attr_using_hardcoded_nonnull_default is %s"
% attr_using_hardcoded_nonnull_default)
print("attr_using_hardcoded_null_default is %s" % attr_using_hardcoded_null_default)
my_macro = macro(
implementation = _impl,
attrs = {
"attr_using_schema_default": attr.string(default="some_default"),
"attr_using_hardcoded_nonnull_default": attr.string(),
"attr_using_hardcoded_null_default": attr.label(),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
attr_using_schema_default = select({
"//common:some_configsetting": None,
"//conditions:default": None,
}),
attr_using_hardcoded_nonnull_default = select({
"//common:some_configsetting": None,
"//conditions:default": None,
}),
attr_using_hardcoded_null_default = select({
"//common:some_configsetting": None,
"//conditions:default": None,
}),
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
// From the macro implementation's point of view, the select() entries are still None,
// regardless of how they are represented and transformed internally.
assertContainsEvent(
"""
attr_using_schema_default is select({Label("//common:some_configsetting"): None, \
Label("//conditions:default"): None})\
""");
assertContainsEvent(
"""
attr_using_hardcoded_nonnull_default is select({Label("//common:some_configsetting"): None, \
Label("//conditions:default"): None})\
""");
assertContainsEvent(
"""
attr_using_hardcoded_null_default is select({Label("//common:some_configsetting"): None, \
Label("//conditions:default"): None})\
""");
}
@Test
public void configurableAttrValuesArePromotedToSelects() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility,
configurable_xyz, nonconfigurable_xyz, configurable_default_xyz):
print("configurable_xyz is '%s' (type %s)" %
(str(configurable_xyz), type(configurable_xyz)))
print("nonconfigurable_xyz is '%s' (type %s)" %
(str(nonconfigurable_xyz), type(nonconfigurable_xyz)))
print("configurable_default_xyz is '%s' (type %s)" %
(str(configurable_default_xyz), type(configurable_default_xyz)))
my_macro = macro(
implementation = _impl,
attrs = {
"configurable_xyz": attr.string(),
"nonconfigurable_xyz": attr.string(configurable=False),
"configurable_default_xyz": attr.string(default = "xyz"),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
configurable_xyz = "configurable",
nonconfigurable_xyz = "nonconfigurable",
# configurable_default_xyz not set
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent(
"configurable_xyz is 'select({\"//conditions:default\": \"configurable\"})' (type select)");
assertContainsEvent("nonconfigurable_xyz is 'nonconfigurable' (type string)");
assertContainsEvent(
"configurable_default_xyz is 'select({\"//conditions:default\": \"xyz\"})' (type select)");
}
@Test
public void nonconfigurableAttrValuesProhibitSelects() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
print("xyz is %s" % xyz)
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(configurable=False),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = select({"//some:condition": ":target1", "//some:other_condition": ":target2"}),
)
""");
assertGetPackageFailsWithEvent("pkg", "attribute \"xyz\" is not configurable");
}
// TODO(b/331193690): Prevent selects from being evaluated as bools
@Test
public void selectableAttrCanBeEvaluatedAsBool() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, xyz):
# Allowed for now when xyz is a select().
# In the future, we'll ban implicit conversion and only allow
# if there's an explicit bool(xyz).
if xyz:
print("xyz evaluates to True")
else:
print("xyz evaluates to False")
my_macro = macro(
implementation = _impl,
attrs = {
"xyz": attr.string(),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
xyz = select({"//conditions:default" :"False"}),
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertContainsEvent("xyz evaluates to True");
assertDoesNotContainEvent("xyz evaluates to False");
}
@Test
public void labelVisitation() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _impl,
attrs = {
"singular": attr.label(configurable=False),
"list": attr.label_list(configurable=False),
"not_a_label": attr.string_list(configurable=False),
"output": attr.output(), # (always nonconfigurable)
"configurable": attr.label(),
"configurable_withdefault": attr.label(default="//common:configurable_withdefault"),
# These are not passed in below.
"omitted": attr.label(configurable=False),
"_implicit_default": attr.label(default="//common:implicit_default"),
"explicit_default": attr.label(default="//common:explicit_default"),
},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(
name = "abc",
singular = "//A:A",
list = ["//A:A", "//B:B"], # duplicate with previous attr
not_a_label = ["qwerty"],
output = "out.txt",
configurable = select({
"//Q:cond1": "//C:C",
"//Q:cond2": None,
"//conditions:default": "//D:D",
}),
configurable_withdefault = select({"//Q:cond": "//E:E", "//conditions:default": None}),
visibility = ["//common:my_package_group"],
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
MacroInstance macroInstance = getMacroById(pkg, "abc:1");
ArrayList<Label> labels = new ArrayList<>();
macroInstance.visitExplicitAttributeLabels(labels::add);
// Order is the same as the attribute definition order.
assertThat(asStringList(labels))
.containsExactly(
"//A:A",
"//A:A", // duplicate not pruned
"//B:B",
// `not_a_label` and `output` are skipped
"//C:C",
// //Q:cond2 maps to default, which doesn't exist for that attr
"//D:D",
"//E:E",
"//common:configurable_withdefault", // from attr default
// `omitted` ignored, it has no default
// `_implicit_default` ignored because it's implicit
"//common:explicit_default" // from attr default
// `visibility` ignored, it's a NODEP label list
)
.inOrder();
}
@Test
public void macrosThreadVisibilityAttrThroughWithCallsiteLocationAdded() throws Exception {
// Don't use test machinery's convenience setup that makes everything public by default.
setPackageOptions("--default_visibility=private");
// Submacro defines two targets, one exported (visibility = visibility) and one internal
// (private to the submacro's package).
scratch.file("lib1/BUILD");
scratch.file(
"lib1/macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(
name = name + "_exported",
visibility = visibility)
native.cc_library(name = name + "_internal")
submacro = macro(implementation=_impl)
""");
// Outer macro also defines two targets, but in addition calls the submacro twice, as an
// exported submacro and an internal one. So a total of six targets across three macro
// instances.
scratch.file("lib2/BUILD");
scratch.file(
"lib2/macro.bzl",
"""
load("//lib1:macro.bzl", "submacro")
def _impl(name, visibility):
native.cc_library(
name = name + "_exported",
visibility = visibility)
native.cc_library(name = name + "_internal")
submacro(name=name + "_subexported", visibility = visibility)
submacro(name=name + "_subinternal")
outer_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load("//lib2:macro.bzl", "outer_macro")
outer_macro(name="abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
// Outer macro visible to BUILD file.
assertThat(getMacroVisibility(pkg, "abc:1")).containsExactly("//pkg:__pkg__");
// The outer macro's exported target and the exported submacro are visible to both the BUILD
// file and the outer macro.
assertThat(getTargetVisibility(pkg, "abc_exported"))
.containsExactly("//pkg:__pkg__", "//lib2:__pkg__");
assertThat(getMacroVisibility(pkg, "abc_subexported:1"))
.containsExactly("//pkg:__pkg__", "//lib2:__pkg__");
// The outer macro's internal target and the internal submacro are visible only to the outer
// macro.
assertThat(getTargetVisibility(pkg, "abc_internal")).containsExactly("//lib2:__pkg__");
assertThat(getMacroVisibility(pkg, "abc_subinternal:1")).containsExactly("//lib2:__pkg__");
// The exported submacro's exported target is visible to everything (exports all the way down).
assertThat(getTargetVisibility(pkg, "abc_subexported_exported"))
.containsExactly("//pkg:__pkg__", "//lib2:__pkg__", "//lib1:__pkg__");
// The internal submacro's exported target is visible to the outer macro and submacro.
assertThat(getTargetVisibility(pkg, "abc_subinternal_exported"))
.containsExactly("//lib2:__pkg__", "//lib1:__pkg__");
// Finally, the internal targets of both the exported and internal submacros are visible only to
// the submacro.
assertThat(getTargetVisibility(pkg, "abc_subexported_internal"))
.containsExactly("//lib1:__pkg__");
assertThat(getTargetVisibility(pkg, "abc_subinternal_internal"))
.containsExactly("//lib1:__pkg__");
}
@Test
public void defaultVisibilityAppliesOnlyToTopLevelMacros() throws Exception {
scratch.file("lib/BUILD");
scratch.file(
"lib/macro.bzl",
"""
def _sub_impl(name, visibility):
pass
submacro = macro(implementation=_sub_impl)
def _impl(name, visibility):
submacro(name=name + "_sub")
outer_macro = macro(implementation=_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load("//lib:macro.bzl", "outer_macro")
package(default_visibility=["//defaulted:__pkg__"])
outer_macro(
name = "macro_with_explicit_vis",
visibility = ["//explicit:__pkg__"],
)
outer_macro(name="macro_without_explicit_vis")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
// Default visibility only applies when visibility is not specified.
assertThat(getMacroVisibility(pkg, "macro_with_explicit_vis:1"))
.containsExactly("//explicit:__pkg__", "//pkg:__pkg__");
// When it does apply, we still append the callsite location.
assertThat(getMacroVisibility(pkg, "macro_without_explicit_vis:1"))
.containsExactly("//defaulted:__pkg__", "//pkg:__pkg__");
// Default visibility never applies inside a symbolic macro (i.e. to submacros).
assertThat(getMacroVisibility(pkg, "macro_with_explicit_vis_sub:1"))
.containsExactly("//lib:__pkg__");
assertThat(getMacroVisibility(pkg, "macro_without_explicit_vis_sub:1"))
.containsExactly("//lib:__pkg__");
}
@Test
public void wrongKeyTypeInAttrsDict_detected() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _impl,
attrs = {123: attr.string()},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name = "abc")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent("got dict<int, Attribute> for 'attrs', want dict<string, Attribute|None>");
}
@Test
public void wrongKeyValueInAttrsDict_detected() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _impl,
attrs = {"bad attr": None},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name = "abc")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent("attribute name `bad attr` is not a valid identifier");
}
@Test
public void wrongValueTypeInAttrsDict_detected() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _impl,
attrs = {"bad": 123},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name = "abc")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent("got dict<string, int> for 'attrs', want dict<string, Attribute|None>");
}
@Test
public void noneValueInAttrsDict_ignored() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _impl,
attrs = {"disabled_attr": None},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
my_macro(name = "abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertMacroDoesNotHaveAttributes(getMacroById(pkg, "abc:1"), ImmutableList.of("disabled_attr"));
}
@Test
public void inheritAttrs_fromInvalidSource_fails() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _my_macro_impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _my_macro_impl,
inherit_attrs = "???",
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent(
"Invalid 'inherit_attrs' value \"???\"; expected a rule, a macro, or \"common\"");
}
@Test
public void inheritAttrs_withoutKwargsInImplementation_fails() throws Exception {
scratch.file(
"pkg/foo.bzl",
"""
def _my_macro_impl(name, visibility, tags):
pass
my_macro = macro(
implementation = _my_macro_impl,
inherit_attrs = "common"
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":foo.bzl", "my_macro")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent(
"If inherit_attrs is set, implementation function must have a **kwargs parameter");
}
@Test
public void inheritAttrs_fromCommon_withOverrides() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _my_macro_impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _my_macro_impl,
attrs = {
# add a new attr
"new_attr": attr.string(),
# override an inherited attr
"tags": attr.string_list(default = ["foo"]),
# remove an inherited attr
"features": None,
},
inherit_attrs = "common",
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
// inherited attrs
MacroInstance macroInstance = getMacroById(pkg, "abc:1");
assertMacroHasAttributes(
macroInstance, ImmutableList.of("compatible_with", "testonly", "toolchains"));
// overridden attr
assertThat(
macroInstance
.getMacroClass()
.getAttributeProvider()
.getAttributeByName("tags")
.getDefaultValueUnchecked())
.isEqualTo(ImmutableList.of("foo"));
// non-inherited attr
assertMacroDoesNotHaveAttributes(macroInstance, ImmutableList.of("features"));
// internal public attrs which macro machinery must avoid inheriting
assertMacroDoesNotHaveAttributes(
macroInstance,
ImmutableList.of("generator_name", "generator_location", "generator_function"));
}
@Test
public void inheritAttrs_fromAnyNativeRule() throws Exception {
// Ensure that a symbolic macro can inherit attributes from (and thus, can conveniently wrap)
// any native rule. Native rules may use attribute definitions which are unavailable to Starlark
// rules, so to verify that we handle the native attribute corner cases, we exhaustively test
// wrapping of all builtin rule classes which are accessible from Starlark. We do not test rule
// classes which are exposed to Starlark via macro wrappers in @_builtins, because Starlark code
// typically cannot get at the wrapped native rule's rule class symbol from which to inherit
// attributes. We also do not test rule target instantiation (and thus, do not test whether such
// a target would pass analysis) because declaring arbitrary native rule targets is difficult to
// automate.
//
// This test is expected to fail if:
// * a native rule adds a mandatory attribute of a type which is not supported by this test's
// fakeMandatoryArgs mechanism below (to fix, add support for it to fakeMandatoryArgs); or
// * a new AttributeValueSource or a new attribute type is introduced, and symbolic macros
// cannot inherit an attribute with a default with this source or of such a type (to fix, add
// a check for it in MacroClass#forceDefaultToNone).
for (RuleClass ruleClass : getBuiltinRuleClasses(false)) {
if (ruleClass.getAttributeProvider().getAttributes().isEmpty()) {
continue;
}
if (!(ruleClass.getRuleClassType().equals(RuleClass.Builder.RuleClassType.NORMAL)
|| ruleClass.getRuleClassType().equals(RuleClass.Builder.RuleClassType.TEST))) {
continue;
}
String pkgName = "pkg_" + ruleClass.getName();
String macroName = "my_" + ruleClass.getName();
// Provide fake values for mandatory attributes in macro invocation
StringBuilder fakeMandatoryArgs = new StringBuilder();
for (Attribute attr : ruleClass.getAttributeProvider().getAttributes()) {
String fakeValue = null;
if (attr.isPublic() && attr.isMandatory() && !attr.getName().equals("name")) {
Type<?> type = attr.getType();
if (type.equals(Type.STRING)
|| type.equals(BuildType.OUTPUT)
|| type.equals(BuildType.LABEL)
|| type.equals(BuildType.NODEP_LABEL)
|| type.equals(BuildType.DORMANT_LABEL)
|| type.equals(BuildType.GENQUERY_SCOPE_TYPE)) {
fakeValue = "\":fake\"";
} else if (type.equals(Types.STRING_LIST)
|| type.equals(BuildType.OUTPUT_LIST)
|| type.equals(BuildType.LABEL_LIST)
|| type.equals(BuildType.NODEP_LABEL_LIST)
|| type.equals(BuildType.DORMANT_LABEL_LIST)
|| type.equals(BuildType.GENQUERY_SCOPE_TYPE_LIST)) {
fakeValue = "[\":fake\"]";
} else if (type.equals(BuildType.LABEL_DICT_UNARY)
|| type.equals(BuildType.LABEL_KEYED_STRING_DICT)) {
fakeValue = "{\":fake\": \":fake\"}";
}
}
if (fakeValue != null) {
fakeMandatoryArgs.append(", ").append(attr.getName()).append(" = ").append(fakeValue);
}
}
scratch.file(
pkgName + "/macro.bzl",
String.format(
"""
def _impl(name, visibility, **kwargs):
pass
%s = macro(
implementation = _impl,
inherit_attrs = native.%s,
)
""",
macroName, ruleClass.getName()));
scratch.file(
pkgName + "/BUILD",
String.format(
"""
load(":macro.bzl", "%s")
%s(name = "abc"%s)
""",
macroName, macroName, fakeMandatoryArgs));
Package pkg = getPackage(pkgName);
assertPackageNotInError(pkg);
assertMacroHasAttributes(
getMacroById(pkg, "abc:1"),
ruleClass.getAttributeProvider().getAttributes().stream()
.filter(a -> a.isPublic() && a.isDocumented())
.map(Attribute::getName)
.collect(toImmutableList()));
assertMacroDoesNotHaveAttributes(
getMacroById(pkg, "abc:1"),
ImmutableList.of(
"generator_name", "generator_location", "generator_function", "generator_location"));
}
}
@Test
public void inheritAttrs_fromGenrule_producesTargetThatPassesAnalysis() throws Exception {
// inheritAttrs_fromAnyNativeRule() above is loading-phase only; by contrast, this test verifies
// that we can wrap a native rule (in this case, genrule) in a macro with inherit_attrs, and
// that the macro-wrapped rule target passes analysis.
scratch.file(
"pkg/my_genrule.bzl",
"""
def _my_genrule_impl(name, visibility, tags, **kwargs):
print("my_genrule: tags = %s" % tags)
for k in kwargs:
print("my_genrule: kwarg %s = %s" % (k, kwargs[k]))
native.genrule(name = name + "_wrapped_genrule", visibility = visibility, **kwargs)
my_genrule = macro(
implementation = _my_genrule_impl,
inherit_attrs = native.genrule,
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_genrule.bzl", "my_genrule")
my_genrule(
name = "abc",
outs = ["out.txt"],
cmd = "touch $@",
)
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertThat(getConfiguredTarget("//pkg:abc_wrapped_genrule")).isNotNull();
assertContainsEvent("my_genrule: tags = None"); // Not []
assertContainsEvent(
"my_genrule: kwarg srcs = None"); // Not select({"//conditions:default": []})
assertContainsEvent(
"my_genrule: kwarg testonly = None"); // Not select({"//conditions:default": False})
}
@Test
public void inheritAttrs_fromExportedStarlarkRule() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _my_rule_impl(ctx):
pass
_my_rule = rule(
implementation = _my_rule_impl,
attrs = {
"srcs": attr.label_list(),
},
)
def _my_macro_impl(name, visibility, **kwargs):
_my_rule(name = name + "_my_rule", visibility = visibility, **kwargs)
my_macro = macro(
implementation = _my_macro_impl,
inherit_attrs = _my_rule,
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertMacroHasAttributes(getMacroById(pkg, "abc:1"), ImmutableList.of("srcs", "tags"));
assertMacroDoesNotHaveAttributes(
getMacroById(pkg, "abc:1"),
ImmutableList.of("generator_name", "generator_location", "generator_function"));
}
@Test
public void inheritAttrs_fromUnexportedStarlarkRule_fails() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _my_rule_impl(ctx):
pass
_unexported = struct(
rule = rule(
implementation = _my_rule_impl,
attrs = {
"srcs": attr.label_list(),
},
),
)
def _my_macro_impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _my_macro_impl,
inherit_attrs = _unexported.rule,
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "abc")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent(
"a rule or macro callable must be assigned to a global variable in a .bzl file before it"
+ " can be inherited from");
}
@Test
public void inheritAttrs_fromExportedMacro() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _other_macro_impl(name, visibility, **kwargs):
pass
_other_macro = macro(
implementation = _other_macro_impl,
attrs = {
"srcs": attr.label_list(),
"tags": attr.string_list(configurable = False),
},
)
def _my_macro_impl(name, visibility, tags, **kwargs):
print("my_macro: tags = %s" % tags)
for k in kwargs:
print("my_macro: kwarg %s = %s" % (k, kwargs[k]))
_other_macro(name = name + "_other_macro", visibility = visibility, tags = tags, **kwargs)
my_macro = macro(
implementation = _my_macro_impl,
inherit_attrs = _other_macro,
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "abc")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
assertMacroHasAttributes(
getMacroById(pkg, "abc:1"), ImmutableList.of("name", "visibility", "srcs", "tags"));
assertThat(
getMacroById(pkg, "abc:1").getMacroClass().getAttributeProvider().getAttributeCount())
.isEqualTo(4);
assertContainsEvent("my_macro: tags = None"); // Not []
assertContainsEvent("my_macro: kwarg srcs = None"); // Not select({"//conditions:default": []})
}
@Test
public void inheritAttrs_fromUnexportedMacro_fails() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _other_macro_impl(name, visibility, **kwargs):
pass
_unexported = struct(
macro = macro(
implementation = _other_macro_impl,
attrs = {
"srcs": attr.label_list(),
},
),
)
def _my_macro_impl(name, visibility, **kwargs):
pass
my_macro = macro(
implementation = _my_macro_impl,
inherit_attrs = _unexported.macro,
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "abc")
""");
reporter.removeHandler(failFastHandler);
assertThat(getPackage("pkg")).isNull();
assertContainsEvent(
"a rule or macro callable must be assigned to a global variable in a .bzl file before it"
+ " can be inherited from");
}
@Test
public void generatorInfoAndCallStack_atTopLevel() throws Exception {
// cc_binary_legacy_macro is a legacy macro instantiating a cc_binary rule.
scratch.file(
"pkg/inner_legacy_macro.bzl",
"""
def inner_legacy_macro(name, **kwargs):
native.cc_binary(name = name, **kwargs)
""");
// my_macro is a symbolic macro that instantiates 2 cc_binary rules: one directly, and one
// wrapped by cc_binary_legacy_macro.
scratch.file(
"pkg/my_macro.bzl",
"""
load(":inner_legacy_macro.bzl", "inner_legacy_macro")
def _impl(name, visibility, **kwargs):
native.cc_binary(name = name + "_lib")
inner_legacy_macro(name = name + "_legacy_macro_lib")
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
MacroInstance foo = getMacroById(pkg, "foo:1");
assertThat(foo.getBuildFileLocation())
.isEqualTo(Location.fromFileLineColumn("/workspace/pkg/BUILD", 3, 9));
assertThat(foo.reconstructParentCallStack())
.containsExactly(
StarlarkThread.callStackEntry(StarlarkThread.TOP_LEVEL, foo.getBuildFileLocation()),
StarlarkThread.callStackEntry(
"my_macro", Location.fromFileLineColumn("/workspace/pkg/my_macro.bzl", 7, 1)))
.inOrder();
Rule fooLib = pkg.getRule("foo_lib");
assertThat(fooLib.isRuleCreatedInMacro()).isTrue();
assertThat(fooLib.getLocation()).isEqualTo(foo.getBuildFileLocation());
assertThat(fooLib.getAttr("generator_name", Type.STRING)).isEqualTo("foo");
assertThat(fooLib.getAttr("generator_function", Type.STRING)).isEqualTo("my_macro");
assertThat(fooLib.getAttr("generator_location", Type.STRING)).isEqualTo("pkg/BUILD:3:9");
assertThat(fooLib.reconstructCallStack())
.isEqualTo(
ImmutableList.builder()
.addAll(foo.reconstructParentCallStack())
.add(
StarlarkThread.callStackEntry(
"_impl", Location.fromFileLineColumn("/workspace/pkg/my_macro.bzl", 4, 21)))
.build());
Rule fooLegacyLib = pkg.getRule("foo_legacy_macro_lib");
assertThat(fooLegacyLib.isRuleCreatedInMacro()).isTrue();
assertThat(fooLegacyLib.getLocation()).isEqualTo(foo.getBuildFileLocation());
assertThat(fooLegacyLib.getAttr("generator_name", Type.STRING)).isEqualTo("foo");
assertThat(fooLegacyLib.getAttr("generator_function", Type.STRING)).isEqualTo("my_macro");
assertThat(fooLegacyLib.getAttr("generator_location", Type.STRING)).isEqualTo("pkg/BUILD:3:9");
assertThat(fooLegacyLib.reconstructCallStack())
.isEqualTo(
ImmutableList.builder()
.addAll(foo.reconstructParentCallStack())
.add(
StarlarkThread.callStackEntry(
"_impl", Location.fromFileLineColumn("/workspace/pkg/my_macro.bzl", 5, 23)))
.add(
StarlarkThread.callStackEntry(
"inner_legacy_macro",
Location.fromFileLineColumn(
"/workspace/pkg/inner_legacy_macro.bzl", 2, 23)))
.build());
}
@Test
public void generatorInfoAndCallStack_nestedMacros() throws Exception {
// inner_legacy_wrapper is a legacy macro wrapper around inner_macro, which is a symbolic macro
// that instantiates a cc_binary rule.
scratch.file(
"pkg/inner.bzl",
"""
def _inner_impl(name, visibility, **kwargs):
native.cc_binary(name = name, **kwargs)
inner_macro = macro(implementation = _inner_impl)
def inner_legacy_wrapper(name, **kwargs):
inner_macro(name = name, **kwargs)
""");
// outer_legacy_wrapper is a legacy wrapper around outer_macro, which is a symbolic macro that
// invokes inner_legacy_wrapper.
scratch.file(
"pkg/outer.bzl",
"""
load(":inner.bzl", "inner_legacy_wrapper")
def _outer_impl(name, visibility, **kwargs):
inner_legacy_wrapper(name = name + "_inner", **kwargs)
outer_macro = macro(implementation = _outer_impl)
def outer_legacy_wrapper(name, **kwargs):
outer_macro(name = name, **kwargs)
""");
scratch.file(
"pkg/BUILD",
"""
load(":outer.bzl", "outer_legacy_wrapper")
outer_legacy_wrapper(name = "foo")
""");
Package pkg = getPackage("pkg");
assertPackageNotInError(pkg);
MacroInstance foo = getMacroById(pkg, "foo:1");
assertThat(foo.getBuildFileLocation())
.isEqualTo(Location.fromFileLineColumn("/workspace/pkg/BUILD", 3, 21));
assertThat(foo.reconstructParentCallStack())
.containsExactly(
StarlarkThread.callStackEntry(StarlarkThread.TOP_LEVEL, foo.getBuildFileLocation()),
StarlarkThread.callStackEntry(
"outer_legacy_wrapper",
Location.fromFileLineColumn("/workspace/pkg/outer.bzl", 9, 16)),
StarlarkThread.callStackEntry(
"outer_macro", Location.fromFileLineColumn("/workspace/pkg/outer.bzl", 6, 1)))
.inOrder();
MacroInstance fooInner = getMacroById(pkg, "foo_inner:1");
assertThat(fooInner.getBuildFileLocation()).isEqualTo(foo.getBuildFileLocation());
assertThat(fooInner.reconstructParentCallStack())
.containsExactly(
StarlarkThread.callStackEntry(
"_outer_impl", Location.fromFileLineColumn("/workspace/pkg/outer.bzl", 4, 25)),
StarlarkThread.callStackEntry(
"inner_legacy_wrapper",
Location.fromFileLineColumn("/workspace/pkg/inner.bzl", 7, 16)),
StarlarkThread.callStackEntry(
"inner_macro", Location.fromFileLineColumn("/workspace/pkg/inner.bzl", 4, 1)))
.inOrder();
Rule fooLib = pkg.getRule("foo_inner");
assertThat(fooLib.isRuleCreatedInMacro()).isTrue();
assertThat(fooLib.getLocation()).isEqualTo(foo.getBuildFileLocation());
assertThat(fooLib.getAttr("generator_name", Type.STRING)).isEqualTo("foo");
assertThat(fooLib.getAttr("generator_function", Type.STRING)).isEqualTo("outer_legacy_wrapper");
assertThat(fooLib.getAttr("generator_location", Type.STRING)).isEqualTo("pkg/BUILD:3:21");
assertThat(fooLib.reconstructCallStack())
.isEqualTo(
ImmutableList.builder()
.addAll(foo.reconstructParentCallStack())
.addAll(fooInner.reconstructParentCallStack())
.add(
StarlarkThread.callStackEntry(
"_inner_impl",
Location.fromFileLineColumn("/workspace/pkg/inner.bzl", 2, 21)))
.build());
}
@Test
public void maxComputationSteps_enforcedInMacros() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
# exceed max_computation_steps
for i in range(1000):
pass
native.cc_library(name = name, visibility = visibility)
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
setBuildLanguageOptions("--max_computation_steps=100"); // sufficient for BUILD but not my_macro
reporter.removeHandler(failFastHandler);
NoSuchPackageException exception =
assertThrows(
NoSuchPackageException.class,
() ->
getPackageManager()
.getPackage(reporter, PackageIdentifier.createInMainRepo("pkg")));
assertThat(exception)
.hasMessageThat()
.containsMatch("computation took 1\\d{3} steps, but --max_computation_steps=100");
}
@Test
public void failingMacro_immediatelyThrowsEvalExceptionWithFullCallStack() throws Exception {
scratch.file(
"pkg/inner.bzl",
"""
def _inner_impl(name, visibility, **kwargs):
fail("Inner macro failed")
inner_macro = macro(implementation = _inner_impl)
""");
scratch.file(
"pkg/outer.bzl",
"""
load(":inner.bzl", "inner_macro")
def _outer_impl(name, visibility, **kwargs):
inner_macro(name = name + "_inner", **kwargs)
fail("This should not be reached")
outer_macro = macro(implementation = _outer_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":outer.bzl", "outer_macro")
outer_macro(name = "foo")
fail("This should not be reached")
""");
reporter.removeHandler(failFastHandler);
Package pkg = getPackage("pkg");
assertThat(pkg.containsErrors()).isTrue();
assertDoesNotContainEvent("This should not be reached");
assertContainsEvent(
"""
\tFile "/workspace/pkg/BUILD", line 3, column 12, in <toplevel>
\t\touter_macro(name = "foo")
\tFile "/workspace/pkg/outer.bzl", line 7, column 1, in outer_macro
\t\touter_macro = macro(implementation = _outer_impl)
\tFile "/workspace/pkg/outer.bzl", line 4, column 16, in _outer_impl
\t\tinner_macro(name = name + "_inner", **kwargs)
\tFile "/workspace/pkg/inner.bzl", line 4, column 1, in inner_macro
\t\tinner_macro = macro(implementation = _inner_impl)
\tFile "/workspace/pkg/inner.bzl", line 2, column 9, in _inner_impl
\t\tfail("Inner macro failed")
""");
}
private void assertMacroHasAttributes(MacroInstance macro, ImmutableList<String> attributeNames) {
for (String attributeName : attributeNames) {
assertThat(
macro.getMacroClass().getAttributeProvider().getAttributeByNameMaybe(attributeName))
.isNotNull();
}
}
private void assertMacroDoesNotHaveAttributes(
MacroInstance macro, ImmutableList<String> attributeNames) {
for (String attributeName : attributeNames) {
assertThat(
macro.getMacroClass().getAttributeProvider().getAttributeByNameMaybe(attributeName))
.isNull();
}
}
}