5.2.1 bazel query — Static Graph Analysis
bazel query answers questions about the dependency graph declared in BUILD
files. It is the fastest member of the query family because it works after
loading, before Bazel resolves build options, platforms, transitions, or
select() branches.1 Use it when the question is structural: “what can this
target reach?”, “what reaches this library?”, or “why is there an edge between
these two areas?”
That boundary matters. query reports the unconfigured graph, not the exact
graph for one build. When the answer depends on a platform or flag, move to
5.2.2 bazel cquery — Configured Graph. When it depends on compiler invocations or artifacts, move to
5.2.3 bazel aquery — Action Graph. This is the deeper version of the three-layer map introduced
in 2.1.5 Inspecting the Graph — Query Preview.
Start With a Set, Then Transform It
A query expression evaluates to a set of targets. A target pattern creates the initial set. Functions navigate or filter it. Set operators combine results. This gives investigations a useful rhythm:
# A package subtree: where should Bazel look?
bazel query //app/...
# A transitive closure: what can this target reach?
bazel query 'deps(//app:runner)'
# A filtered closure: which source files can it reach?
bazel query 'kind("source file", deps(//app:runner))'
Quote the whole expression. Parentheses, *, and set operators otherwise risk
interpretation by the shell instead of the query parser.2 The
basic-query snippet is the running example for
the four core graph questions below: dependencies, reverse dependencies, one
path, and all paths. It continues the first deps() walk from
1.1.8 Basic Query.
Target patterns and query functions solve different problems. //app/...
selects targets by package location. deps(//app:runner) follows graph edges,
so it may leave //app and include source files, generated targets, implicit
dependencies, and external repositories. The target-pattern syntax itself is
introduced in 0.2.3 Target Patterns.
Four Graph Questions Worth Memorizing
What does this target depend on?
bazel query 'deps(//app:runner)'
deps() returns the target plus its transitive dependency closure. Add a depth
to stop after a fixed number of edges: deps(//app:runner, 1) is a compact view
of immediate dependencies.1
By default the result can include implicit and toolchain-related edges. Keep
them when diagnosing Bazel's complete declared graph. Add --noimplicit_deps
when you deliberately want a simpler view of dependencies written by repository
authors.2 Say which view you used when sharing results—a “surprising” edge
may simply be an implicit dependency hidden by the second command.
What depends on this target?
bazel query 'rdeps(//..., //lib:core)'
rdeps(universe, targets) walks edges in reverse. The first argument is not
decoration: Bazel first computes the transitive dependency closure of that
expression, then searches for reverse dependencies inside that graph. Here it
starts with deps(//...). The result includes //lib:core and consumers such
as //service:menu and //app:runner. The universe does not mean “only labels
matching its root pattern”. It means the dependency closure of those roots.1
Choose universe roots whose dependency closure covers the consumers relevant to
the decision. Repository-wide roots are appropriate for a breaking public API
change. Service roots can focus a local impact question, but their dependencies
remain part of the searchable graph. A third argument limits reverse depth.
With depth 1, the result includes the seed //lib:core at depth 0 and its
direct consumers, including //service:menu, at depth 1.
Why does A depend on B?
bazel query 'somepath(//app:runner, //lib:core)'
somepath() returns one dependency path from the first set to the second, or an
empty result if none exists. It is the quickest explanation for an unexpected
transitive dependency. The path is a witness, not necessarily the shortest or
only path.1
When removing a dependency, one witness is not proof that all routes are gone:
bazel query 'allpaths(//app:runner, //lib:core)'
allpaths() returns every node lying on any path between the two sets.1 On a
highly connected graph that subgraph can be large, so begin with somepath()
and expand only when the investigation needs completeness.
In the running example, //app:runner reaches //lib:core through both the
configuration branch and the service branch. somepath() chooses one witness.
allpaths() keeps the nodes from both routes. The query does not promise which
witness somepath() prints, so the useful comparison is one route versus the
complete path subgraph, not a fixed label sequence.
Trace: You remove the edge shown by somepath(), but the dependency
remains. What did the first query establish—and what did it not establish?
Reveal
It proved that at least one path existed. It did not prove that this was the
only path. Rerun somepath() or inspect allpaths().
Narrow Before You Render
Filters are most valuable when they express the question, not merely when they reduce terminal output:
# Filegroups that may be affected by a library change
bazel query \
'kind("filegroup rule", rdeps(//..., //lib:core))'
# Dependencies whose labels are in external repositories
bazel query 'filter("^@", deps(//app:runner))'
kind(pattern, expression) matches a regular expression against a target's
kind. Kind strings include text such as rule, so anchor patterns when an exact
classification matters. filter(pattern, expression) matches labels instead.1
This distinction prevents a common error: using filter() to search rule kinds
or kind() to search package names.
Set operations make comparisons explicit:
bazel query \
'deps(//app:runner) except deps(//app:runner, 1)'
The query language supports union, intersect, and except (also +, ^,
and -). All three operators have equal precedence, so use parentheses rather
than relying on visual intuition in a mixed expression.1
Pick Output for the Next Reader
The expression decides which graph nodes are in the answer. --output
decides how Bazel represents them.
| Need | Output | Why |
|---|---|---|
| Read or pipe labels | label | One label per line. The default |
| See target kinds | label_kind | Adds rule/file classification |
| Inspect loaded rule values | build | Reconstructs BUILD-like rule calls |
| Visualize a small subgraph | graph | Emits Graphviz DOT |
| Feed durable automation | proto, streamed_proto, or streamed_jsonproto | Structured output avoids parsing display text |
--output=build is particularly useful when macros obscure a target's origin:
the reconstructed rule includes expanded attributes and generator metadata or a
macro call trace where available.3 It still shows the loading-phase view.
its BUILD-like appearance does not mean that select() has been resolved. See
4.3.2 Macro Expansion Inspection for the focused macro-debugging workflow.
Graph output should be the last step, after narrowing:
bazel query \
'allpaths(//app:runner, //lib:core)' \
--output=graph | dot -Tsvg > dependency-paths.svg
A DOT rendering of deps(//...) is usually an unreadable hairball. A path,
depth bound, kind filter, or scoped universe turns the same renderer into an
explanation.2
For scripts, prefer a structured format over scraping build or Graphviz
text. For very long generated expressions, --query_file avoids shell command-
length limits.2 Repeatedly launching one query per target also scales poorly.
combine work into a larger query when possible, and use a purpose-built graph
extraction mechanism when a tool needs rich data for thousands of targets.2
Read Empty and Partial Results Carefully
An empty result can mean “there is no path in this unconfigured graph,” “the dependency closure of the reverse-dependency universe roots excluded the consumer,” or “your filter removed the matching nodes.” Reduce a complex expression from the inside out: first inspect the seed pattern, then the closure, then each filter.
In a large repository, --keep_going can preserve results from unaffected
packages after loading errors. Those results are partial evidence, not a clean
bill of health. Retain the command's exit status and diagnostics when using the
output in automation.1
Finally, treat configuration-sensitive answers as a handoff, not a query trick.
Because query sees all declared select() branches, an optional dependency
may appear even when the active build would not choose it.4 Re-run the same
structural question with 5.2.2 bazel cquery — Configured Graph when the decision asks what a specific
build configuration actually uses.
Use bazel query for the declared, unconfigured target graph. Seed a bounded
set, navigate it with deps, rdeps, somepath, or allpaths, narrow before
rendering, and choose output for a human or a machine. Always state the universe,
including that it means the dependency closure of its roots, plus the
implicit-edge policy and graph layer behind the answer. Switch to cquery when
configuration can change that answer.
Check your understanding · 4 questions
1.A dependency appears in bazel query even though the active platform's select() branch does not use it. What is the best next step?
Select one answer
2.You run rdeps(//service/..., //lib:core), but a known consumer under //app is absent. What should you check first?
Select one answer
3.After somepath() reveals one route from //app:runner to //lib:core, which conclusions and actions are justified?
Select all that apply
4.Match each investigation need to the query or output choice that directly addresses it:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
Footnotes
-
Bazel Query Language reference — graph semantics, functions, set operators, ordering, and output formats ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Bazel Query Deep Dive — practical scoping, quoting, output, robustness, and tooling guidance ↩1 ↩2 ↩3 ↩4 ↩5
-
Legacy macro debugging — expanded rule inspection and macro provenance with query output ↩
-
Configured Query reference — loading-phase query versus configuration-resolved target graphs ↩