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 It receives a Target and a ctx, and returns a list of provider instances.13 4.1.7 Starlark Callback Contexts & Types maps these nominal types, the callback's phase, its return value, and its effects to the other Starlark extension points.

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.

This example counts matching source-file occurrences through a graph whose srcs and deps, when present, are label-list attributes. The aspect declares its own extension parameter, while the rules it visits declare srcs and deps. Shared dependencies can contribute more than once along different paths, so this is not a count of unique files.

Inspect the three surfaces inside an aspect callback

file_count_aspect.bzl
FileCountInfo = provider(fields = {    "count": "Number of matching files",})def _file_count_aspect_impl(target, ctx):    count = 0    extension = ctx.attr.extension    if hasattr(ctx.rule.attr, "srcs"):        for src in ctx.rule.attr.srcs:            for f in src.files.to_list():                if extension in ["*", f.extension]:                    count = count + 1    if hasattr(ctx.rule.attr, "deps"):        for dep in ctx.rule.attr.deps:            count = count + dep[FileCountInfo].count    return [FileCountInfo(count = count)]file_count_aspect = aspect(    implementation = _file_count_aspect_impl,    attr_aspects = ["deps"],    attrs = {        "extension": attr.string(            default = "*",            values = ["*", "txt"],        ),    },)
  • target is the visited configured target

    Expression
    target
    Type
    Target

    This parameter gives access to the visited target's providers through target[SomeInfo]. This counting example does not need to read them: it uses ctx.rule to inspect the target's dependencies instead.

  • ctx.rule projects the visited rule

    Expression
    ctx.rule.attr.srcs
    Type
    list[Target]

    A list of configured targets because this example assumes the visited rule declares srcs with attr.label_list. Other rule schemas can give the same attribute name a different type.

    Relevant schema of a visited rule (excerpt)
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "deps": attr.label_list(),
    }
    Example value
    srcs = [':inputs'] becomes one Target. If :inputs exports hello.txt and bye.txt, src.files is a depset[File] containing both files.
  • ctx.attr belongs to the aspect

    Expression
    ctx.attr.extension
    Type
    string

    The aspect declaration below defines extension as a string: '*' counts every file and 'txt' counts text files. A matching requesting-rule parameter can supply its value. This is distinct from srcs, which is declared by the visited rule.

    Example value
    With extension = 'txt', hello.txt and bye.txt each increment count. The file image.png does not.
  • The aspect declares its own parameter

    This attrs entry creates ctx.attr.extension. It does not create ctx.rule.attr.extension on the visited rule. The two schemas belong to different declarations even though both are reached through the same ctx.

  • A propagated dependency already carries aspect providers

    Expression
    dep[FileCountInfo]
    Type
    FileCountInfo instance

    Because the aspect propagates over deps, this configured Target carries the dependency's aspect result. The provider's count field is an int.

    Example accumulation
    Two matching local files plus dep[FileCountInfo].count = 3 gives count = 5. A shared dependency can contribute along multiple paths.
  • The callback contributes a provider

    Expression
    FileCountInfo(count = count)
    Type
    FileCountInfo instance

    With count = 5, this constructs FileCountInfo(count = 5). The enclosing return wraps it in a list of provider instances, enriching this aspect application without replacing the target's existing providers.

    Phase Analysis
    When
    Bazel is applying the aspect to one configured target.
    What happens now
    The callback reads providers and may register actions before returning its own providers.
    Not yet
    The actions themselves run later during Execution.

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

Read Structured Hints From Visited Targets

aspect_hints is a common attribute on build rules, but it belongs to the visited target rather than to the aspect declaration. Each entry is a label to a target that can return providers carrying structured, ruleset-specific information. An aspect reads those targets through ctx.rule.attr.aspect_hints, but the visited rule's own implementation does not see this implicit attribute.31

For example, a language-interoperability ruleset can define a hint rule that returns InteropInfo, then let an ordinary C++ target opt into a module name:

cc_library(
    name = "codec",
    srcs = ["codec.cc"],
    aspect_hints = [":codec_interop"],
)

The consuming aspect looks for the provider rather than parsing a string:

for hint in ctx.rule.attr.aspect_hints:
    if InteropInfo in hint:
        interop = hint[InteropInfo]

This is richer than tags: a tag carries a boolean-like string, while a hint target can expose typed fields and files through providers.31 It is also not a general user-extension mechanism. A hint has no effect unless a particular aspect documents and reads its provider contract.

rules_swift demonstrates the production shape. C/C++ and Objective-C targets put swift_interop_hint or swift_overlay targets in aspect_hints, and the Swift Clang-module aspect reads the providers from ctx.rule.attr.aspect_hints to customize cross-language import behavior.32 Use this pattern when metadata is for an aspect attached by a reverse dependency and the original rule should not need a new public attribute or implementation change.

Returning Providers

An aspect implementation returns a list of providers, just like a rule.33 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.34 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.35
  • 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.36

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.37

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.38 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.39

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

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.41

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

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"]).43 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.44

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().
An aspect can inspect provider-bearing labels from a visited target's aspect_hints through ctx.rule.attr.aspect_hints.
Adding a label to aspect_hints automatically changes the target even when no aspect reads that hint's provider.

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 files — aspect() 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 files — implementation is a Starlark function with two parameters: Target and ctx, evaluated during analysis for each application of the aspect. ↩

  6. .bzl files — attr_aspects is a list of attribute names, or "*", that the aspect propagates along. ↩

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

  8. .bzl files — attrs 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 files — provides is required when another aspect's required_aspect_providers depends on them. ↩

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

  11. .bzl files — apply_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_attributes — ctx.rule exposes attr, executable, file, files, kind, toolchains, exec_groups, and var of the rule the aspect is applied to. ↩

  16. rule_attributes — kind 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. Aspects — attr_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 files — required_providers is a list of provider lists. Matches a target if it provides all entries of at least one inner list. ↩

  23. Aspects — required_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. Aspects — attrs 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. Common definitions — aspect_hints labels are exposed to aspects, especially those invoked by reverse dependencies, but not to the target's own implementation. Provider-bearing hints are the structured alternative to string tags. ↩1 ↩2

  32. rules_swift — Swift rules, toolchains, and interoperability examples — swift_overlay, swift_interop_hint, and swift_clang_module_aspect.bzl show hint targets supplying providers that a cross-language aspect reads through ctx.rule.attr.aspect_hints. ↩

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

  34. 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. ↩

  35. 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. ↩

  36. Aspects — OutputGroupInfo is merged when rule and aspect declare different output groups. InstrumentedFilesInfo is taken from the aspect. ↩

  37. 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). ↩

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

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

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

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

  42. attr — aspects parameter on attr.label / attr.label_list attaches aspects to dependencies of the attribute. ↩

  43. Aspects — file_count_rule invokes file_count_aspect via attr.label_list(aspects = [...]). Aspect is evaluated for the rule and all targets reachable via deps. ↩

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