blob: 9d355d50429d308093be0cb812e03ba20dbae1ec [file] [log] [blame]
// Copyright 2021 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 static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Objects;
import javax.annotation.Nullable;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;
/**
* AspectsList represents the list of aspects specified via --aspects command line option or
* declared in attribute aspects list. The class is responsible for wrapping the information
* necessary for constructing those aspects.
*/
public final class AspectsList {
private final ImmutableList<AspectDetails<?>> aspects;
private AspectsList(ImmutableList<AspectDetails<?>> aspects) {
this.aspects = aspects;
}
public boolean hasAspects() {
return !aspects.isEmpty();
}
/** Returns the list of aspects required for dependencies through this attribute. */
public ImmutableList<Aspect> getAspects(Rule rule) {
if (aspects.isEmpty()) {
return ImmutableList.of();
}
ImmutableList.Builder<Aspect> builder = null;
for (AspectDetails<?> aspect : aspects) {
Aspect a = aspect.getAspect(rule);
if (a != null) {
if (builder == null) {
builder = ImmutableList.builder();
}
builder.add(a);
}
}
return builder == null ? ImmutableList.of() : builder.build();
}
public ImmutableList<AspectClass> getAspectClasses() {
ImmutableList.Builder<AspectClass> result = ImmutableList.builder();
for (AspectDetails<?> aspect : aspects) {
result.add(aspect.getAspectClass());
}
return result.build();
}
/** Returns a list of Aspect objects for top level aspects. */
public ImmutableList<Aspect> buildAspects(ImmutableMap<String, String> aspectsParameters)
throws EvalException {
Preconditions.checkArgument(aspectsParameters != null, "aspectsParameters cannot be null");
ImmutableList.Builder<Aspect> aspectsList = ImmutableList.builder();
for (AspectDetails<?> aspect : aspects) {
aspectsList.add(aspect.getTopLevelAspect(aspectsParameters));
}
return aspectsList.build();
}
public void validateRulePropagatedAspectsParameters(RuleClass ruleClass) throws EvalException {
for (AspectDetails<?> aspect : aspects) {
ImmutableSet<String> requiredAspectParameters = aspect.getRequiredParameters();
for (Attribute aspectAttribute : aspect.getAspectAttributes()) {
String aspectAttrName = aspectAttribute.getPublicName();
Type<?> aspectAttrType = aspectAttribute.getType();
// When propagated from a rule, explicit aspect attributes must be of type boolean, int
// or string. Integer and string attributes must have the `values` restriction.
if (!aspectAttribute.isImplicit() && !aspectAttribute.isLateBound()) {
if (aspectAttrType != Type.BOOLEAN && !aspectAttribute.checkAllowedValues()) {
throw Starlark.errorf(
"Aspect %s: Aspect parameter attribute '%s' must use the 'values' restriction.",
aspect.getName(), aspectAttrName);
}
}
// Required aspect parameters must be specified by the rule propagating the aspect with
// the same parameter type.
if (requiredAspectParameters.contains(aspectAttrName)) {
if (!ruleClass.hasAttr(aspectAttrName, aspectAttrType)) {
throw Starlark.errorf(
"Aspect %s requires rule %s to specify attribute '%s' with type %s.",
aspect.getName(), ruleClass.getName(), aspectAttrName, aspectAttrType);
}
}
}
}
}
/**
* Validates top-level aspects parameters and reports error in the following cases:
*
* <p>If a parameter name is specified in command line but no aspect has a parameter with that
* name.
*
* <p>If a mandatory aspect attribute is not given a value in the top-level parameters list.
*/
public void validateTopLevelAspectsParameters(ImmutableMap<String, String> aspectsParameters)
throws EvalException {
Preconditions.checkArgument(aspectsParameters != null, "aspectsParameters cannot be null");
ImmutableSet.Builder<String> usedParametersBuilder = ImmutableSet.builder();
for (AspectDetails<?> aspectDetails : aspects) {
if (aspectDetails instanceof StarlarkAspectDetails) {
ImmutableList<Attribute> aspectAttributes =
((StarlarkAspectDetails) aspectDetails).aspect.getAttributes();
for (Attribute attr : aspectAttributes) {
if (attr.isImplicit() || attr.isLateBound()) {
continue;
}
String attrName = attr.getName();
if (aspectsParameters.containsKey(attrName)) {
usedParametersBuilder.add(attrName);
} else if (attr.isMandatory()) {
throw Starlark.errorf(
"Missing mandatory attribute '%s' for aspect '%s'.",
attrName, aspectDetails.getName());
}
}
}
}
ImmutableSet<String> usedParameters = usedParametersBuilder.build();
ImmutableList<String> unusedParameters =
aspectsParameters.keySet().stream()
.filter(p -> !usedParameters.contains(p))
.collect(toImmutableList());
if (!unusedParameters.isEmpty()) {
throw Starlark.errorf(
"Parameters '%s' are not parameters of any of the top-level aspects but they are"
+ " specified in --aspects_parameters.",
unusedParameters);
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AspectsList aspectsList = (AspectsList) o;
return Objects.equals(aspects, aspectsList.aspects);
}
@Override
public int hashCode() {
return aspects.hashCode();
}
/** Wraps the information necessary to construct an Aspect. */
private abstract static class AspectDetails<C extends AspectClass> {
final C aspectClass;
final Function<Rule, AspectParameters> parametersExtractor;
final String requiredByAspect;
private AspectDetails(C aspectClass, Function<Rule, AspectParameters> parametersExtractor) {
this.aspectClass = aspectClass;
this.parametersExtractor = parametersExtractor;
this.requiredByAspect = null;
}
private AspectDetails(
C aspectClass,
Function<Rule, AspectParameters> parametersExtractor,
String requiredByAspect) {
this.aspectClass = aspectClass;
this.parametersExtractor = parametersExtractor;
this.requiredByAspect = requiredByAspect;
}
public String getName() {
return this.aspectClass.getName();
}
public ImmutableSet<String> getRequiredParameters() {
return ImmutableSet.of();
}
public ImmutableList<Attribute> getAspectAttributes() {
return ImmutableList.of();
}
protected abstract Aspect getAspect(Rule rule);
protected abstract Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters)
throws EvalException;
C getAspectClass() {
return aspectClass;
}
}
private static class NativeAspectDetails extends AspectDetails<NativeAspectClass> {
NativeAspectDetails(
NativeAspectClass aspectClass, Function<Rule, AspectParameters> parametersExtractor) {
super(aspectClass, parametersExtractor);
}
NativeAspectDetails(
NativeAspectClass aspectClass,
Function<Rule, AspectParameters> parametersExtractor,
String requiredByAspect) {
super(aspectClass, parametersExtractor, requiredByAspect);
}
@Nullable
@Override
public Aspect getAspect(Rule rule) {
AspectParameters params = parametersExtractor.apply(rule);
return params == null ? null : Aspect.forNative(aspectClass, params);
}
@Override
protected Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters)
throws EvalException {
// Native aspects ignore their top-level parameters values for now.
return Aspect.forNative(aspectClass, AspectParameters.EMPTY);
}
}
private static class StarlarkAspectDetails extends AspectDetails<StarlarkAspectClass> {
private final StarlarkDefinedAspect aspect;
private StarlarkAspectDetails(StarlarkDefinedAspect aspect, String requiredByAspect) {
super(aspect.getAspectClass(), aspect.getDefaultParametersExtractor(), requiredByAspect);
this.aspect = aspect;
}
@Override
public ImmutableSet<String> getRequiredParameters() {
return aspect.getParamAttributes();
}
@Override
public ImmutableList<Attribute> getAspectAttributes() {
return aspect.getAttributes();
}
@Override
public Aspect getAspect(Rule rule) {
AspectParameters params = parametersExtractor.apply(rule);
return Aspect.forStarlark(aspectClass, aspect.getDefinition(params), params);
}
@Override
public Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters)
throws EvalException {
AspectParameters params = aspect.extractTopLevelParameters(aspectParameters);
return Aspect.forStarlark(aspectClass, aspect.getDefinition(params), params);
}
}
/** Aspect details that just wrap a pre-existing Aspect that doesn't vary with the Rule. */
private static class PredefinedAspectDetails extends AspectDetails<AspectClass> {
private final Aspect aspect;
PredefinedAspectDetails(Aspect aspect) {
super(aspect.getAspectClass(), null);
this.aspect = aspect;
}
@Override
public Aspect getAspect(Rule rule) {
return aspect;
}
@Override
public Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters)
throws EvalException {
return aspect;
}
}
@SerializationConstant @VisibleForSerialization
static final Function<Rule, AspectParameters> EMPTY_FUNCTION = input -> AspectParameters.EMPTY;
/** A builder for AspectsList */
public static class Builder {
private final HashMap<String, AspectDetails<?>> aspects = new LinkedHashMap<>();
public Builder() {}
public Builder(AspectsList aspectsList) {
for (AspectDetails<?> aspect : aspectsList.aspects) {
aspects.put(aspect.getName(), aspect);
}
}
public AspectsList build() {
return new AspectsList(ImmutableList.copyOf(aspects.values()));
}
/**
* Adds a native aspect with its parameters extraction function to the aspects list.
*
* @param aspect the native aspect to be added
* @param evaluator function that extracts aspect parameters from rule.
*/
public void addAspect(NativeAspectClass aspect, Function<Rule, AspectParameters> evaluator) {
NativeAspectDetails nativeAspectDetails = new NativeAspectDetails(aspect, evaluator);
AspectDetails<?> oldAspect =
this.aspects.put(nativeAspectDetails.getName(), nativeAspectDetails);
if (oldAspect != null) {
throw new AssertionError(
String.format("Aspect %s has already been added", oldAspect.getName()));
}
}
/**
* Adds a native aspect that does not need a parameters extractor to the aspects list.
*
* @param aspect the native aspect to be added
*/
public void addAspect(NativeAspectClass aspect) {
addAspect(aspect, EMPTY_FUNCTION);
}
/** Attaches this aspect and its required aspects */
public void addAspect(StarlarkAspect starlarkAspect) throws EvalException {
addAspect(starlarkAspect, null);
}
private void addAspect(StarlarkAspect starlarkAspect, @Nullable String requiredByAspect)
throws EvalException {
if (starlarkAspect instanceof StarlarkDefinedAspect) {
StarlarkDefinedAspect starlarkDefinedAspect = (StarlarkDefinedAspect) starlarkAspect;
if (!starlarkDefinedAspect.isExported()) {
throw Starlark.errorf(
"Aspects should be top-level values in extension files that define them.");
}
for (StarlarkAspect requiredAspect : starlarkDefinedAspect.getRequiredAspects()) {
addAspect(requiredAspect, starlarkDefinedAspect.getName());
}
}
boolean needsToAdd = needsToBeAdded(starlarkAspect.getName(), requiredByAspect);
if (needsToAdd) {
final AspectDetails<?> aspectDetails;
if (starlarkAspect instanceof StarlarkDefinedAspect) {
aspectDetails =
new StarlarkAspectDetails((StarlarkDefinedAspect) starlarkAspect, requiredByAspect);
} else if (starlarkAspect instanceof StarlarkNativeAspect) {
aspectDetails =
new NativeAspectDetails(
(StarlarkNativeAspect) starlarkAspect,
starlarkAspect.getDefaultParametersExtractor(),
requiredByAspect);
} else {
throw new IllegalArgumentException();
}
this.aspects.put(starlarkAspect.getName(), aspectDetails);
}
}
/** Should only be used for deserialization. */
public void addAspect(final Aspect aspect) {
PredefinedAspectDetails predefinedAspectDetails = new PredefinedAspectDetails(aspect);
AspectDetails<?> oldAspect =
this.aspects.put(predefinedAspectDetails.getName(), predefinedAspectDetails);
if (oldAspect != null) {
throw new AssertionError(
String.format("Aspect %s has already been added", oldAspect.getName()));
}
}
/**
* Adds all aspect from the list.
*
* <p>The function is intended for extended Starlark rules, where aspect list is already built
* and may include aspects required by other aspects.
*/
public void addAspects(AspectsList aspectsList) throws EvalException {
for (AspectDetails<?> aspect : aspectsList.aspects) {
boolean needsToAdd = needsToBeAdded(aspect.getName(), aspect.requiredByAspect);
if (needsToAdd) {
aspects.put(aspect.getName(), aspect);
}
}
}
private boolean needsToBeAdded(String aspectName, @Nullable String requiredByAspect)
throws EvalException {
AspectDetails<?> oldAspect = this.aspects.get(aspectName);
if (oldAspect != null) {
if (requiredByAspect != null) {
// If the aspect to be added already exists and it is required by another aspect, no need
// to
// add it again.
return false;
} else {
// If the aspect to be added is not required by another aspect, then we should throw error
String oldAspectBaseAspectName = oldAspect.requiredByAspect;
if (oldAspectBaseAspectName != null) {
throw Starlark.errorf(
"aspect %s was added before as a required aspect of aspect %s",
oldAspect.getName(), oldAspectBaseAspectName);
}
throw Starlark.errorf("aspect %s added more than once", oldAspect.getName());
}
}
return true; // we need to add the new aspect
}
}
}