4.8.1 Aspects — Cross-Cutting Graph Traversal

An aspect is a Bazel extension that augments the dependency graph with extra information and actions, without modifying the rules that define the visited targets.1 The rule for java_library does what it always did. The aspect rides along on top, visiting java_library targets across the graph and emitting whatever extra outputs or providers the cross-cutting job needs — a per-target JSON file for an IDE, a lint report, a license entry, a coverage descriptor.1,2 This is the mechanism that lets infrastructure teams add features that require intimate knowledge of the build graph but that a rule maintainer would never want to bake into the rule itself.2

Aspects build a shadow graph over selected attrs
The target graph still contains runtime_deps. attr_aspects = ["deps"] decides which edges get aspect applications.
Target graph

Rule attributes create ordinary dependency edges.

rule analysis
:X
deps = [":Y", ":Z"] runtime_deps = [":T"]
deps
:Y

ordinary dep

deps
:Z

ordinary dep

runtime_deps
:T

edge exists

aspect declaration attr_aspects = ["deps"]

Bazel mirrors only deps edges into aspect-application nodes.

Aspect shadow graph

Each visited target gets a parallel A(target) node.

aspect analysis
A(X)

application on :X

visited
A(Y)

from deps

visited
A(Z)

from deps

skipped
no A(T)

not in propagation attrs

At each visited node, _impl(target, ctx) reads the visited target and ctx.rule.attr, registers actions, and returns aspect providers; the rule target is not mutated.

The Shadow Graph Mental Model

Rules and aspects both have an implementation function that returns providers and may register actions, but the graph they see is different.1 A rule implementation runs once per configured target on the regular target dependency graph from 2.1 Directed Acyclic Graph (DAG). During analysis, applying an aspect A to a target X yields an aspect application node A(X), and A is then applied recursively to every target reached through X's attributes in the propagation list. The result is a "shadow graph" of the original target graph, where only the chosen attribute edges are traversed.1

Concretely, the official aspects guide shows an A that propagates along deps. When it visits a target with deps = [':Y', ':Z'] and runtime_deps = [':T'], only the :Y and :Z edges are shadowed. The aspect never descends into :T because runtime_deps is not in its propagation set.1 Inside the implementation, the aspect can read providers from the visited target, register its own actions, and return providers that the aspect application of the parent target will see — the same way rule implementations propagate data up through deps.1

A printing aspect is small enough to read as one block:1

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 []

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

Two details earn their place even in a concept article. The implementation takes (target, ctx)two arguments, not the single ctx of a rule — because the aspect needs access to the visited target's providers (target) and to the rule's attributes through ctx.rule.attr.1,3 And required_providers restricts traversal: the aspect only descends into targets whose rules advertise the named providers, which is how production aspects stay scoped to "the part of the graph that actually carries this concern" (in the snippet, only targets in the C++ side of the graph).1 The full implementation contract — aspect() parameters, propagation attributes, parameters, and provider rules — is the subject of 4.8.2 Aspect Implementation Basics.

Two Ways To Invoke An Aspect

There are exactly two attachment points, and they target different audiences.1

A command-line aspect is requested with --aspects. The official docs' canonical form is bazel build //MyExample:example --aspects print.bzl%print_aspect, with the argument syntax <extension file label>%<aspect top-level name>.1 Command-line aspects are the right shape for tools that run outside of normal builds: an IDE driver that asks Bazel to enrich a graph with per-target metadata, a CI step that runs lint across every library, an SBOM generator that walks the graph for one release artifact. Users of the rule set are not asked to change anything. The aspect is layered on at request time.

A rule-propagated aspect is wired into a rule's attribute with attr.label_list(aspects = [my_aspect]). The rule receives the aspect's providers from each dep alongside the dep's own providers, and can act on them.1 This pattern is what lets a normal bazel build //pkg:target carry an aspect-driven analysis along for the ride — the canonical example is cc_shared_library, whose rule attaches an aspect along deps to build a "parallel graph" of GraphNodeInfo providers that tell the rule which transitive libraries are already covered by other shared libraries.4

The two forms are not mutually exclusive: the same aspect definition can be invoked either way. The choice is about who is supposed to opt in — a tool operator (command-line) or a rule author (rule-propagated).

What Aspects Are Actually Used For

The official guide lists IDE integration and language-specific code generation as the headline examples,1 but the real catalogue is broader and worth knowing before writing one.

IDE project metadata. Editors that integrate with Bazel use aspects to extract a target's source files, compiler flags, and provider metadata so the editor can reason about the project without re-implementing the build. gopackagesdriver from rules_go is the worked example: for each Go target reached through deps, the aspect reads the GoArchive provider and writes a per-target .json file with the metadata gopls needs to do go-to-definition and type checking — generated .pb.go sources and export data are built on demand the same way.5 The architectural pattern of editor → LSP → package-loading library → build-system driver → rule providers is general. 4.8.5 Language Server Integration Architecture returns to it.

Linting and static analysis. rules_lint runs linters such as ESLint, Ruff, clang-tidy, and golangci-lint as aspects over existing *_library targets. Because the aspect attaches to the library graph, the infrastructure team can add or change linters in a linters.bzl file without modifying the rules themselves or asking every BUILD author to wrap their targets — visiting the library graph is exactly what aspects are designed for.6 When to choose aspects versus validation actions for the same job is a real decision. 4.8.3 Validation Actions vs Aspects works through it, and the broader operator-side wiring of formatters and linters is 3.4.4 Code Quality Integration.

Compliance metadata and SBOMs. Walking a release target's transitive deps to gather license / package metadata and emit an SBOM is a long-standing aspect use case. Aspects, hermeticity, and the fine-grained build graph together let Bazel determine SBOM contents more precisely than build systems that rely on post-hoc binary scanning.7 The same pattern appears in production: aspect-based transitive license collection replaces manual license_collection targets, along the path that became today's rules_supply_chain.8

Compilation databases and other build-tool metadata. A compile_commands.json generator for clangd can be implemented as an aspect that runs over the cc_library graph and writes a JSON entry per compile action. A naïve approach reconstructs the command from CcInfo.compilation_context and may diverge from what Bazel's toolchain actually invokes. The aspect can instead read the real CppCompileAction arguments and, with a little extra plumbing, even cover tree-artifact actions to keep the database accurate.9

Whole-repo code transformations. When the cross-cutting job is to do work on every target rather than just inspect it — for example wrapping OpenRewrite as an aspect-driven Bazel action so a JVM migration recipe runs across an entire 30M-line monorepo in parallel on RBE workers — the same shadow-graph model carries through. The pattern is the subject of 4.8.4 Large-Scale Refactoring via Aspects.

The common thread is concision: in every one of these cases, the alternative without aspects would be to modify every rule that participates, or to bolt a separate tool onto the side of the build that re-discovers the graph Bazel already analyzed. Aspects let the cross-cutting concern live as a single Starlark file alongside the rules it visits.

How Aspects Differ From Rules

Aspects share a surprising amount of machinery with rules — implementation functions, providers, actions, attrs, configuration — but two differences explain the rest. 4.8.2 Aspect Implementation Basics covers the implementation contract.

The implementation receives a target as well as a ctx. The target is the analyzed target the aspect is currently visiting. Aspect-author code reads its providers with target[SomeInfo]. The ctx is an aspect context: attributes declared on the underlying rule live behind ctx.rule.attr, ctx.rule.executable, ctx.rule.file, ctx.rule.files, and ctx.rule.kind, while attrs declared on the aspect itself live on plain ctx.attr.1,3 That asymmetry is what makes one aspect able to operate over many rule kinds: it inspects the rule's attributes generically rather than being baked into a specific rule definition like in 4.2 Custom Rules, Providers & Actions.

Aspect providers are scoped, not blended into the target's. The set of providers visible at A(X) is the union of the rule's providers for X and the aspect's. The target's own providers are frozen before aspects run and an aspect cannot mutate them.1 An aspect implementation may therefore never return DefaultInfo — that provider comes from the underlying rule — and same-type provider clashes are errors except for OutputGroupInfo (merged when the rule and aspect use different output groups) and InstrumentedFilesInfo (the aspect's wins).1 These rules, together with the attribute-type constraints and aspect-on-aspect mechanics, are the body of work in 4.8.2 Aspect Implementation Basics.

Two further capabilities surface elsewhere in this section. When an aspect needs to compose its work with an existing rule-declared action — for example, adding instrumentation to a compile without reconstructing the entire action — ctx.actions.run(shadowed_action = ...) inherits the original action's inputs and environment. 4.8.7 shadowed_action — Action Composition is the dedicated subsection. And when an aspect needs to decide at the time of each target which attributes to propagate along, the static attr_aspects = [...] list can be replaced by the runtime propagation_ctx API. 4.8.6 Dynamic Aspect Propagation covers it.

For a compact runnable instance of the shadow-graph model, inspect the mini-ruleset's glyph_metadata_aspect. It follows deps and exports, reads GlyphInfo from each visited target, and publishes a transitive glyph_metadata output group without changing the Glyph rules.

key takeaway

An aspect is not "a kind of rule". It is a way to walk a shadow of the rule graph and attach extra information or actions to every visited target without changing the rules that defined them.

You declare propagation attributes (or use required_providers to filter where the aspect runs), the implementation receives a (target, ctx) pair, and you return providers or output groups that downstream tooling can consume. IDE project metadata, linting, SBOMs, compile-command extraction, and whole-repo refactoring all use the same primitive — and they all work because the aspect lives next to the rules without being part of them.

Check your understanding · 4 questions

1.Which sentence best describes what an aspect is in Bazel?

Select one answer

2.Which statements match the shadow-graph mental model for aspects?

Select all that apply

3.Match each real-world use case to the aspect-driven mechanism it relies on.

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

Answers
IntelliJ / gopls editor integration
rules_lint running ESLint, Ruff, clang-tidy, golangci-lint
rules_supply_chain SBOM generation
cc_shared_library link-decision logic

4.True or false: choosing an aspect invocation style

Choose True or False for each sentence

A rule-propagated aspect is a good fit when the owning rule needs aspect-produced providers from its dependencies as part of its own analysis.
A rule-propagated aspect is attached on a declaring rule's label attribute with aspects = [...], so that propagation becomes part of the declaring rule's contract.
The same aspect definition can never be used both from the command line and through a rule attribute.
A macro is equivalent to an aspect because both can walk already-configured dependency graphs.
0 of 4 answered

Footnotes

  1. Aspects — shadow-graph model, (target, ctx) signature, propagation through attr_aspects, required_providers filtering, command-line vs rule-propagated invocation, --aspects file%name syntax, aspect attribute constraints, provider merge/conflict rules, prohibition of DefaultInfo from aspects. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

  2. Aspects: The Fan-fic of Build Rules — framing of aspects as features that need intimate knowledge of the build graph but that rule maintainers would not add themselves. 1 2

  3. rule attributesctx.rule.attr, ctx.rule.executable, ctx.rule.file, ctx.rule.files, and ctx.rule.kind for accessing the visited rule's attributes from an aspect implementation. 1 2

  4. Bazel's Take on (Cc) Shared Libraries — rule-propagated aspect building a parallel GraphNodeInfo graph used by cc_shared_library to decide what to link.

  5. Go Editor Support in Bazel Workspacesgopackagesdriver uses a Bazel aspect to read GoArchive providers and emit per-target JSON metadata for gopls.

  6. Rules_lint: Formatting and Linting All Languages — aspect-based linting over existing *_library targets, the aspect-factory pattern with private _config attrs, and the choice of aspects over validation actions for infrastructure-level quality gates.

  7. Automating Software Supply Chain Security With Bazel — aspects as a structural advantage for SBOM precision. Reflecting over the build graph rather than scanning final binaries.

  8. {Fast, Correct, Secure} - Choose Three — aspect-based transitive license collection replacing manual license_collection targets.

  9. The State of Compilation Database in Bazel — aspect-based compilation database generation, including the trade-off between reconstructing commands from CcInfo and reusing action arguments directly.