4.8.2 Aspect Implementation Basics

An aspect is declared with aspect() and an implementation function — the surface is parallel to 4.2.1 Rule Function, but, during analysis, its result attaches to existing dependency edges instead of defining a new target kind.1 The aspect declaration says how the shadow graph from 4.8.1 Aspects — Cross-Cutting Graph Traversal is built and what each visited node may produce: providers, actions, and named output groups.2

The aspect() Declaration

aspect() is a global factory. Its return value must be stored in a global symbol in a .bzl file so Bazel and users can refer to it by <file label>%<global name>.3 The smallest useful declaration names the implementation function and the edges to follow:

print_aspect = aspect(
    implementation = _print_aspect_impl,
    attr_aspects = ["deps"],
    required_providers = [CcInfo],
)

The most important parameters at this level are:4

ParameterWhat it decides
implementationThe Starlark function Bazel runs for every visited target.5
attr_aspectsWhich attributes of each visited rule the aspect follows next.6
required_providersWhich targets are eligible — the aspect only stops on targets whose rule advertises one of these provider sets.7
attrsAspect attributes: private label deps for tools and public string parameters.8
providesProviders the implementation promises to return. Required when another aspect's required_aspect_providers reads them.9

Other parameters are useful but secondary for a first aspect. toolchains, exec_groups, and toolchains_aspects connect the aspect to platform-aware tooling. requires, required_aspect_providers, and provides chain aspects together. propagation_predicate and the function form of attr_aspects enable runtime propagation decisions — those belong in 4.8.6 Dynamic Aspect Propagation.10 apply_to_generating_rules redirects propagation from an output file back to the rule that generates it.11

The Implementation Function

An aspect implementation has exactly two parameters: the target it is being applied to and ctx, the analysis context.12 The shape is (Target, ctx) → [Provider].13

def _print_aspect_impl(target, ctx):
    if hasattr(ctx.rule.attr, "srcs"):
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                print(f.path)
    return []

Two things separate this from a rule implementation:

  • target is the visited rule's configured target. The aspect reads providers from it with the usual target[SomeInfo] indexing.14
  • ctx.rule exposes the visited rule's own metadata, even though the aspect didn't declare those attributes. ctx.rule.attr is the attribute struct, ctx.rule.kind is the rule class name ("cc_library", "py_binary", …), and ctx.rule.executable / ctx.rule.file / ctx.rule.files / ctx.rule.toolchains mirror the rule's analysis-time accessors.15 ctx.rule.kind is the standard hook for rule-type-specific logic, such as "only emit a Makefile rule for cc_binary."16

ctx still gives access to the aspect's own attrs through ctx.attr and to action APIs through ctx.actions, exactly like a rule context.17 An aspect can therefore call ctx.actions.run(), declare files, and emit OutputGroupInfo the same way a rule does.18

Propagation Edges

attr_aspects is the list of attribute names the aspect follows. Common values are ["deps"] and ["deps", "exports"]. The wildcard ["*"] propagates along every attribute of every visited rule.19 Bazel rewrites the visited rule's view of those attributes for the duration of the aspect: if a target X has Y and Z in deps, then ctx.rule.attr.deps for A(X) is [A(Y), A(Z)] — Target objects that already carry the aspect's providers.20 This is the mechanism behind transitive collection patterns.

def _file_count_aspect_impl(target, ctx):
    count = 0
    if hasattr(ctx.rule.attr, "srcs"):
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                if ctx.attr.extension == "*" or ctx.attr.extension == f.extension:
                    count = count + 1
    for dep in ctx.rule.attr.deps:
        count = count + dep[FileCountInfo].count
    return [FileCountInfo(count = count)]

The aspect reads the dep's FileCountInfo after applying itself to that dep — the recursion is implicit in the propagation list.21

required_providers narrows traversal to targets whose rules advertise certain providers. The value is a list of provider lists with OR-of-ANDs semantics: [[FooInfo], [BarInfo], [BazInfo, QuxInfo]] matches a rule that provides FooInfo, or BarInfo, or both BazInfo and QuxInfo.22 Use it to keep the shadow graph focused — a Java-only aspect should not stop at cc_library nodes.23 The wildcard ["*"] propagation combined with no required_providers is the most aggressive shape and can cause subtle problems: cc_shared_library's linking aspect uses "*" so it can reach object files reachable through any attribute, and a custom rule that incidentally returns CcInfo ends up considered for linking by accident.24

Aspect Attrs

Aspects can declare their own attributes, but the rules differ from regular rules.25

  • Private attributes (names starting with _) must have defaults and must be of type label or label_list. They are how an aspect carries the tools and configuration files it needs without forcing every visited rule to expose them:26

    attrs = {
        "_protoc": attr.label(
            default = Label("//tools:protoc"),
            executable = True,
            cfg = "exec",
        ),
    }
    
  • Public attributes are parameters. In the current API reference, explicit public aspect attrs must be string attrs and use the values restriction so Bazel can validate parameter values against the consuming rule.27 For command-line aspects, parameter values come from --aspects_parameters and the values restriction may be relaxed.28

That public-parameter restriction is what makes the aspect-factory pattern common in production rulesets: a top-level Starlark function captures richer configuration, such as a label, and returns an aspect() whose private attr defaults to that value.29

def stylelint_aspect(config):
    return aspect(
        implementation = _stylelint_impl,
        attr_aspects = ["deps"],
        attrs = {
            "_config": attr.label(default = config),
        },
    )

That is how rules_lint lets each consumer wire their own tool config without exposing a public label-typed aspect parameter.30

Returning Providers

An aspect implementation returns a list of providers, just like a rule.31 The provider set for the aspect application A(X) is the union of the rule's providers on X and the aspect's own providers.32 Two rules differ from regular rule authoring:

  • An aspect must not return DefaultInfo. Default outputs belong to the rule. It is also an error if the rule and the aspect both return the same provider type — with two documented exceptions.33
  • OutputGroupInfo from rules and aspects is merged, as long as the rule and aspect declare different group names. InstrumentedFilesInfo from the aspect supersedes the rule's, used for coverage instrumentation.34

OutputGroupInfo is therefore the standard way an aspect surfaces files to users and tools — IDE drivers, linters, and SBOM aspects use named groups so a single bazel build can request only the aspect's artifacts.35

return [
    FileCountInfo(count = count),
    OutputGroupInfo(file_count_report = depset([report])),
]

Aspect-generated actions should also use stable mnemonic values. Mnemonics show up in profiles, bazel aquery, and CI logs alongside actions from regular rules.36 For multi-action augmentation patterns — adding instrumentation or coverage to an existing rule's action without reconstructing its full input list — see 4.8.7 shadowed_action — Action Composition.

Two Ways To Invoke

An aspect runs only when something asks for it. There are two invocation modes, called command-line aspects and rule-propagated aspects in the official docs.37

Command-line — pass --aspects and a label-style spec on a normal bazel build:38

bazel build //pkg:app \
    --aspects=//tools/lint:linters.bzl%ruff_aspect \
    --output_groups=rules_lint_human,rules_lint_machine

The spec format is <.bzl file label>%<aspect global name>. Command-line aspects are how IDE plugins, lint drivers, and SBOM generators run aspects across an existing graph without changing any BUILD files.39

Rule-propagated — a custom rule attaches the aspect to one of its label attributes:40

file_count_rule = rule(
    implementation = _file_count_rule_impl,
    attrs = {
        "deps": attr.label_list(aspects = [file_count_aspect]),
        "extension": attr.string(default = "*"),
    },
)

Here, building any file_count_rule target runs file_count_aspect on the targets in deps and on their transitive deps (because attr_aspects = ["deps"]).41 Aspect parameters are taken from the requesting rule's attribute of the same name and type — that is why the rule has a matching extension attr with a default value, and why public string parameters need a values restriction on the aspect side.42

The choice between the two is a deployment question, not an implementation one. Both run the same implementation function over the same shadow graph. They differ only in how the request reaches the graph. Lint workflows usually start as command-line aspects because they don't want to touch rule sets. Once that quality gate matures, the team may decide to fold it into the rule contract instead — that is the comparison developed in 4.8.3 Validation Actions vs Aspects.

The mini-ruleset assembles the core pieces in glyph_metadata_aspect.bzl: an implementation that registers an output, propagation over two attributes, required_providers, and an OutputGroupInfo result. Request it with bazel build //examples/basic:metadata --aspects=//tools/aspects:glyph_metadata_aspect.bzl%glyph_metadata_aspect --output_groups=glyph_metadata.

key takeaway

An aspect is an analysis-time overlay that follows selected dependency edges, not a new target kind.

aspect() declares a parallel function over an existing graph: implementation plus attr_aspects decide where it runs, required_providers decides what it stops on, attrs give it tools and parameters, and the returned providers and output groups are how its results escape. Public params are string-valued. Private label attrs carry tools and richer config. Never return DefaultInfo. Merge with rules through OutputGroupInfo. Pick command-line invocation when you can't change rule sets, rule-propagated when the aspect is part of a target's contract.

Check your understanding · 4 questions

1.An aspect's implementation function is called with (target, ctx). What does ctx.rule give the aspect that ctx alone does not?

Select one answer

2.Which of the following are valid for an aspect's attrs?

Select all that apply

3.True or false: provider rules for aspects

Choose True or False for each sentence

An aspect may return DefaultInfo to add files to the visited target's default outputs.
If a rule and an aspect both return OutputGroupInfo with different group names, Bazel merges the providers.
It is an error if a rule and the aspect applied to it return providers of the same custom type.
An aspect can only return providers — it cannot register actions like ctx.actions.run().

4.Match each aspect() parameter to what it controls.

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
attr_aspects
required_providers
attrs
implementation
0 of 4 answered

Footnotes

  1. Aspects — aspects are similar to rules in that they have an implementation function that generates actions and returns providers, but they propagate along edges in a shadow graph instead of defining new target kinds.

  2. Aspects — typical use cases: IDE project info, code generation, linting, coverage, metadata collection.

  3. .bzl filesaspect() creates an aspect value that must be stored in a global value. Reference format <file label>%<aspect name> for --aspects.

  4. .bzl files — full parameter list for aspect().

  5. .bzl filesimplementation is a Starlark function with two parameters: Target and ctx, evaluated during analysis for each application of the aspect.

  6. .bzl filesattr_aspects is a list of attribute names, or "*", that the aspect propagates along.

  7. .bzl filesrequired_providers limits propagation to targets whose rules advertise the listed providers, with OR-of-ANDs semantics on lists of lists.

  8. .bzl filesattrs maps attribute name to attr.* object. Implicit _ attrs must be label/label_list with defaults. Explicit attrs must be string and use values.

  9. .bzl filesprovides is required when another aspect's required_aspect_providers depends on them.

  10. .bzl filestoolchains, toolchains_aspects, exec_groups, propagation_predicate, and function-form attr_aspects are documented as the dynamic and toolchain extension points.

  11. .bzl filesapply_to_generating_rules propagates from an output file to its generating rule.

  12. Aspects — implementation function takes target and ctx. Returns providers, may generate actions.

  13. Bazel's Take on (Cc) Shared Libraries — aspect is essentially a function (Target, Context) → [Provider] walking the build tree and enriching it.

  14. Aspects — the implementation can examine providers provided by the target via the target argument.

  15. rule_attributesctx.rule exposes attr, executable, file, files, kind, toolchains, exec_groups, and var of the rule the aspect is applied to.

  16. rule_attributeskind is the rule class name, e.g. cc_library.

  17. Aspects — parameters and private attributes are passed in the attributes of ctx.

  18. Aspects — aspect implementation functions are like rule implementations: they return providers and can generate actions.

  19. Aspectsattr_aspects propagation list. "*" propagates along all attributes.

  20. Aspects — values of attributes along which the aspect propagates are replaced with the results of applying the aspect to them.

  21. Aspects — file_count_aspect example accumulates FileCountInfo.count from ctx.rule.attr.deps after the aspect has been applied to them.

  22. .bzl filesrequired_providers is a list of provider lists. Matches a target if it provides all entries of at least one inner list.

  23. Aspectsrequired_providers keeps traversal focused on targets that advertise the listed providers, e.g. only CcInfo-carrying targets.

  24. Bazel's Take on (Cc) Shared Libraries — the linking aspect propagates via "*" to every edge and looks at every target providing CcInfo. Custom rules that incidentally provide CcInfo get pulled into linking decisions.

  25. Aspectsattrs defines a set of attributes for an aspect. Type and visibility rules differ from rules.

  26. Aspects — private label/label_list attributes can specify dependencies on tools or libraries. _protoc example with executable = True, cfg = "exec".

  27. .bzl files — current aspect() API reference: explicit public aspect attrs must be string attrs and use the values restriction. Private attrs must be label / label_list with defaults.

  28. Aspects — command-line aspects pass parameter values via --aspects_parameters and the values restriction may be omitted.

  29. Rules_lint: Formatting and Linting All Languages — aspect factory pattern: a function returns an aspect whose _config private label attr is the captured config label. Needed because aspects only allow string parameters publicly and need label types via underscore prefix.

  30. Rules_lint: Formatting and Linting All Languages — each linter ships a *_aspect(config = ...) factory used in user linters.bzl.

  31. Aspects — aspect implementation returns a struct/list of providers that are accessible to its dependencies.

  32. Aspects — provider set for A(X) is the union of the rule's providers on X and the aspect's providers. Rule providers are frozen and cannot be modified by the aspect.

  33. Aspects — it is an error if a target and the aspect both provide a provider of the same type, except for OutputGroupInfo and InstrumentedFilesInfo. Aspect implementations may never return DefaultInfo.

  34. AspectsOutputGroupInfo is merged when rule and aspect declare different output groups. InstrumentedFilesInfo is taken from the aspect.

  35. Rules_lint: Formatting and Linting All Languages — aspect output groups such as human-readable and machine-readable lint reports decouple lint execution from consumption mode (build failure, test, report).

  36. actionsmnemonic makes actions identifiable in profiles, logs, and aquery. Same API applies whether the action is registered from a rule or an aspect.

  37. Aspects — distinction between rule-propagated aspects and command-line aspects.

  38. Aspects — command-line invocation: bazel build //tgt --aspects file.bzl%aspect_name. Spec format <extension file label>%<aspect top-level name>.

  39. Rules_lint: Formatting and Linting All Languages — aspect visits existing *_library rules without modifying them. Designed so no rule set modification is needed.

  40. attraspects parameter on attr.label / attr.label_list attaches aspects to dependencies of the attribute.

  41. Aspectsfile_count_rule invokes file_count_aspect via attr.label_list(aspects = [...]). Aspect is evaluated for the rule and all targets reachable via deps.

  42. Aspects — for rule-propagated aspects, parameter values come from the requesting rule's attribute of the same name and type.