blob: 12a20e43951cbba4efab7abcef13c77ddc39f6dd [file] [log] [blame]
// 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.events.EventHandler;
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,
targetDefinitionContext.getLabelConverter(),
attributeValues,
failOnUnknownAttributes,
targetDefinitionContext.getListInterner(),
targetDefinitionContext.getLocalEventHandler(),
targetDefinitionContext.simplifyUnconditionalSelectsInRuleAttrs());
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,
LabelConverter labelConverter,
AttributeValues<T> attributeValues,
boolean failOnUnknownAttributes,
Interner<ImmutableList<?>> listInterner,
EventHandler eventHandler,
boolean simplifyUnconditionalSelects) {
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()))),
eventHandler);
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()),
eventHandler);
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(), eventHandler);
}
}
boolean explicit = attributeValues.isExplicitlySpecified(attributeAccessor);
ruleOrMacroInstance.setAttributeValue(attr, nativeAttributeValue, explicit);
checkAllowedValues(ruleOrMacroInstance, attr, eventHandler);
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.getLocalEventHandler());
}
// 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.getLocalEventHandler());
} 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, EventHandler eventHandler) {
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)),
eventHandler);
}
}
}
}
}