| // Copyright 2014 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.base.Preconditions.checkNotNull; |
| import static com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper.attributeOrNull; |
| |
| import com.google.devtools.build.lib.analysis.AliasProvider.TargetMode; |
| import com.google.devtools.build.lib.analysis.RuleContext.PrerequisiteValidator; |
| import com.google.devtools.build.lib.analysis.configuredtargets.PackageGroupConfiguredTarget; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.packages.Aspect; |
| import com.google.devtools.build.lib.packages.Attribute; |
| import com.google.devtools.build.lib.packages.FunctionSplitTransitionAllowlist; |
| import com.google.devtools.build.lib.packages.InputFile; |
| import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper; |
| import com.google.devtools.build.lib.packages.PackageSpecification.PackageGroupContents; |
| import com.google.devtools.build.lib.packages.RawAttributeMapper; |
| import com.google.devtools.build.lib.packages.Rule; |
| import com.google.devtools.build.lib.packages.RuleClass; |
| import com.google.devtools.build.lib.packages.StarlarkAspectClass; |
| import com.google.devtools.build.lib.packages.Type; |
| import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData; |
| |
| /** |
| * A base implementation of {@link PrerequisiteValidator} that performs common checks based on |
| * definitions of what is considered the same logical package and what is considered "experimental" |
| * code, which has may have relaxed checks for visibility and deprecation. |
| */ |
| public abstract class CommonPrerequisiteValidator implements PrerequisiteValidator { |
| @Override |
| public void validate( |
| RuleContext.Builder contextBuilder, |
| ConfiguredTargetAndData prerequisite, |
| Attribute attribute) { |
| validateDirectPrerequisiteLocation(contextBuilder, prerequisite); |
| validateDirectPrerequisiteVisibility(contextBuilder, prerequisite, attribute); |
| validateDirectPrerequisiteForTestOnly(contextBuilder, prerequisite); |
| validateDirectPrerequisiteForDeprecation( |
| contextBuilder, contextBuilder.getRule(), prerequisite, contextBuilder.forAspect()); |
| } |
| |
| /** |
| * Returns whether two packages are considered the same for purposes of deprecation warnings. |
| * Dependencies within the same package do not print deprecation warnings; a package in the |
| * javatests directory may also depend on its corresponding java package without a warning. |
| */ |
| public abstract boolean isSameLogicalPackage( |
| PackageIdentifier thisPackage, PackageIdentifier prerequisitePackage); |
| |
| /** |
| * Returns whether a package is considered experimental. Packages outside of experimental may not |
| * depend on packages that are experimental. |
| */ |
| protected abstract boolean packageUnderExperimental(PackageIdentifier packageIdentifier); |
| |
| protected abstract boolean checkVisibilityForExperimental(RuleContext.Builder context); |
| |
| protected abstract boolean checkVisibilityForToolchains( |
| RuleContext.Builder context, Label prerequisite); |
| |
| protected abstract boolean allowExperimentalDeps(RuleContext.Builder context); |
| |
| private void validateDirectPrerequisiteVisibility( |
| RuleContext.Builder context, ConfiguredTargetAndData prerequisite, Attribute attribute) { |
| String attrName = attribute.getName(); |
| Rule rule = context.getRule(); |
| |
| checkVisibilityAttributeContents(context, prerequisite, attribute, attrName, rule); |
| |
| // We don't check the visibility of late-bound attributes, because it would break some |
| // features. |
| if (Attribute.isLateBound(attrName)) { |
| return; |
| } |
| |
| // Determine whether we should check toolchain target visibility. |
| if (attrName.equals(RuleContext.TOOLCHAIN_ATTR_NAME) |
| && !checkVisibilityForToolchains(context, prerequisite.getTargetLabel())) { |
| return; |
| } |
| |
| // Only verify visibility of implicit dependencies of the current aspect. |
| // Dependencies of other aspects as well as the rule itself are checked when they are |
| // evaluated. |
| Aspect mainAspect = context.getMainAspect(); |
| if (mainAspect != null) { |
| if (!attribute.isImplicit() |
| || !mainAspect.getDefinition().getAttributes().containsKey(attrName)) { |
| return; |
| } |
| } |
| |
| if (!attribute.isImplicit() |
| || attribute.getName().equals(RuleClass.CONFIG_SETTING_DEPS_ATTRIBUTE) |
| || !context.isStarlarkRuleOrAspect()) { |
| // Default check: The attribute must be visible from the target. |
| if (!isVisible(rule.getLabel(), prerequisite)) { |
| handleVisibilityConflict(context, prerequisite, rule.getLabel()); |
| } |
| } else { |
| // For implicit attributes of Starlark rules or aspects, check if the prerequisite is visible |
| // from the location of the definition that declares the attribute. Only perform this check |
| // for the current aspect. |
| Label implicitDefinition = null; |
| if (mainAspect != null) { |
| StarlarkAspectClass aspectClass = (StarlarkAspectClass) mainAspect.getAspectClass(); |
| // Never null since we already checked that the aspect is Starlark-defined. |
| implicitDefinition = checkNotNull(aspectClass.getExtensionLabel()); |
| } else { |
| // Never null since we already checked that the rule is a Starlark rule. |
| implicitDefinition = |
| checkNotNull(rule.getRuleClassObject().getRuleDefinitionEnvironmentLabel()); |
| } |
| // Check that the prerequisite is visible from the definition. As a fallback, check if the |
| // prerequisite is visible from the target so that adopting this new style of checking |
| // visibility is not a breaking change. |
| if (implicitDefinition != null |
| && !isVisible(implicitDefinition, prerequisite) |
| && !isVisible(rule.getLabel(), prerequisite)) { |
| // In the error message, always suggest making the prerequisite visible from the definition, |
| // not the target. |
| handleVisibilityConflict(context, prerequisite, implicitDefinition); |
| } |
| } |
| } |
| |
| private boolean isVisible(Label target, ConfiguredTargetAndData prerequisite) { |
| if (isSameLogicalPackage( |
| target.getPackageIdentifier(), |
| AliasProvider.getDependencyLabel(prerequisite.getConfiguredTarget()) |
| .getPackageIdentifier())) { |
| return true; |
| } |
| // Check visibility attribute |
| for (PackageGroupContents specification : |
| prerequisite |
| .getConfiguredTarget() |
| .getProvider(VisibilityProvider.class) |
| .getVisibility() |
| .toList()) { |
| if (specification.containsPackage(target.getPackageIdentifier())) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private void checkVisibilityAttributeContents( |
| RuleContext.Builder context, |
| ConfiguredTargetAndData prerequisite, |
| Attribute attribute, |
| String attrName, |
| Rule rule) { |
| if (prerequisite.getConfiguredTarget().unwrapIfMerged() |
| instanceof PackageGroupConfiguredTarget) { |
| Attribute configuredAttribute = RawAttributeMapper.of(rule).getAttributeDefinition(attrName); |
| if (configuredAttribute == null) { // handles aspects |
| configuredAttribute = attribute; |
| } |
| String description = configuredAttribute.getRequiredProviders().getDescription(); |
| boolean containsPackageSpecificationProvider = |
| description.contains("PackageSpecificationProvider") |
| || description.contains("PackageSpecificationInfo"); |
| // TODO(plf): Add the PackageSpecificationProvider to the 'visibility' attribute. |
| if (!attrName.equals("visibility") |
| && !attrName.equals(FunctionSplitTransitionAllowlist.ATTRIBUTE_NAME) |
| && !containsPackageSpecificationProvider) { |
| context.attributeError( |
| attrName, |
| "in " |
| + attrName |
| + " attribute of " |
| + rule.getRuleClass() |
| + " rule " |
| + rule.getLabel() |
| + ": " |
| + AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITH_KIND) |
| + " is misplaced here (they are only allowed in the visibility attribute)"); |
| } |
| } |
| } |
| |
| private void handleVisibilityConflict( |
| RuleContext.Builder context, ConfiguredTargetAndData prerequisite, Label rule) { |
| if (packageUnderExperimental(rule.getPackageIdentifier()) |
| && !checkVisibilityForExperimental(context)) { |
| return; |
| } |
| |
| if (!context.getConfiguration().checkVisibility()) { |
| String errorMessage = |
| String.format( |
| "Target '%s' violates visibility of " |
| + "%s. Continuing because --nocheck_visibility is active", |
| rule, AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITHOUT_KIND)); |
| context.ruleWarning(errorMessage); |
| } else { |
| // Visibility error: |
| // target '//land:land' is not visible from |
| // target '//red_delicious:apple' |
| // Recommendation: ... |
| String errorMessage = |
| String.format( |
| "Visibility error:\n" |
| + "%s is not visible from\n" |
| + "target '%s'\n" |
| + "Recommendation: modify the visibility declaration if you think the dependency" |
| + " is legitimate. For more info see https://bazel.build/concepts/visibility", |
| AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITHOUT_KIND), rule); |
| |
| if (prerequisite.getTargetKind().equals(InputFile.targetKind())) { |
| errorMessage += |
| ". To set the visibility of that source file target, use the exports_files() function"; |
| } |
| context.ruleError(errorMessage); |
| } |
| } |
| |
| private void validateDirectPrerequisiteLocation( |
| RuleContext.Builder context, ConfiguredTargetAndData prerequisite) { |
| Rule rule = context.getRule(); |
| Label prerequisiteLabel = prerequisite.getTargetLabel(); |
| |
| if (packageUnderExperimental(prerequisiteLabel.getPackageIdentifier()) |
| && !packageUnderExperimental(rule.getLabel().getPackageIdentifier())) { |
| String message = |
| "non-experimental target '" |
| + rule.getLabel() |
| + "' depends on experimental target '" |
| + prerequisiteLabel |
| + "'"; |
| if (allowExperimentalDeps(context)) { |
| context.ruleWarning( |
| message + " (ignored due to --experimental_deps_ok;" + " do not submit)"); |
| } else { |
| context.ruleError( |
| message |
| + " (you may not check in such a dependency," |
| + " though you can test " |
| + "against it by passing --experimental_deps_ok)"); |
| } |
| } |
| } |
| |
| /** Checks if the given prerequisite is deprecated and prints a warning if so. */ |
| private void validateDirectPrerequisiteForDeprecation( |
| RuleErrorConsumer errors, |
| Rule rule, |
| ConfiguredTargetAndData prerequisite, |
| boolean forAspect) { |
| if (forAspect || attributeOrNull(rule, "deprecation", Type.STRING) != null) { |
| // No warning for aspects because the base target would already have the warning. |
| // No warning if the current target is already deprecated. |
| return; |
| } |
| |
| String warning = prerequisite.getDeprecationWarning(); |
| if (warning == null) { |
| return; // No warning if it's not deprecated. |
| } |
| |
| PackageIdentifier thisPackage = rule.getLabel().getPackageIdentifier(); |
| Label prerequisiteLabel = prerequisite.getTargetLabel(); |
| PackageIdentifier thatPackage = prerequisiteLabel.getPackageIdentifier(); |
| if (isSameLogicalPackage(thisPackage, thatPackage)) { |
| return; // Doesn't report deprecation edges within a package. |
| } |
| |
| Label generatingRuleLabel = prerequisite.getGeneratingRuleLabel(); |
| if (generatingRuleLabel != null) { |
| errors.ruleWarning( |
| "target '" |
| + rule.getLabel() |
| + "' depends on the output file " |
| + prerequisiteLabel |
| + " of a deprecated rule " |
| + generatingRuleLabel |
| + "': " |
| + warning); |
| } else { |
| errors.ruleWarning( |
| "target '" |
| + rule.getLabel() |
| + "' depends on deprecated target '" |
| + prerequisiteLabel |
| + "': " |
| + warning); |
| } |
| } |
| |
| /** Check that the dependency is not test-only, or the current rule is test-only. */ |
| private void validateDirectPrerequisiteForTestOnly( |
| RuleContext.Builder context, ConfiguredTargetAndData prerequisite) { |
| Rule rule = context.getRule(); |
| |
| if (rule.getRuleClassObject().getAdvertisedProviders().canHaveAnyProvider()) { |
| // testonly-ness will be checked directly between the depender and the target of the alias; |
| // getTarget() called by the depender will not return the alias rule, but its actual target |
| return; |
| } |
| if (!prerequisite.isTestOnly() || isTestOnlyRule(rule)) { |
| return; |
| } |
| |
| String message; |
| Label generatingRuleLabel = prerequisite.getGeneratingRuleLabel(); |
| if (generatingRuleLabel == null) { |
| message = |
| "non-test target '" |
| + rule.getLabel() |
| + "' depends on testonly " |
| + AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITHOUT_KIND) |
| + " and doesn't have testonly attribute set"; |
| } else if (context.getConfiguration().checkTestonlyForOutputFiles()) { |
| message = |
| "non-test target '" |
| + rule.getLabel() |
| + "' depends on the output file " |
| + AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITHOUT_KIND) |
| + " of a testonly rule " |
| + generatingRuleLabel |
| + " and doesn't have testonly attribute set"; |
| } else { |
| return; |
| } |
| |
| PackageIdentifier thisPackage = rule.getLabel().getPackageIdentifier(); |
| if (packageUnderExperimental(thisPackage)) { |
| context.ruleWarning(message); |
| } else { |
| context.ruleError(message); |
| } |
| } |
| |
| private static boolean isTestOnlyRule(Rule rule) { |
| NonconfigurableAttributeMapper mapper = NonconfigurableAttributeMapper.of(rule); |
| return mapper.has("testonly", Type.BOOLEAN) && mapper.get("testonly", Type.BOOLEAN); |
| } |
| } |