// 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 com.google.devtools.build.lib.analysis.AliasProvider.TargetMode;
import com.google.devtools.build.lib.analysis.RuleContext.PrerequisiteValidator;
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.OutputFile;
import com.google.devtools.build.lib.packages.PackageGroup;
import com.google.devtools.build.lib.packages.RawAttributeMapper;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
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.packages.semantics.BuildLanguageOptions;
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 allowExperimentalDeps(RuleContext.Builder context);

  private void validateDirectPrerequisiteVisibility(
      RuleContext.Builder context, ConfiguredTargetAndData prerequisite, Attribute attribute) {
    String attrName = attribute.getName();
    Rule rule = context.getRule();
    Target prerequisiteTarget = prerequisite.getTarget();

    // We don't check the visibility of late-bound attributes, because it would break some
    // features.
    if (!isSameLogicalPackage(
            rule.getLabel().getPackageIdentifier(),
            AliasProvider.getDependencyLabel(prerequisite.getConfiguredTarget())
                .getPackageIdentifier())
        && !Attribute.isLateBound(attrName)) {
      // 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;
        }
      }

      // Determine if we should use the new visibility rules for tools.
      boolean toolCheckAtDefinition =
              context
                      .getStarlarkSemantics()
                      .getBool(
                              BuildLanguageOptions.INCOMPATIBLE_VISIBILITY_PRIVATE_ATTRIBUTES_AT_DEFINITION);

      if (!toolCheckAtDefinition
              || !attribute.isImplicit()
              || attribute.getName().equals(RuleClass.CONFIG_SETTING_DEPS_ATTRIBUTE)
              || !context.isStarlarkRuleOrAspect()) {
        // Default check: The attribute must be visible from the target.
        if (!context.isVisible(prerequisite.getConfiguredTarget())) {
          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
            && !RuleContext.isVisible(implicitDefinition, prerequisite.getConfiguredTarget())
            && !context.isVisible(prerequisite.getConfiguredTarget())) {
          // In the error message, always suggest making the prerequisite visible from the definition,
          // not the target.
          handleVisibilityConflict(context, prerequisite, implicitDefinition);
        }
      }
    }

    if (prerequisiteTarget instanceof PackageGroup) {
      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)
          && !attrName.equals(FunctionSplitTransitionAllowlist.LEGACY_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 {
      String errorMessage =
          String.format(
              "%s is not visible from target '%s'. Check "
                  + "the visibility declaration of the former target if you think "
                  + "the dependency is legitimate",
              AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITHOUT_KIND), rule);

      if (prerequisite.getTarget().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();
    Target prerequisiteTarget = prerequisite.getTarget();
    Label prerequisiteLabel = prerequisiteTarget.getLabel();

    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)");
      }
    }
  }

  /** Returns whether a deprecation warning should be printed for the prerequisite described. */
  private boolean shouldEmitDeprecationWarningFor(
      String thisDeprecation,
      PackageIdentifier thisPackage,
      String prerequisiteDeprecation,
      PackageIdentifier prerequisitePackage,
      boolean forAspect) {
    // Don't report deprecation edges from javatests to java or within a package;
    // otherwise tests of deprecated code generate nuisance warnings.
    // Don't report deprecation if the current target is also deprecated,
    // or if the current context is evaluating an aspect,
    // as the base target would have already printed the deprecation warnings.
    return (!forAspect
        && prerequisiteDeprecation != null
        && !isSameLogicalPackage(thisPackage, prerequisitePackage)
        && thisDeprecation == null);
  }

  /** Checks if the given prerequisite is deprecated and prints a warning if so. */
  private void validateDirectPrerequisiteForDeprecation(
      RuleErrorConsumer errors,
      Rule rule,
      ConfiguredTargetAndData prerequisite,
      boolean forAspect) {
    Target prerequisiteTarget = prerequisite.getTarget();
    Label prerequisiteLabel = prerequisiteTarget.getLabel();
    PackageIdentifier thatPackage = prerequisiteLabel.getPackageIdentifier();
    PackageIdentifier thisPackage = rule.getLabel().getPackageIdentifier();

    if (prerequisiteTarget instanceof Rule) {
      Rule prerequisiteRule = (Rule) prerequisiteTarget;
      String thisDeprecation =
          NonconfigurableAttributeMapper.of(rule).has("deprecation", Type.STRING)
              ? NonconfigurableAttributeMapper.of(rule).get("deprecation", Type.STRING)
              : null;
      String thatDeprecation =
          NonconfigurableAttributeMapper.of(prerequisiteRule).has("deprecation", Type.STRING)
              ? NonconfigurableAttributeMapper.of(prerequisiteRule).get("deprecation", Type.STRING)
              : null;
      if (shouldEmitDeprecationWarningFor(
          thisDeprecation, thisPackage, thatDeprecation, thatPackage, forAspect)) {
        errors.ruleWarning(
            "target '"
                + rule.getLabel()
                + "' depends on deprecated target '"
                + prerequisiteLabel
                + "': "
                + thatDeprecation);
      }
    }

    if (prerequisiteTarget instanceof OutputFile) {
      Rule generatingRule = ((OutputFile) prerequisiteTarget).getGeneratingRule();
      String thisDeprecation =
          NonconfigurableAttributeMapper.of(rule).get("deprecation", Type.STRING);
      String thatDeprecation =
          NonconfigurableAttributeMapper.of(generatingRule).get("deprecation", Type.STRING);
      if (shouldEmitDeprecationWarningFor(
          thisDeprecation, thisPackage, thatDeprecation, thatPackage, forAspect)) {
        errors.ruleWarning(
            "target '"
                + rule.getLabel()
                + "' depends on the output file "
                + prerequisiteLabel
                + " of a deprecated rule "
                + generatingRule.getLabel()
                + "': "
                + thatDeprecation);
      }
    }
  }

  /** 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;
    }

    Target prerequisiteTarget = prerequisite.getTarget();
    PackageIdentifier thisPackage = rule.getLabel().getPackageIdentifier();

    String message = null;
    if (prerequisiteTarget instanceof Rule) {
      Rule prerequisiteRule = (Rule) prerequisiteTarget;
      if (isTestOnlyRule(prerequisiteRule) && !isTestOnlyRule(rule)) {
        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()
        && prerequisiteTarget instanceof OutputFile) {
      Rule generatingRule = ((OutputFile) prerequisiteTarget).getGeneratingRule();
      if (isTestOnlyRule(generatingRule) && !isTestOnlyRule(rule)) {
        message =
            "non-test target '"
                + rule.getLabel()
                + "' depends on the output file "
                + AliasProvider.describeTargetWithAliases(prerequisite, TargetMode.WITHOUT_KIND)
                + " of a testonly rule "
                + generatingRule.getLabel()
                + " and doesn't have testonly attribute set";
      }
    }
    if (message != null) {
      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);
  }
}
