// Copyright 2025 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.packages;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Interner;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
import com.google.devtools.build.lib.packages.Attribute.StarlarkComputedDefaultTemplate;
import com.google.devtools.build.lib.packages.Attribute.StarlarkComputedDefaultTemplate.CannotPrecomputeDefaultsException;
import com.google.devtools.build.lib.packages.RuleFactory.AttributeValues;
import com.google.devtools.build.lib.packages.Type.ConversionException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;
import net.starlark.java.spelling.SpellChecker;

/** Provides access to the attributes of a rule or macro class. */
public class AttributeProvider {
  /**
   * A (unordered) mapping from attribute names to small integers indexing into the {@code
   * attributes} array.
   */
  private final Map<String, Integer> attributeIndex;

  /**
   * All attributes of this rule or macro class (including inherited ones) ordered by attributeIndex
   * value.
   */
  private final ImmutableList<Attribute> attributes;

  /**
   * Names of the non-configurable attributes of this rule or macro class. This is null for macros
   * because it isn't used.
   */
  @Nullable private final ImmutableList<String> nonConfigurableAttributes;

  /* The name of the rule or macro class that owns these attributes. */
  private final String owner;

  private final boolean ignoreLicenses;

  AttributeProvider(
      ImmutableList<Attribute> attributes,
      Map<String, Integer> attributeIndex,
      @Nullable ImmutableList<String> nonConfigurableAttributes,
      String owner,
      boolean ignoreLicenses) {
    this.attributes = attributes;
    this.attributeIndex = attributeIndex;
    this.nonConfigurableAttributes = nonConfigurableAttributes;
    this.owner = owner;
    this.ignoreLicenses = ignoreLicenses;
  }

  @Override
  public String toString() {
    return owner;
  }

  /**
   * If true, no rule of this class ever declares a license regardless of what the rule's or
   * package's <code>licenses</code> attribute says.
   *
   * <p>This is useful for rule types that don't make sense for license checking.
   */
  boolean ignoreLicenses() {
    return ignoreLicenses;
  }

  /**
   * Returns true iff the attribute 'attrName' is defined for this rule or macro class, and has type
   * 'type'.
   */
  public boolean hasAttr(String attrName, Type<?> type) {
    Integer index = getAttributeIndex(attrName);
    return index != null && getAttribute(index).getType() == type;
  }

  /**
   * Returns the index of the specified attribute name. Use of indices allows space-efficient
   * storage of attribute values in rules or macros, since hashtables are not required. (The index
   * mapping is specific to each RuleClass and an attribute may have a different index in the parent
   * RuleClass.)
   *
   * <p>Returns null if the named attribute is not defined for this class of rule or macro.
   */
  public Integer getAttributeIndex(String attrName) {
    return attributeIndex.get(attrName);
  }

  /** Returns the attribute whose index is 'attrIndex'. Fails if attrIndex is not in range. */
  public Attribute getAttribute(int attrIndex) {
    return attributes.get(attrIndex);
  }

  /**
   * Returns the attribute whose name is 'attrName'; fails with NullPointerException if not found.
   */
  public Attribute getAttributeByName(String attrName) {
    Integer attrIndex =
        Preconditions.checkNotNull(
            getAttributeIndex(attrName), "Attribute %s does not exist", attrName);
    return attributes.get(attrIndex);
  }

  /** Returns the attribute whose name is {@code attrName}, or null if not found. */
  @Nullable
  public Attribute getAttributeByNameMaybe(String attrName) {
    Integer i = getAttributeIndex(attrName);
    return i == null ? null : attributes.get(i);
  }

  /** Returns the number of attributes defined for this rule or macro class. */
  public int getAttributeCount() {
    return attributeIndex.size();
  }

  /**
   * Returns an (immutable) list of all Attributes defined for this class of rule or macro, ordered
   * by increasing index.
   */
  public ImmutableList<Attribute> getAttributes() {
    return attributes;
  }

  /**
   * Returns set of non-configurable attribute names defined for this class of rule. null for macros
   * to save memory, since this field is never read for macros.
   */
  @Nullable
  public List<String> getNonConfigurableAttributes() {
    return nonConfigurableAttributes;
  }

  /**
   * Populates the attributes table of the new {@link RuleOrMacroInstance} with the values in the
   * {@code attributeValues} map and with default values provided by this {@link AttributeProvider}
   * and the {@code pkgBuilder}.
   *
   * <p>Errors are reported on {@code eventHandler}.
   */
  <T> void populateRuleAttributeValues(
      RuleOrMacroInstance ruleOrMacroInstance,
      TargetDefinitionContext targetDefinitionContext,
      AttributeValues<T> attributeValues,
      boolean failOnUnknownAttributes,
      boolean isStarlark)
      throws InterruptedException, CannotPrecomputeDefaultsException {

    BitSet definedAttrIndices =
        populateDefinedRuleAttributeValues(
            ruleOrMacroInstance, attributeValues, failOnUnknownAttributes, targetDefinitionContext);
    populateDefaultRuleAttributeValues(
        ruleOrMacroInstance, targetDefinitionContext, definedAttrIndices, isStarlark);
    // Now that all attributes are bound to values, collect and store configurable attribute keys.
    populateConfigDependenciesAttribute(ruleOrMacroInstance);
  }

  /**
   * Populates the attributes table of the new {@link RuleOrMacroInstance} with the values in the
   * {@code attributeValues} map.
   *
   * <p>Handles the special cases of the attribute named {@code "name"} and attributes with value
   * {@link Starlark#NONE}.
   *
   * <p>Returns a bitset {@code b} where {@code b.get(i)} is {@code true} if this method set a value
   * for the attribute with index {@code i} in this {@link AttributeProvider}. Errors are reported
   * on {@code eventHandler}.
   */
  private <T> BitSet populateDefinedRuleAttributeValues(
      RuleOrMacroInstance ruleOrMacroInstance,
      AttributeValues<T> attributeValues,
      boolean failOnUnknownAttributes,
      TargetDefinitionContext targetDefinitionContext) {
    LabelConverter labelConverter = targetDefinitionContext.getLabelConverter();
    Interner<ImmutableList<?>> listInterner = targetDefinitionContext.getListInterner();
    boolean simplifyUnconditionalSelects =
        targetDefinitionContext.simplifyUnconditionalSelectsInRuleAttrs();
    BitSet definedAttrIndices = new BitSet();
    for (T attributeAccessor : attributeValues.getAttributeAccessors()) {
      String attributeName = attributeValues.getName(attributeAccessor);
      Object attributeValue = attributeValues.getValue(attributeAccessor);
      // Ignore all None values.
      if (attributeValue == Starlark.NONE && !failOnUnknownAttributes) {
        continue;
      }

      // If the user sets "applicable_liceneses", change it to the correct name.
      // TODO(aiuto): In the time frame of Bazel 9, remove this alternate spelling.
      if (attributeName.equals(RuleClass.APPLICABLE_METADATA_ATTR_ALT)) {
        attributeName = RuleClass.APPLICABLE_METADATA_ATTR;
      }

      // Check that the attribute's name belongs to a valid attribute for this rule or macro class.
      Integer attrIndex = getAttributeIndex(attributeName);
      if (attrIndex == null) {
        ruleOrMacroInstance.reportError(
            String.format(
                "%s: no such attribute '%s' in '%s' %s%s",
                ruleOrMacroInstance.getLabel(),
                attributeName,
                owner,
                ruleOrMacroInstance.isRuleInstance() ? "rule" : "macro",
                SpellChecker.didYouMean(
                    attributeName,
                    ruleOrMacroInstance.getAttributes().stream()
                        .filter(Attribute::isDocumented)
                        .map(Attribute::getName)
                        .collect(ImmutableList.toImmutableList()))),
            targetDefinitionContext);
        continue;
      }
      // Ignore all None values (after reporting an error)
      if (attributeValue == Starlark.NONE) {
        continue;
      }

      Attribute attr = getAttribute(attrIndex);

      if (attributeName.equals("licenses") && ignoreLicenses) {
        ruleOrMacroInstance.setAttributeValue(attr, License.NO_LICENSE, /* explicit= */ false);
        definedAttrIndices.set(attrIndex);
        continue;
      }

      // Convert the build-lang value to a native value, if necessary.
      Object nativeAttributeValue;
      if (attributeValues.valuesAreBuildLanguageTyped()) {
        try {
          nativeAttributeValue =
              BuildType.convertFromBuildLangType(
                  ruleOrMacroInstance.getAttributeProvider().toString(),
                  attr,
                  attributeValue,
                  labelConverter,
                  listInterner,
                  simplifyUnconditionalSelects);
        } catch (ConversionException e) {
          ruleOrMacroInstance.reportError(
              String.format("%s: %s", ruleOrMacroInstance.getLabel(), e.getMessage()),
              targetDefinitionContext);
          continue;
        }
        // Ignore select({"//conditions:default": None}) values for attr types with null default.
        if (nativeAttributeValue == null) {
          continue;
        }
      } else {
        nativeAttributeValue = attributeValue;
      }

      if (attr.getName().equals("visibility")) {
        @SuppressWarnings("unchecked")
        List<Label> vis = (List<Label>) nativeAttributeValue;
        try {
          nativeAttributeValue = RuleVisibility.validateAndSimplify(vis);
        } catch (EvalException e) {
          ruleOrMacroInstance.reportError(
              ruleOrMacroInstance.getLabel() + " " + e.getMessage(), targetDefinitionContext);
        }
      }

      boolean explicit = attributeValues.isExplicitlySpecified(attributeAccessor);
      ruleOrMacroInstance.setAttributeValue(attr, nativeAttributeValue, explicit);
      checkAllowedValues(ruleOrMacroInstance, attr, targetDefinitionContext);
      definedAttrIndices.set(attrIndex);
    }
    return definedAttrIndices;
  }

  /**
   * Populates the attributes table of the new {@link RuleOrMacroInstance} with default values
   * provided by this {@link AttributeProvider} and the {@code pkgBuilder}. This will only provide
   * values for attributes that haven't already been populated, using {@code definedAttrIndices} to
   * determine whether an attribute was populated.
   *
   * <p>Errors are reported on {@code eventHandler}.
   */
  private void populateDefaultRuleAttributeValues(
      RuleOrMacroInstance ruleOrMacroInstance,
      TargetDefinitionContext targetDefinitionContext,
      BitSet definedAttrIndices,
      boolean isStarlark)
      throws InterruptedException, CannotPrecomputeDefaultsException {
    // Set defaults; ensure that every mandatory attribute has a value. Use the default if none
    // is specified.
    List<Attribute> attrsWithComputedDefaults = new ArrayList<>();
    int numAttributes = getAttributeCount();
    for (int attrIndex = 0; attrIndex < numAttributes; ++attrIndex) {
      if (definedAttrIndices.get(attrIndex)) {
        continue;
      }
      Attribute attr = getAttribute(attrIndex);
      if (attr.isMandatory()) {
        ruleOrMacroInstance.reportError(
            String.format(
                "%s: missing value for mandatory attribute '%s' in '%s' %s",
                ruleOrMacroInstance.getLabel(),
                attr.getName(),
                owner,
                ruleOrMacroInstance.isRuleInstance() ? "rule" : "macro"),
            targetDefinitionContext);
      }

      // Macros don't have computed defaults or special logic for licenses or distributions.
      if (ruleOrMacroInstance instanceof Rule ruleInstance) {
        // We must check both the name and the type of each attribute below in case a Starlark rule
        // defines a licenses or distributions attribute of another type.

        if (attr.hasComputedDefault()) {
          // Note that it is necessary to set all non-computed default values before calling
          // Attribute#getDefaultValue for computed default attributes. Computed default attributes
          // may have a condition predicate (i.e. the predicate returned by Attribute#getCondition)
          // that depends on non-computed default attribute values, and that condition predicate is
          // evaluated by the call to Attribute#getDefaultValue.
          attrsWithComputedDefaults.add(attr);

        } else if (attr.isLateBound()) {
          ruleInstance.setAttributeValue(attr, attr.getLateBoundDefault(), /* explicit= */ false);
        } else if (attr.isMaterializing()) {
          ruleInstance.setAttributeValue(attr, attr.getMaterializer(), false);
        } else if (attr.getName().equals(RuleClass.APPLICABLE_METADATA_ATTR)
            && attr.getType() == BuildType.LABEL_LIST) {
          // The check here is preventing against a corner case where the license()/package_info()
          // rule can get itself as applicable_metadata. This breaks the graph because there is now
          // a self-edge.
          //
          // There are two ways that I can see to resolve this. The first, what is shown here,
          // simply prunes the attribute if the source is a new-style license/metadata rule, based
          // on what's been provided publicly. This does create a tight coupling to the
          // implementation, but this is unavoidable since licenses are no longer a first-class type
          // but we want first class behavior in Bazel core.
          //
          // A different approach that would not depend on the implementation of the rule could
          // filter the list of default_applicable_metadata and not include the metadata rule if it
          // matches the name of the current rule. This obviously fixes the self-assignment rule,
          // but the resulting graph is semantically strange. The interpretation of the graph would
          // be that the metadata rule is subject to the metadata of the *other* default metadata,
          // but not itself. That looks very odd, and it's not semantically accurate.
          // As an alternate, if the self-edge is detected, why not simply drop all the
          // default_applicable_metadata attributes and avoid this oddness? That would work and
          // fix the self-edge problem, but for nodes that don't have the self-edge problem, they
          // would get all default_applicable_metadata and now the graph is inconsistent in that
          // license() rules have applicable_metadata while others do not.
          if (ruleInstance.getRuleClassObject().isPackageMetadataRule()) {
            ruleInstance.setAttributeValue(attr, ImmutableList.of(), /* explicit= */ false);
          }

        } else if (attr.getName().equals("licenses") && attr.getType() == BuildType.LICENSE) {
          ruleInstance.setAttributeValue(
              attr,
              ignoreLicenses
                  ? License.NO_LICENSE
                  : targetDefinitionContext.getPartialPackageArgs().license(),
              /* explicit= */ false);
        }
        // Don't store default values, querying materializes them at read time.
      }
    }
    // An instance of the built-in 'test_suite' rule with an undefined or empty 'tests' attribute
    // attribute gets an '$implicit_tests' attribute, whose value is a shared per-package list
    // of all test labels, populated later.
    // TODO(blaze-rules-team): This should be in test_suite's implementation, not
    // here.
    if (owner.equals("test_suite") && !isStarlark) {
      Attribute implicitTests = this.getAttributeByName("$implicit_tests");
      NonconfigurableAttributeMapper attributeMapper =
          NonconfigurableAttributeMapper.of(ruleOrMacroInstance);
      if (implicitTests != null && attributeMapper.get("tests", BuildType.LABEL_LIST).isEmpty()) {
        boolean explicit = true; // so that it appears in query output
        ruleOrMacroInstance.setAttributeValue(
            implicitTests,
            targetDefinitionContext.getTestSuiteImplicitTestsRef(
                attributeMapper.get("tags", Types.STRING_LIST)),
            explicit);
      }
    }
    // Set computed default attribute values now that all other (i.e. non-computed) default values
    // have been set. Macros won't hit this because they don't have attrs with computed defaults.
    for (Attribute attr : attrsWithComputedDefaults) {
      // If Attribute#hasComputedDefault was true above, Attribute#getDefaultValue returns the
      // computed default function object or a Starlark computed default template. Note that we
      // cannot determine the exact value of the computed default function here because it may
      // depend on other attribute values that are configurable (i.e. they came from select({..})
      // expressions in the build language, and they require configuration data from the analysis
      // phase to be resolved). Instead, we're setting the attribute value to a reference to the
      // computed default function, or if #getDefaultValue is a Starlark computed default
      // template, setting the attribute value to a reference to the StarlarkComputedDefault
      // returned from StarlarkComputedDefaultTemplate#computePossibleValues.
      //
      // StarlarkComputedDefaultTemplate#computePossibleValues pre-computes all possible values the
      // function may evaluate to, and records them in a lookup table. By calling it here, with an
      // EventHandler, any errors that might occur during the function's evaluation can
      // be discovered and propagated here.
      Object valueToSet;
      Object defaultValue = attr.getDefaultValue(null);
      if (defaultValue instanceof StarlarkComputedDefaultTemplate template) {
        valueToSet =
            template.computePossibleValues(attr, ruleOrMacroInstance, targetDefinitionContext);
      } else if (defaultValue instanceof ComputedDefault computedDefault) {
        // Compute all possible values to verify that the ComputedDefault is well-defined. This
        // was previously done implicitly as part of visiting all labels to check for null-ness in
        // Rule.checkForNullLabels, but that was changed to skip non-label attributes to improve
        // performance.
        // TODO: b/287492305 - This is technically an illegal call to getPossibleValues as the
        // package has not yet finished loading. Do we even need this still?
        var unused = computedDefault.getPossibleValues(attr.getType(), ruleOrMacroInstance);
        valueToSet = defaultValue;
      } else {
        valueToSet = defaultValue;
      }
      ruleOrMacroInstance.setAttributeValue(attr, valueToSet, /* explicit= */ false);
    }
  }

  /**
   * Collects all labels used as keys for configurable attributes and places them into the special
   * implicit attribute that tracks them.
   */
  private static void populateConfigDependenciesAttribute(RuleOrMacroInstance ruleOrMacroInstance) {
    RawAttributeMapper attributes = RawAttributeMapper.of(ruleOrMacroInstance);
    Attribute configDepsAttribute =
        attributes.getAttributeDefinition(RuleClass.CONFIG_SETTING_DEPS_ATTRIBUTE);
    if (configDepsAttribute == null) {
      return;
    }

    LinkedHashSet<Label> configLabels = new LinkedHashSet<>();
    for (Attribute attr : ruleOrMacroInstance.getAttributeProvider().getAttributes()) {
      BuildType.SelectorList<?> selectorList =
          attributes.getSelectorList(attr.getName(), attr.getType());
      if (selectorList != null) {
        configLabels.addAll(selectorList.getKeyLabels());
      }
    }

    ruleOrMacroInstance.setAttributeValue(
        configDepsAttribute, ImmutableList.copyOf(configLabels), /* explicit= */ false);
  }

  /**
   * Verifies that the {@link RuleOrMacroInstance} has a valid value for the attribute according to
   * its allowed values.
   *
   * <p>If the value for the given attribute on the given {@link RuleOrMacroInstance} is invalid, an
   * error will be recorded in the given EventHandler.
   *
   * <p>If the {@code attribute} is configurable, all of its potential values are evaluated, and
   * errors for each of the invalid values are reported.
   */
  private static void checkAllowedValues(
      RuleOrMacroInstance ruleOrMacroInstance,
      Attribute attribute,
      TargetDefinitionContext targetDefinitionContext) {
    if (attribute.checkAllowedValues()) {
      PredicateWithMessage<Object> allowedValues = attribute.getAllowedValues();
      Iterable<?> values =
          AggregatingAttributeMapper.of(ruleOrMacroInstance)
              .visitAttribute(attribute.getName(), attribute.getType());
      for (Object value : values) {
        if (!allowedValues.apply(value)) {
          ruleOrMacroInstance.reportError(
              String.format(
                  "%s: invalid value in '%s' attribute: %s",
                  ruleOrMacroInstance.getLabel(),
                  attribute.getName(),
                  allowedValues.getErrorReason(value)),
              targetDefinitionContext);
        }
      }
    }
  }
}
