| // 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.truth.Truth.assertWithMessage; | |
| import com.google.common.base.Preconditions; | |
| import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; | |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; | |
| import com.google.testing.junit.testparameterinjector.TestParameters; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| /** | |
| * Tests for the how the visibility system works with respect to symbolic macros, i.e. the | |
| * Macro-Aware Visibility design. | |
| * | |
| * <p>This does *not* include tests of how the {@code visibility} attribute's value gets determined | |
| * and threaded through macros. See SymbolicMacroTest.java for that. | |
| */ | |
| @RunWith(TestParameterInjector.class) | |
| public final class MacroVisibilityTest extends BuildViewTestCase { | |
| @Override | |
| protected String getDefaultVisibility() { | |
| // We're testing visibility. Avoid having to litter our test cases with `visibility=` attribute | |
| // declarations, by using the same behavior seen in production. | |
| return "private"; | |
| } | |
| /** | |
| * Returns a substring expected to be in the error event reported for a visibility violation from | |
| * {@code consumer} (expressed as a label string) to {@code dependency} (expressed as a label | |
| * string, or for {@code alias} targets, the string returned by {@link | |
| * AliasProvider#describeTargetWithAliases}). | |
| */ | |
| private String visibilityErrorMessage( | |
| String consumer, String dependency, boolean dependencyIsAlias) { | |
| if (!dependencyIsAlias) { | |
| dependency = String.format("target '%s'", dependency); | |
| } | |
| return String.format( | |
| "Visibility error:\n%s is not visible from\ntarget '%s'", dependency, consumer); | |
| } | |
| /** | |
| * Convenience wrapper for {@link #visibilityErrorMessage} for when {@code dependency} is not an | |
| * {@code alias}. | |
| */ | |
| private String visibilityErrorMessage(String consumer, String dependency) { | |
| return visibilityErrorMessage(consumer, dependency, /* dependencyIsAlias= */ false); | |
| } | |
| /** | |
| * Requests the evaluation of the configured target identified by the label {@code consumer}, and | |
| * asserts that the target analyzes successfully and has a dependency on the target identified by | |
| * the label {@code dependency}. | |
| * | |
| * <p>Does not work when {@code dependency} is an {@code alias} target. | |
| */ | |
| private void assertVisibilityPermits(String consumer, String dependency) throws Exception { | |
| ConfiguredTarget consumerTarget = getConfiguredTarget(consumer); | |
| // If the dependency is in fact *not* visible, consumerTarget will be null. So we assert on the | |
| // absence of the visibility error event first, to deliver a better error message than just | |
| // saying consumerTarget was null. (We can't just attach a nicer message to the null check | |
| // because consumerTarget could be null due to other unexpected errors.) | |
| // | |
| // This nicer handling is moot if the failFastHandler is present, in which case we'll see the | |
| // visibility error event in a traceback. | |
| assertDoesNotContainEvent(visibilityErrorMessage(consumer, dependency)); | |
| assertWithMessage( | |
| String.format( | |
| "%s had an error (other than a visibility violation on %s)", consumer, dependency)) | |
| .that(consumerTarget) | |
| .isNotNull(); | |
| assertWithMessage(String.format("%s does not have a dependency on %s", consumer, dependency)) | |
| .that(getDirectPrerequisite(consumerTarget, dependency)) | |
| .isNotNull(); | |
| } | |
| /** | |
| * Requests the evaluation of the configured target identified by the label {@code consumer}, and | |
| * asserts that the target is in error due to a visibility violation on the target identified by | |
| * the label {@code dependency}. | |
| * | |
| * <p>Removes the {@code failFastHandler}. | |
| */ | |
| private void assertVisibilityDisallows( | |
| String consumer, String dependency, boolean dependencyIsAlias) throws Exception { | |
| reporter.removeHandler(failFastHandler); | |
| ConfiguredTarget consumerTarget = getConfiguredTarget(consumer); | |
| assertWithMessage( | |
| String.format( | |
| "%s unexpectedly did not have an error (no visibility violation due to %s)", | |
| consumer, dependency)) | |
| .that(consumerTarget) | |
| .isNull(); | |
| assertContainsEvent(visibilityErrorMessage(consumer, dependency, dependencyIsAlias)); | |
| } | |
| /** | |
| * Convenience wrapper for {@link #assertVisibilityDisallows} for when {@code dependency} is not | |
| * an {@code alias}. | |
| */ | |
| private void assertVisibilityDisallows(String consumer, String dependency) throws Exception { | |
| assertVisibilityDisallows(consumer, dependency, /* dependencyIsAlias= */ false); | |
| } | |
| /** | |
| * Creates definition of {@code //rules:simple_rule.bzl%simple_rule}, a rule that has a label | |
| * attribute {@code dep} and implicit output {@code <NAME>.bin}. | |
| */ | |
| private void defineSimpleRule() throws Exception { | |
| scratch.file("rules/BUILD"); | |
| scratch.file( | |
| "rules/simple_rule.bzl", | |
| """ | |
| def _impl(ctx): | |
| ctx.actions.write(ctx.outputs.out, "") | |
| simple_rule = rule( | |
| implementation = _impl, | |
| attrs = {"dep": attr.label(mandatory=False, allow_files=True)}, | |
| outputs = {"out": "%{name}.bin"}, | |
| ) | |
| """); | |
| } | |
| /** | |
| * Creates a package {@code //common} defining several {@code simple_rule} targets with the given | |
| * names and visibility declarations. | |
| * | |
| * <p>The argument list must have even length, and contains pairs of (unquoted) target names and | |
| * Starlark expression strings that evaluate to visibility lists (e.g. pass the string {@code | |
| * "[]"} for no visibility). | |
| */ | |
| private void defineCommonPackageWithSimpleTargets(String... targetNamesAndVisibilities) | |
| throws Exception { | |
| Preconditions.checkArgument(targetNamesAndVisibilities.length % 2 == 0); | |
| StringBuilder s = new StringBuilder(); | |
| s.append( | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| """); | |
| for (int i = 0; i < targetNamesAndVisibilities.length; i += 2) { | |
| s.append( | |
| String.format( | |
| """ | |
| simple_rule( | |
| name = "%s", | |
| visibility = %s, | |
| ) | |
| """, | |
| targetNamesAndVisibilities[i], targetNamesAndVisibilities[i + 1])); | |
| } | |
| scratch.file("common/BUILD", s.toString()); | |
| } | |
| @Test | |
| public void buildFileAccessToMacroTargetsIsControlled() throws Exception { | |
| defineSimpleRule(); | |
| scratch.file("lib/BUILD"); | |
| scratch.file( | |
| "lib/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule( | |
| name = name + "_exported", | |
| visibility = ["//pkg:__pkg__"], | |
| ) | |
| simple_rule( | |
| name = name + "_internal", | |
| ) | |
| native.exports_files([name + "_exported_input"], visibility = ["//pkg:__pkg__"]) | |
| native.exports_files([name + "_internal_input"], visibility = ["//visibility:private"]) | |
| my_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| load("//lib:macro.bzl", "my_macro") | |
| my_macro(name = "foo") | |
| simple_rule(name = "consumes_exported_ruletarget", dep = ":foo_exported") | |
| simple_rule(name = "consumes_exported_output", dep = ":foo_exported.bin") | |
| simple_rule(name = "consumes_exported_input", dep = ":foo_exported_input") | |
| simple_rule(name = "consumes_internal_ruletarget", dep = ":foo_internal") | |
| simple_rule(name = "consumes_internal_output", dep = ":foo_internal.bin") | |
| simple_rule(name = "consumes_internal_input", dep = ":foo_internal_input") | |
| """); | |
| assertVisibilityPermits("//pkg:consumes_exported_ruletarget", "//pkg:foo_exported"); | |
| assertVisibilityPermits("//pkg:consumes_exported_output", "//pkg:foo_exported.bin"); | |
| assertVisibilityPermits("//pkg:consumes_exported_input", "//pkg:foo_exported_input"); | |
| assertVisibilityDisallows("//pkg:consumes_internal_ruletarget", "//pkg:foo_internal"); | |
| assertVisibilityDisallows("//pkg:consumes_internal_output", "//pkg:foo_internal.bin"); | |
| assertVisibilityDisallows("//pkg:consumes_internal_input", "//pkg:foo_internal_input"); | |
| } | |
| @Test | |
| public void macroAccessToBuildFileTargetsIsControlled() throws Exception { | |
| defineSimpleRule(); | |
| scratch.file("lib/BUILD"); | |
| scratch.file( | |
| "lib/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule(name = name + "_consumes_exported_ruletarget", dep = "//pkg:exported") | |
| simple_rule(name = name + "_consumes_exported_output", dep = "//pkg:exported.bin") | |
| simple_rule(name = name + "_consumes_exported_input", dep = "//pkg:exported_input") | |
| simple_rule(name = name + "_consumes_internal_ruletarget", dep = "//pkg:internal") | |
| simple_rule(name = name + "_consumes_internal_output", dep = "//pkg:internal.bin") | |
| simple_rule(name = name + "_consumes_internal_input", dep = "//pkg:internal_input") | |
| my_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| load("//lib:macro.bzl", "my_macro") | |
| my_macro(name = "foo") | |
| simple_rule( | |
| name = "exported", | |
| visibility = ["//lib:__pkg__"], | |
| ) | |
| simple_rule( | |
| name = "internal", | |
| ) | |
| exports_files(["exported_input"], visibility = ["//lib:__pkg__"]) | |
| exports_files(["internal_input"], visibility = ["//visibility:private"]) | |
| """); | |
| assertVisibilityPermits("//pkg:foo_consumes_exported_ruletarget", "//pkg:exported"); | |
| assertVisibilityPermits("//pkg:foo_consumes_exported_output", "//pkg:exported.bin"); | |
| assertVisibilityPermits("//pkg:foo_consumes_exported_input", "//pkg:exported_input"); | |
| assertVisibilityDisallows("//pkg:foo_consumes_internal_ruletarget", "//pkg:internal"); | |
| assertVisibilityDisallows("//pkg:foo_consumes_internal_output", "//pkg:internal.bin"); | |
| assertVisibilityDisallows("//pkg:foo_consumes_internal_input", "//pkg:internal_input"); | |
| } | |
| @Test | |
| public void macroAccessToOtherMacrosInSameBuildFileIsControlled() throws Exception { | |
| defineSimpleRule(); | |
| scratch.file("producer/BUILD"); | |
| scratch.file( | |
| "producer/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule( | |
| name = name + "_internal", | |
| ) | |
| simple_rule( | |
| name = name + "_visible_to_pkg", | |
| visibility = ["//pkg:__pkg__"], | |
| ) | |
| simple_rule( | |
| name = name + "_visible_to_consumer", | |
| visibility = ["//consumer:__pkg__"], | |
| ) | |
| producer_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file("consumer/BUILD"); | |
| scratch.file( | |
| "consumer/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule( | |
| name = name + "_consumes_internal", | |
| dep = "//pkg:foo_internal", | |
| ) | |
| simple_rule( | |
| name = name + "_consumes_visible_to_pkg", | |
| dep = "//pkg:foo_visible_to_pkg", | |
| ) | |
| simple_rule( | |
| name = name + "_consumes_visible_to_consumer", | |
| dep = "//pkg:foo_visible_to_consumer", | |
| ) | |
| consumer_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//producer:macro.bzl", "producer_macro") | |
| load("//consumer:macro.bzl", "consumer_macro") | |
| producer_macro(name = "foo") | |
| consumer_macro(name = "bar") | |
| """); | |
| assertVisibilityDisallows("//pkg:bar_consumes_internal", "//pkg:foo_internal"); | |
| assertVisibilityDisallows("//pkg:bar_consumes_visible_to_pkg", "//pkg:foo_visible_to_pkg"); | |
| assertVisibilityPermits( | |
| "//pkg:bar_consumes_visible_to_consumer", "//pkg:foo_visible_to_consumer"); | |
| } | |
| @Test | |
| public void siblingsInSameMacroCanSeeEachOther() throws Exception { | |
| defineSimpleRule(); | |
| scratch.file("lib/BUILD"); | |
| scratch.file( | |
| "lib/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule( | |
| name = name + "_ruletarget", | |
| ) | |
| simple_rule( | |
| name = name + "_consumes_ruletarget", | |
| dep = name + "_ruletarget", | |
| ) | |
| native.exports_files([name + "_input"], visibility = ["//visibility:private"]) | |
| simple_rule( | |
| name = name + "_consumes_input", | |
| dep = name + "_input", | |
| ) | |
| my_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| load("//lib:macro.bzl", "my_macro") | |
| my_macro(name = "foo") | |
| simple_rule( | |
| name = "control", | |
| dep = ":foo_ruletarget", | |
| ) | |
| """); | |
| assertVisibilityPermits("//pkg:foo_consumes_ruletarget", "//pkg:foo_ruletarget"); | |
| assertVisibilityPermits("//pkg:foo_consumes_input", "//pkg:foo_input"); | |
| assertVisibilityDisallows("//pkg:control", "//pkg:foo_ruletarget"); | |
| } | |
| @Test | |
| public void buildFileTargetIsVisibleToTargetsDefinedByMacroFromSamePackage() throws Exception { | |
| // This test is needed because the declaration location of a target is only materialized in its | |
| // visibility attribute when the target is declared inside a symbolic macro. We need to | |
| // confirm that we're still allowing the edge from a target declared by a macro whose code is in | |
| // //pkg, to a dependency declared in //pkg:BUILD, despite the dependency not materializing | |
| // //pkg in its own visibility. | |
| defineSimpleRule(); | |
| scratch.file( | |
| "pkg/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule(name = name + "_macro_target", dep = "//pkg:build_target") | |
| my_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file( | |
| "other_pkg/BUILD", | |
| """ | |
| load("//pkg:macro.bzl", "my_macro") | |
| my_macro(name = "foo") | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| simple_rule(name = "build_target") | |
| """); | |
| // Allowed because my_macro's code was defined in //pkg, even though it was instantiated in | |
| // //other_pkg. | |
| assertVisibilityPermits("//other_pkg:foo_macro_target", "//pkg:build_target"); | |
| } | |
| @Test | |
| public void macroDefinitionLocationIsDeterminedByExportAssignment() throws Exception { | |
| // Consider a target declared by a helper func in //A, which is called from an impl func in //B, | |
| // which is used in a macro() definition in //C, which is loaded and exported by //D, and called | |
| // by a legacy macro defined in //E, itself called in a BUILD file in //F. Then the declaration | |
| // location of the target is considered to be //D, where the export occurs, rather than any of | |
| // those other packages. | |
| // | |
| // This test defines six targets in //pkg, visible to each of //A through //F. The helper | |
| // function in //A declares six targets that try to consume each of the respective targets from | |
| // //pkg. Only the one that consumes the target visible to //D is allowed to succeed. | |
| defineSimpleRule(); | |
| scratch.file("A/BUILD"); | |
| scratch.file( | |
| "A/helper.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def helper(name): | |
| for c in ["A", "B", "C", "D", "E", "F"]: | |
| simple_rule( | |
| name = name + "_wants_vis_to_%s" % c, | |
| dep = "//pkg:vis_to_%s" % c, | |
| ) | |
| """); | |
| scratch.file("B/BUILD"); | |
| scratch.file( | |
| "B/impl.bzl", | |
| """ | |
| load("//A:helper.bzl", "helper") | |
| def impl(name, visibility): | |
| helper(name) | |
| """); | |
| scratch.file("C/BUILD"); | |
| scratch.file( | |
| "C/metamacro.bzl", | |
| """ | |
| load("//B:impl.bzl", "impl") | |
| def metamacro(): | |
| return macro(implementation = impl) | |
| """); | |
| scratch.file("D/BUILD"); | |
| scratch.file( | |
| "D/my_macro.bzl", | |
| """ | |
| load("//C:metamacro.bzl", "metamacro") | |
| my_macro = metamacro() | |
| """); | |
| scratch.file("E/BUILD"); | |
| scratch.file( | |
| "E/legacy_macro.bzl", | |
| """ | |
| load("//D:my_macro.bzl", "my_macro") | |
| def legacy_macro(name): | |
| my_macro(name = name) | |
| """); | |
| scratch.file( | |
| "F/BUILD", | |
| """ | |
| load("//E:legacy_macro.bzl", "legacy_macro") | |
| legacy_macro(name = "consumer") | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| [ | |
| simple_rule( | |
| name = "vis_to_%s" % c, | |
| visibility = ["//%s:__pkg__" % c]) | |
| for c in ["A", "B", "C", "D", "E", "F"] | |
| ] | |
| """); | |
| assertVisibilityDisallows("//F:consumer_wants_vis_to_A", "//pkg:vis_to_A"); | |
| assertVisibilityDisallows("//F:consumer_wants_vis_to_B", "//pkg:vis_to_B"); | |
| assertVisibilityDisallows("//F:consumer_wants_vis_to_C", "//pkg:vis_to_C"); | |
| assertVisibilityPermits("//F:consumer_wants_vis_to_D", "//pkg:vis_to_D"); | |
| assertVisibilityDisallows("//F:consumer_wants_vis_to_E", "//pkg:vis_to_E"); | |
| assertVisibilityDisallows("//F:consumer_wants_vis_to_F", "//pkg:vis_to_F"); | |
| } | |
| @Test | |
| public void locationOfTargetDeclarationIsInnermostMacro() throws Exception { | |
| // The //common package has a target visible to an outer macro and a target visible to an inner | |
| // macro. In //pkg, the outer macro is instantiated as "foo" and the inner as "foo_inner". We | |
| // check each combination of foo and foo_inner trying to access the two common targets. | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets( | |
| "vis_to_inner", "['//inner:__pkg__']", "vis_to_outer", "['//outer:__pkg__']"); | |
| scratch.file("inner/BUILD"); | |
| scratch.file( | |
| "inner/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility): | |
| simple_rule( | |
| name = name + "_wants_vis_to_inner", | |
| dep = "//common:vis_to_inner", | |
| ) | |
| simple_rule( | |
| name = name + "_wants_vis_to_outer", | |
| dep = "//common:vis_to_outer", | |
| ) | |
| inner_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file("outer/BUILD"); | |
| scratch.file( | |
| "outer/macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| load("//inner:macro.bzl", "inner_macro") | |
| def _impl(name, visibility): | |
| inner_macro(name = name + "_inner") | |
| simple_rule( | |
| name = name + "_wants_vis_to_inner", | |
| dep = "//common:vis_to_inner", | |
| ) | |
| simple_rule( | |
| name = name + "_wants_vis_to_outer", | |
| dep = "//common:vis_to_outer", | |
| ) | |
| outer_macro = macro(implementation=_impl) | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//outer:macro.bzl", "outer_macro") | |
| outer_macro(name = "foo") | |
| """); | |
| assertVisibilityPermits("//pkg:foo_inner_wants_vis_to_inner", "//common:vis_to_inner"); | |
| assertVisibilityDisallows("//pkg:foo_inner_wants_vis_to_outer", "//common:vis_to_outer"); | |
| assertVisibilityDisallows("//pkg:foo_wants_vis_to_inner", "//common:vis_to_inner"); | |
| assertVisibilityPermits("//pkg:foo_wants_vis_to_outer", "//common:vis_to_outer"); | |
| } | |
| // This is the simplest version of the caller delegating visibility privileges to a macro. | |
| // Subsequent test cases don't bother to test opting out of select() promotion by setting | |
| // configurable=False. | |
| @Test | |
| @TestParameters({"{depIsConfigurable: false}", "{depIsConfigurable: true}"}) | |
| public void buildFileCanDelegateVisibilityPrivilegesToMacro(boolean depIsConfigurable) | |
| throws Exception { | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets("vis_to_pkg", "['//pkg:__pkg__']"); | |
| scratch.file("macros/BUILD"); | |
| scratch.file( | |
| "macros/simple_rule_wrapper.bzl", | |
| String.format( | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility, dep): | |
| simple_rule( | |
| name = name, | |
| dep = dep, | |
| ) | |
| simple_rule_wrapper = macro( | |
| implementation = _impl, | |
| attrs = {"dep": attr.label(configurable=%s)}, | |
| ) | |
| """, | |
| depIsConfigurable ? "True" : "False")); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//macros:simple_rule_wrapper.bzl", "simple_rule_wrapper") | |
| simple_rule_wrapper( | |
| name = "foo", | |
| dep = "//common:vis_to_pkg", | |
| ) | |
| """); | |
| // Allowed, even though //pkg:foo's declaration location is not //pkg, because the macro that | |
| // //pkg:foo is declared in is itself declared in //pkg and passes vis_to_pkg in as an | |
| // attribute. | |
| assertVisibilityPermits("//pkg:foo", "//common:vis_to_pkg"); | |
| } | |
| /** | |
| * Defines a macro {@code name} whose body calls symbol {@code wraps} to instantiate its main | |
| * target or submacro, passing along {@code attrExpr}. | |
| * | |
| * <p>The new macro is declared in {@code //<name>:macro.bzl}. It takes an optional {@code dep} | |
| * attribute. | |
| * | |
| * @param name the macro being introduced, e.g. "my_macro", which would be loaded from {@code | |
| * //my_macro:macro.bzl} | |
| * @param wraps the rule or macro to be called by this macro, in {@code bzlLabel%symbol} form, | |
| * e.g. "//some_pkg:defs.bzl%my_rule" | |
| * @param attrExpr an attribute argument expression for the call site of {@code wraps}, e.g. | |
| * "some_attr = dep" | |
| */ | |
| private void defineWrappingMacro(String name, String wraps, String attrExpr) throws Exception { | |
| int i = wraps.indexOf("%"); | |
| Preconditions.checkArgument(i != -1); | |
| String bzlToLoad = wraps.substring(0, i); | |
| String symbolName = wraps.substring(i + 1); | |
| scratch.file(String.format("%s/BUILD", name)); | |
| scratch.file( | |
| String.format("%s/macro.bzl", name), | |
| String.format( | |
| """ | |
| load("%2$s", "%3$s") | |
| def _impl(name, visibility, dep): | |
| %3$s( | |
| name = name, | |
| %4$s, | |
| ) | |
| %1$s = macro( | |
| implementation = _impl, | |
| attrs = {"dep": attr.label(mandatory=False)}, | |
| ) | |
| """, | |
| name, bzlToLoad, symbolName, attrExpr)); | |
| } | |
| /** | |
| * Defines a macro {@code name} whose body calls {@code wraps} to instantiate its main target or | |
| * submacro, forwarding along the {@code dep} attribute (if supplied) unchanged. | |
| * | |
| * <p>The new macro is declared in {@code //<name>:macro.bzl}. | |
| * | |
| * @param name the macro being introduced, e.g. "my_macro", which would be loaded from {@code | |
| * //my_macro:macro.bzl} | |
| * @param wraps the rule or macro to be called by this macro, in {@code bzlLabel%symbol} form, | |
| * e.g. "//some_pkg:defs.bzl%my_rule" | |
| */ | |
| private void defineWrappingMacroWithSameDep(String name, String wraps) throws Exception { | |
| defineWrappingMacro(name, wraps, "dep = dep"); | |
| } | |
| /** | |
| * Defines a macro {@code name} whose body calls {@code wraps} to instantiate its main target or | |
| * submacro, passing {@code hardcodedDep} as the value of the {@code dep} attribute. | |
| * | |
| * <p>The new macro is declared in {@code //<name>:macro.bzl}. It takes an optional {@code dep} | |
| * attribute itself, which gets ignored. | |
| * | |
| * @param name the macro being introduced, e.g. "my_macro", which would be loaded from {@code | |
| * //my_macro:macro.bzl} | |
| * @param wraps the rule or macro to be called by this macro, in {@code bzlLabel%symbol} form, | |
| * e.g. "//some_pkg:defs.bzl%my_rule" | |
| * @param hardcodedDep a label string to pass as {@code dep = <hardcodedDep>} at the call site of | |
| * {@code wraps}, e.g. "//some_pkg:my_target" | |
| */ | |
| private void defineWrappingMacroWithHardcodedDep(String name, String wraps, String hardcodedDep) | |
| throws Exception { | |
| defineWrappingMacro(name, wraps, String.format("dep = \"%s\"", hardcodedDep)); | |
| } | |
| @Test | |
| public void outerMacroCanDelegateVisibilityPrivilegesToSubmacro() throws Exception { | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets("vis_to_outer", "['//outer:__pkg__']"); | |
| defineWrappingMacroWithSameDep("inner", "//rules:simple_rule.bzl%simple_rule"); | |
| defineWrappingMacroWithHardcodedDep( | |
| "outer", "//inner:macro.bzl%inner", "//common:vis_to_outer"); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//outer:macro.bzl", "outer") | |
| outer(name = "foo") | |
| """); | |
| // Allowed by same reasoning as buildFileCanDelegateVisibilityPrivilegesToMacro(), but with the | |
| // privilege originating from an outer macro rather than from the BUILD file. | |
| assertVisibilityPermits("//pkg:foo", "//common:vis_to_outer"); | |
| } | |
| @Test | |
| public void delegationCanBeTransitiveThroughAnIntermediaryMacro() throws Exception { | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets("vis_to_pkg", "['//pkg:__pkg__']"); | |
| defineWrappingMacroWithSameDep("inner", "//rules:simple_rule.bzl%simple_rule"); | |
| defineWrappingMacroWithSameDep("outer", "//inner:macro.bzl%inner"); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//outer:macro.bzl", "outer") | |
| outer( | |
| name = "foo", | |
| dep = "//common:vis_to_pkg", | |
| ) | |
| """); | |
| // Allowed because the package delegates its privilege to the outer macro, which in turn | |
| // delegates its privilege to the inner macro. | |
| assertVisibilityPermits("//pkg:foo", "//common:vis_to_pkg"); | |
| } | |
| @Test | |
| public void callerPermissionIsCheckedEvenIfCalleeHasItsOwnPermission() throws Exception { | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets("vis_to_macro", "['//macros:__pkg__']"); | |
| defineWrappingMacroWithSameDep("my_macro", "//rules:simple_rule.bzl%simple_rule"); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//my_macro:macro.bzl", "my_macro") | |
| my_macro( | |
| name = "foo", | |
| dep = "//common:vis_to_macro", | |
| ) | |
| """); | |
| // Even though the macro has visibility on the dep, we still fail because the caller tried to | |
| // pass it in and the caller has no such permission. | |
| assertVisibilityDisallows("//pkg:foo", "//common:vis_to_macro"); | |
| } | |
| @Test | |
| public void noDelegationIfCallerCannotSeeDep() throws Exception { | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets("not_visible", "[]"); | |
| defineWrappingMacroWithSameDep("my_macro", "//rules:simple_rule.bzl%simple_rule"); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//my_macro:macro.bzl", "my_macro") | |
| my_macro( | |
| name = "foo", | |
| dep = "//common:not_visible", | |
| ) | |
| """); | |
| // Passed in from caller, but caller had no permission to delegate. | |
| assertVisibilityDisallows("//pkg:foo", "//common:not_visible"); | |
| } | |
| @Test | |
| public void noDelegationIfCallerDoesNotPassInTarget() throws Exception { | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets( | |
| // Read: "v2[P|M]" -> "visible to [package|macro]" | |
| "v2P_hardcoded", | |
| "['//pkg:__pkg__']", | |
| "v2P_implicitdep", | |
| "['//pkg:__pkg__']", | |
| "v2M_hardcoded", | |
| "['//macros:__pkg__']", | |
| "v2M_implicitdep", | |
| "['//macros:__pkg__']"); | |
| scratch.file("macros/BUILD"); | |
| scratch.file( | |
| "macros/my_macro.bzl", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| def _impl(name, visibility, _v2P_implicitdep, _v2M_implicitdep): | |
| simple_rule( | |
| name = name + "_consumes_v2P_hardcoded", | |
| dep = "//common:v2P_hardcoded", | |
| ) | |
| simple_rule( | |
| name = name + "_consumes_v2P_implicitdep", | |
| dep = _v2P_implicitdep, | |
| ) | |
| simple_rule( | |
| name = name + "_consumes_v2M_hardcoded", | |
| dep = "//common:v2M_hardcoded", | |
| ) | |
| simple_rule( | |
| name = name + "_consumes_v2M_implicitdep", | |
| dep = _v2M_implicitdep, | |
| ) | |
| my_macro = macro( | |
| implementation = _impl, | |
| attrs = { | |
| "_v2P_implicitdep": attr.label( | |
| default="//common:v2P_implicitdep"), | |
| "_v2M_implicitdep": attr.label( | |
| default="//common:v2M_implicitdep"), | |
| }, | |
| ) | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//macros:my_macro.bzl", "my_macro") | |
| my_macro(name = "foo") | |
| """); | |
| // Macro can't see it. Caller didn't pass it in, so caller's permission is not delegated to the | |
| // macro. | |
| assertVisibilityDisallows("//pkg:foo_consumes_v2P_hardcoded", "//common:v2P_hardcoded"); | |
| // Having an implicit dep doesn't count as the caller passing it in. | |
| assertVisibilityDisallows("//pkg:foo_consumes_v2P_implicitdep", "//common:v2P_implicitdep"); | |
| // Macro can see it. Caller didn't pass it in, so it's fine that the caller can't see it. | |
| assertVisibilityPermits("//pkg:foo_consumes_v2M_hardcoded", "//common:v2M_hardcoded"); | |
| // Having an implicit dep doesn't count as the caller passing it in. | |
| assertVisibilityPermits("//pkg:foo_consumes_v2M_implicitdep", "//common:v2M_implicitdep"); | |
| // Theoretically another behavior we could try to demonstrate might be that having a label in | |
| // the "visibility" attribute does not qualify as the caller passing it in. However, the only | |
| // non-synthetic labels that can appear in visibility are package_groups or rules that imitate | |
| // them by providing PackageSpecificationProvider. package_groups always have public visibility, | |
| // so we can't manufacture a situation demonstrating that delegation has failed unless we make | |
| // an alternate rule class for that purpose, which doens't seem worth it. See also | |
| // SymbolicMacroTest#labelVisitation which covers this case at a lower level. | |
| } | |
| @Test | |
| public void delegationCannotSkipOverAGap() throws Exception { | |
| // We have a chain of macro calls A -> B -> C -> D, with a target in D consuming a dep passed in | |
| // by A. But the dep is not actually threaded all the way through; C hardcodes it, instead of | |
| // taking it as an argument from B. | |
| // | |
| // In this case, when validating the dep's visibility w.r.t. D, we don't consider A at all, even | |
| // though both A and D participate in passing or receiving this dep, respectively. In | |
| // particular, 1) we don't get the benefit of A's privileges passed on to us, and 2) if A is | |
| // violating the dep's visibility, we don't actually enforce that check (because macro | |
| // visibility violations are only discovered when actually relying on them for visibility | |
| // delegation). | |
| defineSimpleRule(); | |
| defineCommonPackageWithSimpleTargets( | |
| "some_dep", "['//A:__pkg__']", "irrelevant", "['//B:__pkg__']"); | |
| // D calls simple_rule with args unchanged. | |
| defineWrappingMacroWithSameDep("D", "//rules:simple_rule.bzl%simple_rule"); | |
| // C calls D with hardcoded dep = "//common:some_dep". | |
| defineWrappingMacroWithHardcodedDep("C", "//D:macro.bzl%D", "//common:some_dep"); | |
| // B calls C with a different hardcoded dep = "//common:irrelevant" | |
| defineWrappingMacroWithHardcodedDep("B", "//C:macro.bzl%C", "//common:irrelevant"); | |
| // A calls B with hardcoded dep = "//common:some_dep". | |
| defineWrappingMacroWithHardcodedDep("A", "//B:macro.bzl%B", "//common:some_dep"); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//A:macro.bzl", "A") | |
| A(name = "foo") | |
| """); | |
| // No privileges delegated through gap. | |
| assertVisibilityDisallows("//pkg:foo", "//common:some_dep"); | |
| // Let's retry it with the visibility rewritten to allow C, but not A. | |
| scratch.deleteFile("common/BUILD"); | |
| defineCommonPackageWithSimpleTargets("some_dep", "['//C:__pkg__']"); | |
| invalidatePackages(); // to pick up the changes to common/BUILD | |
| eventCollector.clear(); // to avoid the assertion matching events emitted above | |
| // Now D has its own privileges, and we *don't* discover that A is illegally referring to it. | |
| assertVisibilityPermits("//pkg:foo", "//common:some_dep"); | |
| } | |
| @Test | |
| public void delegationDoesNotFollowAliases() throws Exception { | |
| // The whole visibility system considers alias targets to have distinct permissions from the | |
| // underlying target they refer to. So check that these are separate entities for the purpose | |
| // of a caller delegating permission to a macro. | |
| defineSimpleRule(); | |
| scratch.file( | |
| "common/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| simple_rule( | |
| name = "vis_to_pkg", | |
| visibility = ["//pkg:__pkg__"], | |
| ) | |
| alias( | |
| name = "aliasof_vis_to_pkg", | |
| actual = ":vis_to_pkg", | |
| ) | |
| """); | |
| defineWrappingMacroWithHardcodedDep( | |
| "my_macro", "//rules:simple_rule.bzl%simple_rule", "//common:vis_to_pkg"); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//my_macro:macro.bzl", "my_macro") | |
| my_macro( | |
| name = "foo", | |
| dep = "//common:aliasof_vis_to_pkg", | |
| ) | |
| """); | |
| // Delegation doesn't work, needed the actual but passed the alias. | |
| assertVisibilityDisallows("//pkg:foo", "//common:vis_to_pkg"); | |
| // Try it the other way around. | |
| scratch.deleteFile("my_macro/BUILD"); | |
| scratch.deleteFile("my_macro/macro.bzl"); | |
| defineWrappingMacroWithHardcodedDep( | |
| "my_macro", "//rules:simple_rule.bzl%simple_rule", "//common:aliasof_vis_to_pkg"); | |
| scratch.overwriteFile( | |
| "pkg/BUILD", | |
| """ | |
| load("//my_macro:macro.bzl", "my_macro") | |
| my_macro( | |
| name = "foo", | |
| dep = "//common:vis_to_pkg", | |
| ) | |
| """); | |
| invalidatePackages(); // to pick up the changes to common/BUILD | |
| eventCollector.clear(); // to avoid the assertion matching events emitted above | |
| // Delegation doesn't work, needed the alias but passed the actual. | |
| assertVisibilityDisallows( | |
| "//pkg:foo", | |
| "alias '//common:aliasof_vis_to_pkg' referring to target '//common:vis_to_pkg'", | |
| /* dependencyIsAlias= */ true); | |
| } | |
| @Test | |
| public void packageDefaultVisibilityDoesNotPropagateToInsideMacro() throws Exception { | |
| defineSimpleRule(); | |
| defineWrappingMacroWithSameDep("my_macro", "//rules:simple_rule.bzl%simple_rule"); | |
| defineWrappingMacroWithSameDep("inner", "//rules:simple_rule.bzl%simple_rule"); | |
| defineWrappingMacroWithSameDep("outer", "//inner:macro.bzl%inner"); | |
| scratch.file( | |
| "lib/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| load("//my_macro:macro.bzl", "my_macro") | |
| load("//outer:macro.bzl", "outer") | |
| package(default_visibility=["//pkg:__pkg__"]) | |
| simple_rule(name = "foo") | |
| my_macro(name = "bar") | |
| outer(name = "baz") | |
| """); | |
| scratch.file( | |
| "pkg/BUILD", | |
| """ | |
| load("//rules:simple_rule.bzl", "simple_rule") | |
| simple_rule( | |
| name = "consumes_foo", | |
| dep = "//lib:foo", | |
| ) | |
| simple_rule( | |
| name = "consumes_bar", | |
| dep = "//lib:bar", | |
| ) | |
| simple_rule( | |
| name = "consumes_baz", | |
| dep = "//lib:baz", | |
| ) | |
| """); | |
| // Not in a macro, default_visibility applies. | |
| assertVisibilityPermits("//pkg:consumes_foo", "//lib:foo"); | |
| // In a macro, default_visibility does not propagate. | |
| assertVisibilityDisallows("//pkg:consumes_bar", "//lib:bar"); | |
| // In more than one macro, default_visibility still does not propagate. | |
| assertVisibilityDisallows("//pkg:consumes_baz", "//lib:baz"); | |
| } | |
| /* | |
| * TODO: #19922 - Tests cases to add: | |
| * | |
| * ---- Implicit deps ---- | |
| * | |
| * - If a rule's implicit dep is defined in a macro, the check of the rule's def loc against the | |
| * dep takes into account the macro's location. In particular, a rule can see implicit deps | |
| * defined by macros whose defs are in the same package as the rule's def (when the macro is | |
| * called from a totally different package), even if the dep is otherwise private. | |
| * | |
| * - If a macro has an implicit dep, that dep's visibility is checked against the macro def's | |
| * location, not its instantiation location. So pass even when the instance doesn't have | |
| * permission. And fail if the macro def location doesn't have permission, even if the instance | |
| * does have permission (either because of its declaration location or because its caller | |
| * received permission delegated by its own caller via an explicit attr value). | |
| * | |
| * ---- Visibility attr usage ---- | |
| * | |
| * - Can compose public and private visibilities with other visibilities via concatenation. | |
| * Visibility attr is normalized. (Unclear whether to apply normalization to targets defined | |
| * outside symbolic macros.) | |
| * | |
| * ---- Accounting for CommonPrerequisiteValidator#isSameLogicalPackage() ---- | |
| * | |
| * - Targets of a macro defined in //javatests/foo can see targets defined in //java/foo, even | |
| * when the macro is instantiated in //bar. (Rationale: We just look at the consumer's location, | |
| * irrespective of whether that location comes from being in a macro or a BUILD file.) | |
| * | |
| * - Targets in //javatest/foo cannot automatically see unexported targets created by a symbolic | |
| * macro defined in //java/foo, even when that macro is also instantiated in //java/foo. | |
| * (Rationale: Additional visibilities granted by same-logical-package apply only at the | |
| * top-level, similar to default_visibility. Macros can still propagate the additional | |
| * visibility down to exported targets.) | |
| * | |
| * - A target exported from a macro instantiated in //java/foo can be seen in //javatests/foo, | |
| * even when the macro is defined in //bar. (Rationale: Exported targets behave similarly to | |
| * declaring the target in the BUILD file.) | |
| */ | |
| // TODO: #19922 - Consider any other edge cases regarding exotic dependencies and other | |
| // PrerequisiteValidator code paths, e.g. implicit deps, toolchain resolution, etc. | |
| // TODO: #19922 - Consider correctness for alias targets. Probably just means auditing anywhere we | |
| // use a PackageIdentifier in visibility logic without going through | |
| // AliasProvider.getDependencyLabel(). | |
| } |