blob: 72c9680eb802efeafcf1f7c377e47f9ddd533c4f [file] [log] [blame]
# Copyright 2024 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.
"""Generates provider factories."""
load("@bazel_skylib//lib:structs.bzl", "structs")
load("@rules_testing//lib:truth.bzl", "subjects")
visibility("private")
def generate_factory(type, name, attrs):
"""Generates a factory for a custom struct.
There are three reasons we need to do so:
1. It's very difficult to read providers printed by these types.
eg. If you have a 10 layer deep diamond dependency graph, and try to
print the top value, the bottom value will be printed 2^10 times.
2. Collections of subjects are not well supported by rules_testing
eg. `FeatureInfo(flag_sets = [FlagSetInfo(...)])`
(You can do it, but the inner values are just regular bazel structs and
you can't do fluent assertions on them).
3. Recursive types are not supported at all
eg. `FeatureInfo(implies = depset([FeatureInfo(...)]))`
To solve this, we create a factory that:
* Validates that the types of the children are correct.
* Inlines providers to their labels when unambiguous.
For example, given:
```
foo = FeatureInfo(name = "foo", label = Label("//:foo"))
bar = FeatureInfo(..., implies = depset([foo]))
```
It would convert itself a subject for the following struct:
`FeatureInfo(..., implies = depset([Label("//:foo")]))`
Args:
type: (type) The type to create a factory for (eg. FooInfo)
name: (str) The name of the type (eg. "FooInfo")
attrs: (dict[str, Factory]) The attributes associated with this type.
Returns:
A struct `FooFactory` suitable for use with
* `analysis_test(provider_subject_factories=[FooFactory])`
* `generate_factory(..., attrs=dict(foo = FooFactory))`
* `ProviderSequence(FooFactory)`
* `DepsetSequence(FooFactory)`
"""
attrs["label"] = subjects.label
want_keys = sorted(attrs.keys())
def validate(*, value, meta):
got_keys = sorted(structs.to_dict(value).keys())
if got_keys != want_keys:
meta.add_failure("Wanted a %s with keys %r, got %r" % (name, want_keys, got_keys), "")
def type_factory(value, *, meta):
validate(value = value, meta = meta)
transformed_value = {}
transformed_factories = {}
for field, factory in attrs.items():
field_value = getattr(value, field)
# If it's a type generated by generate_factory, inline it.
if hasattr(factory, "factory"):
factory.validate(value = field_value, meta = meta.derive(field))
transformed_value[field] = field_value.label
transformed_factories[field] = subjects.label
else:
transformed_value[field] = field_value
transformed_factories[field] = factory
return subjects.struct(
struct(**transformed_value),
meta = meta,
attrs = transformed_factories,
)
return struct(
type = type,
name = name,
factory = type_factory,
validate = validate,
)
def _provider_collection(element_factory, fn):
def factory(value, *, meta):
value = fn(value)
# Validate that it really is the correct type
for i in range(len(value)):
element_factory.validate(
value = value[i],
meta = meta.derive("offset({})".format(i)),
)
# Inline the providers to just labels.
return subjects.collection([v.label for v in value], meta = meta)
return factory
# This acts like a class, so we name it like one.
# buildifier: disable=name-conventions
ProviderSequence = lambda element_factory: _provider_collection(
element_factory,
fn = lambda x: list(x),
)
# buildifier: disable=name-conventions
ProviderDepset = lambda element_factory: _provider_collection(
element_factory,
fn = lambda x: x.to_list(),
)