| // 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 com.google.errorprone.annotations.CanIgnoreReturnValue; | |
| 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}. | |
| * | |
| * @return true if all mandatory attributes were present, or false if any mandatory attribute was | |
| * missing a value. | |
| */ | |
| @CanIgnoreReturnValue | |
| <T> boolean populateRuleAttributeValues( | |
| RuleOrMacroInstance ruleOrMacroInstance, | |
| TargetDefinitionContext targetDefinitionContext, | |
| AttributeValues<T> attributeValues, | |
| boolean failOnUnknownAttributes, | |
| boolean isStarlark) | |
| throws InterruptedException, CannotPrecomputeDefaultsException { | |
| BitSet definedAttrIndices = | |
| populateDefinedRuleAttributeValues( | |
| ruleOrMacroInstance, attributeValues, failOnUnknownAttributes, targetDefinitionContext); | |
| boolean allMandatoryAttributesPresent = | |
| populateDefaultRuleAttributeValues( | |
| ruleOrMacroInstance, targetDefinitionContext, definedAttrIndices, isStarlark); | |
| // Now that all attributes are bound to values, collect and store configurable attribute keys. | |
| populateConfigDependenciesAttribute(ruleOrMacroInstance); | |
| return allMandatoryAttributesPresent; | |
| } | |
| /** | |
| * 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}. | |
| * | |
| * @return true if all mandatory attributes were present, or false if any mandatory attribute was | |
| * missing a value. | |
| */ | |
| private boolean 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. | |
| boolean allMandatoryAttributesPresent = true; | |
| 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); | |
| allMandatoryAttributesPresent = false; | |
| } | |
| // 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); | |
| } | |
| return allMandatoryAttributesPresent; | |
| } | |
| /** | |
| * 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); | |
| } | |
| } | |
| } | |
| } | |
| } |