Skip to content

mbt(bazel): add a streamed_proto decoder for bazel query output - #8703

Open
maksymilianrozanski wants to merge 1 commit into
scalameta:main-v2from
maksymilianrozanski:streamed-proto-decoder
Open

mbt(bazel): add a streamed_proto decoder for bazel query output#8703
maksymilianrozanski wants to merge 1 commit into
scalameta:main-v2from
maksymilianrozanski:streamed-proto-decoder

Conversation

@maksymilianrozanski

@maksymilianrozanski maksymilianrozanski commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

A parsing layer for bazel query --output=streamed_proto --proto:flatten_selects=false.

part of #8494

No observable changes yet.

Summary by CodeRabbit

  • New Features
    • Added support for reading Bazel targets through streamed protocol output.
    • Improved Bazel metadata extraction, including rules, dependencies, outputs, source files, and conditional Scala-version sources.
    • Added handling for selecting the highest valid Scala version and identifying inactive sources.
    • Added Bazel 5 compatibility and a streamed-proto-based real-output workflow.
  • Tests
    • Added unit and Bazel integration test coverage for streamed-proto parsing, Scala-version selection, and inactive-source behavior.
    • Added new Bazel fixture resources used by importer decoding tests.
  • Refactor
    • Centralized Bazel query execution to support multiple output modes, including streamed-proto parsing.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb1ac4c4-b74d-44f7-9f71-18ff7def873d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Bazel streamed-proto query support, protobuf decoding, rule and target models, Scala-version source analysis, dependency extraction, fixture regeneration, and unit/integration tests.

Changes

Bazel streamed proto importer

Layer / File(s) Summary
Proto contract and rule decoding
jsemanticdb/src/main/protobuf/bazel_query.proto, metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelRule.scala, metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelStreamedProto.scala, tests/unit/src/test/scala/tests/bazel/BazelStreamedProtoSuite.scala
Defines the Bazel query protobuf subset, converts streamed targets into BazelRule values, and tests selector, attribute, import, and non-rule handling.
Streamed query execution
metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelQuery.scala, metals/src/main/scala/scala/meta/internal/builds/BazelBuildTool.scala, bin/regenerate-bazel-proto-fixture.sh, tests/slow/src/test/scala/tests/bazel/BazelStreamedProtoBazel5QuerySuite.scala
Adds streamed-proto Bazel arguments, raw-byte process execution, runProtoDump, the shared Bazel version constant, fixture regeneration, and Bazel 5 integration coverage.
Target and source analysis
metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelBuildSrcs.scala, BazelScalaVersions.scala, BazelTargetsProtoDump.scala, tests/unit/src/test/resources/bazel/*, tests/unit/src/test/scala/tests/bazel/BazelTargetsProtoDumpRealOutputSuite.scala, BazelScalaVersionSuite.scala, BazelUnconfiguredSourcesSuite.scala
Adds Scala-version source partitioning, inactive-source selection, filegroup expansion, dependency reachability, external dependency filtering, and fixture-based validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BazelQuery
  participant Bazel
  participant BazelStreamedProto
  participant BazelTargetsProtoDump
  BazelQuery->>Bazel: Execute streamed_proto query
  Bazel-->>BazelQuery: Return target byte stream
  BazelQuery->>BazelStreamedProto: Parse target stream
  BazelStreamedProto-->>BazelQuery: Return BazelRule map
  BazelQuery->>BazelTargetsProtoDump: Build target metadata
Loading

Possibly related PRs

  • scalameta/metals#8685: Adds raw-byte process output plumbing used by streamed-proto Bazel query handling.

Suggested reviewers: zielinsky, tgodzik

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a streamed_proto decoder for Bazel query output.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maksymilianrozanski

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
bin/regenerate-bazel-proto-fixture.sh (1)

30-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixture flags don't fully match production flags.

The comment says these are "the importer's production query flags," but --proto:locations=false (line 39) isn't part of OutputMode.StreamedProto.targetStreamArgs in BazelQuery.scala. Harmless for decoding (location fields aren't consumed), but the fixture then isn't byte-identical to real production output, and the in-script claim is misleading for future maintainers regenerating it.

Align comment or flags
-# The importer's production query flags (see BazelQuery.scala). `--keep_going`
+# The importer's production query flags (see BazelQuery.scala) plus
+# `--proto:locations=false` to keep the fixture compact. `--keep_going`
 # exits 3 on partial success, which is expected here, so don't let `set -e` trip
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/regenerate-bazel-proto-fixture.sh` around lines 30 - 42, Align the
fixture query with production by removing the extra --proto:locations=false flag
from run. Update the adjacent comment so it accurately describes the shared
production flags, while preserving the existing streamed-proto output and
--keep_going behavior.
metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelBuildSrcs.scala (1)

31-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor naming ambiguity: label here is a source-file label, not a select() branch label.

label <- labels shadows the more common use of "label" elsewhere in this codebase (BazelSelectBranch.label, the config_setting guard). Renaming to srcLabel/fileLabel would reduce confusion for future readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelBuildSrcs.scala`
around lines 31 - 37, Rename the iteration variable `label` in the
`byTarget`/`byVersion` candidate-building loop to `srcLabel` or `fileLabel`, and
update its references in the `active.contains` check and `candidatesByLabel`
lookup/update; preserve the existing behavior.
metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelScalaVersions.scala (1)

18-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider logging unparseable versions as warnings rather than errors.

Since arbitrary string values (e.g., "latest") can be legitimately present in Bazel configurations and are simply skipped by this logic without breaking the importer, an error log might be too loud and alert users unnecessarily. A warn log might be more appropriate.

♻️ Proposed fix
-          case Failure(_) =>
-            scribe.error(
-              s"bazel-mbt: could not parse Scala version '$version'; ignoring it"
-            )
-            None
+          case Failure(_) =>
+            scribe.warn(
+              s"bazel-mbt: could not parse Scala version '$version'; ignoring it"
+            )
+            None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelScalaVersions.scala`
around lines 18 - 22, In the Failure branch handling unparseable Scala versions,
update the scribe logging call from error-level to warning-level while
preserving the existing message and None return behavior.
tests/slow/src/test/scala/tests/bazel/BazelStreamedProtoBazel5QuerySuite.scala (1)

68-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use NonFatal instead of Exception for safer error catching.

Catching Exception can inadvertently swallow control-flow exceptions (like InterruptedException). In Scala, it is considered best practice to use scala.util.control.NonFatal for capturing general errors.

♻️ Proposed fix
-    } catch {
-      case e: Exception =>
+    } catch {
+      case scala.util.control.NonFatal(e) =>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/slow/src/test/scala/tests/bazel/BazelStreamedProtoBazel5QuerySuite.scala`
around lines 68 - 70, Update the shutdown error handler in
BazelStreamedProtoBazel5QuerySuite to match NonFatal rather than catching
Exception, importing scala.util.control.NonFatal if needed. Preserve the
existing warning message and shutdown handling for non-fatal failures.
metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelTargetsProtoDump.scala (1)

93-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Optimize BFS to mark nodes as seen upon enqueuing.

In the current implementation, nodes are marked as seen only after they are dequeued. If multiple paths lead to the same unvisited target in the dependency graph, it will be enqueued multiple times, which can cause the queue to grow exponentially in dense graphs. Marking nodes as seen when enqueuing prevents duplicate processing and queue bloat.

⚡ Proposed fix for queue optimization
-    val seen = scala.collection.mutable.LinkedHashSet.empty[String]
-    val queue = scala.collection.mutable.Queue(root)
-    while (queue.nonEmpty) {
-      val current = queue.dequeue()
-      if (!seen(current)) {
-        seen += current
-        for (dep <- adjacency.getOrElse(current, Nil)) {
-          if (!seen(dep)) queue.enqueue(dep)
-        }
-      }
-    }
+    val seen = scala.collection.mutable.LinkedHashSet.empty[String]
+    seen += root
+    val queue = scala.collection.mutable.Queue(root)
+    while (queue.nonEmpty) {
+      val current = queue.dequeue()
+      for (dep <- adjacency.getOrElse(current, Nil)) {
+        if (!seen(dep)) {
+          seen += dep
+          queue.enqueue(dep)
+        }
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelTargetsProtoDump.scala`
around lines 93 - 102, Update the BFS loop around the seen set and queue so
nodes are marked as seen when they are enqueued, including root initialization
and newly discovered dependencies. Enqueue each dependency only when it is newly
added to seen, and remove the redundant dequeue-time seen guard while preserving
traversal order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@jsemanticdb/src/main/protobuf/bazel_query.proto`:
- Around line 8-11: Update the protobuf package declaration in BazelQueryProto
to match the file’s directory structure and satisfy buf’s
PACKAGE_DIRECTORY_MATCH rule, while preserving the existing Java package and
outer class options.

---

Nitpick comments:
In `@bin/regenerate-bazel-proto-fixture.sh`:
- Around line 30-42: Align the fixture query with production by removing the
extra --proto:locations=false flag from run. Update the adjacent comment so it
accurately describes the shared production flags, while preserving the existing
streamed-proto output and --keep_going behavior.

In
`@metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelBuildSrcs.scala`:
- Around line 31-37: Rename the iteration variable `label` in the
`byTarget`/`byVersion` candidate-building loop to `srcLabel` or `fileLabel`, and
update its references in the `active.contains` check and `candidatesByLabel`
lookup/update; preserve the existing behavior.

In
`@metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelScalaVersions.scala`:
- Around line 18-22: In the Failure branch handling unparseable Scala versions,
update the scribe logging call from error-level to warning-level while
preserving the existing message and None return behavior.

In
`@metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelTargetsProtoDump.scala`:
- Around line 93-102: Update the BFS loop around the seen set and queue so nodes
are marked as seen when they are enqueued, including root initialization and
newly discovered dependencies. Enqueue each dependency only when it is newly
added to seen, and remove the redundant dequeue-time seen guard while preserving
traversal order.

In
`@tests/slow/src/test/scala/tests/bazel/BazelStreamedProtoBazel5QuerySuite.scala`:
- Around line 68-70: Update the shutdown error handler in
BazelStreamedProtoBazel5QuerySuite to match NonFatal rather than catching
Exception, importing scala.util.control.NonFatal if needed. Preserve the
existing warning message and shutdown handling for non-fatal failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90281adb-6207-44ed-88f1-52979f2c32b9

📥 Commits

Reviewing files that changed from the base of the PR and between 06e947b and f3449a7.

📒 Files selected for processing (15)
  • bin/regenerate-bazel-proto-fixture.sh
  • jsemanticdb/src/main/protobuf/bazel_query.proto
  • metals/src/main/scala/scala/meta/internal/builds/BazelBuildTool.scala
  • metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelBuildSrcs.scala
  • metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelQuery.scala
  • metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelRule.scala
  • metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelScalaVersions.scala
  • metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelStreamedProto.scala
  • metals/src/main/scala/scala/meta/internal/metals/mbt/importer/BazelTargetsProtoDump.scala
  • tests/slow/src/test/scala/tests/bazel/BazelStreamedProtoBazel5QuerySuite.scala
  • tests/unit/src/test/resources/bazel/rules-scala-fullinfo-fragments.pb
  • tests/unit/src/test/scala/tests/bazel/BazelScalaVersionSuite.scala
  • tests/unit/src/test/scala/tests/bazel/BazelStreamedProtoSuite.scala
  • tests/unit/src/test/scala/tests/bazel/BazelTargetsProtoDumpRealOutputSuite.scala
  • tests/unit/src/test/scala/tests/bazel/BazelUnconfiguredSourcesSuite.scala

Comment thread jsemanticdb/src/main/protobuf/bazel_query.proto
@maksymilianrozanski
maksymilianrozanski force-pushed the streamed-proto-decoder branch 3 times, most recently from 9c3e37f to 13c20ec Compare July 22, 2026 05:40
@maksymilianrozanski

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.


import scala.meta.internal.metals.mbt.importer.BazelBuildSrcs.TargetSrcs

class BazelTargetsProtoDump(rulesByName: Map[String, BazelRule]) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maksymilianrozanski
maksymilianrozanski marked this pull request as ready for review July 22, 2026 06:49
A parsing layer for `bazel query --output=streamed_proto
--proto:flatten_selects=false`, not yet used by the importer:

- bazel_query.proto (jsemanticdb): a trimmed subset of Bazel's build.proto —
 field numbers identical, unknown fields skipped — compiled by the existing
 protoc setup.
- BazelStreamedProto: decodes the length-delimited binary Target stream into
 BazelRule values whose attributes keep select() branches keyed by their
 config_setting label (BazelSelectBranch).
- BazelTargetsProtoDump: the per-target views the importer reads (srcs with
 filegroup expansion, rule classes/outputs/deps, import-target jars),
 mirroring BazelTargetsXmlDump.
- BazelBuildSrcs: partitions a rule's srcs by scala_version select() branch
 and recovers inactive-branch sources with their version + origin target.
- BazelScalaVersions.maxVersion: SemVer-max over free-form version strings.
- BazelQuery: OutputMode.StreamedProto + runProtoDump, capturing stdout as
 raw bytes (line-based capture would mangle the binary stream). The XML and
 cquery paths are untouched; fullInformationQuery still runs XML.

Covered by unit suites over synthetic wire bytes (BazelStreamedProtoSuite),
captured real Bazel 7.7.1 output (BazelTargetsProtoDumpRealOutputSuite, .pb
fixture regenerated by bin/regenerate-bazel-proto-fixture.sh), inactive-source
recovery (BazelUnconfiguredSourcesSuite), and a slow suite running the real
query against Bazel 5.0.0 — Metals' minimum supported version, which rejects
streamed_jsonproto (BazelStreamedProtoBazel5QuerySuite).
@maksymilianrozanski
maksymilianrozanski force-pushed the streamed-proto-decoder branch 2 times, most recently from 7a4ccae to 25d3166 Compare July 23, 2026 07:07
@odisseus
odisseus self-requested a review July 24, 2026 11:19
@@ -0,0 +1,51 @@
#!/usr/bin/env bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAybe it makes sense to put it into test resources?

@tgodzik tgodzik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally managed to review it. @maksymilianrozanski do you still have time to take a look at it?


// Highest SemVer-parseable version; `scala_version` is a free-form STRING, so
// unparseable values are logged and skipped.
def maxVersion(versions: Iterable[String]): Option[String] =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probable move it to ScalaVersions class, which handles this already.

Comment on lines +8 to +14
case class TargetSrcs(
unconditional: Set[String],
byVersion: Map[String, Set[String]],
) {
def activeFor(scalaVersion: Option[String]): Set[String] =
unconditional ++ scalaVersion.flatMap(byVersion.get).getOrElse(Set.empty)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case class TargetSrcs(
unconditional: Set[String],
byVersion: Map[String, Set[String]],
) {
def activeFor(scalaVersion: Option[String]): Set[String] =
unconditional ++ scalaVersion.flatMap(byVersion.get).getOrElse(Set.empty)
}
case class TargetSrcs(
alwaysIncludedFiles: Set[String],
versionSpecificFiles: Map[String, Set[String]],
) {
def activeFilesForVersion(scalaVersion: Option[String]): Set[String] =
alwaysIncluded ++ scalaVersion.flatMap(versionSpecificFiles.get).getOrElse(Set.empty)
}

it took me a while to understand, so this is maybe a bit better?


case class InactiveSource(version: String, originTarget: String)

// Highest inactive-branch version wins; ties broken by smallest origin label.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a bit more why use it, why do we have inactive sources in bazel.

Files.writeString(workspace.resolve(name).toNIO, content)

override def beforeAll(): Unit = {
write(".bazelversion", s"$bazelVersion\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use FileLayout for the tests here


import tests.BaseSuite

class BazelScalaVersionSuite extends BaseSuite {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have this in ScalaVersionsSuite


// [[BazelTargetsProtoDump]] over synthetic wire bytes built with Bazel's
// `build.proto` field numbers, including fields the decoder must skip.
class BazelStreamedProtoSuite extends BaseSuite {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the same approach like in BazelStreamedProtoBazel5QuerySuite of just running Bazel. The code here seems tricky to understand.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAybe we don't need this suite at all? The next suite seems to test it much better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could achieve a similar test coverage with
BazelStreamedProtoBazel5QuerySuite.
One area which is tested in BazelStreamedProtoSuite and not tested in BazelStreamedProtoBazel5QuerySuite is jarLabelsByImportTarget and sourceJarByImportTarget fields. Aside from this one difference, I agree that we could remove BazelStreamedProtoSuite.

@maksymilianrozanski

Copy link
Copy Markdown
Contributor Author

Finally managed to review it. @maksymilianrozanski do you still have time to take a look at it?

Thanks for the review! I won't be able to rebase and re-test right now, feel free to push to my branch or raise a fresh PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants