Skip to content

[java][bidi] Add BiDi code generator#17777

Open
pujagani wants to merge 1 commit into
SeleniumHQ:trunkfrom
pujagani:java-bidi-codegen
Open

[java][bidi] Add BiDi code generator#17777
pujagani wants to merge 1 commit into
SeleniumHQ:trunkfrom
pujagani:java-bidi-codegen

Conversation

@pujagani

@pujagani pujagani commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🔗 Related Issues

💥 What does this PR do?

Adds the BiDi Java client generator: reads the shared, binding-neutral BiDi schema and emits the full typed Java BiDi protocol layer — module classes (one per domain, with commands and events) plus supporting POJOs, enums, and discriminated unions — across all 14 domains (~79 commands, 27 events, 298 generated classes total), on top of the hand-written seam (Module base class + Handle) that makes it usable.

Shape of a generated module class (real output, network.Network trimmed to 2 events + 2 commands — the full file has 5 events and 13 commands):


// This file is generated. Do not edit — regenerate via BiDiGenerator.

package org.openqa.selenium.bidi.protocol.module;

import org.openqa.selenium.Beta;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.Command;
import org.openqa.selenium.bidi.ConverterFunctions;
import org.openqa.selenium.bidi.Event;
import org.openqa.selenium.bidi.Module;

/**
 * This is an unsupported API. No compatibility guarantees are provided.
 * It tracks the W3C WebDriver BiDi specification directly. As the specification
 * evolves, this API will change or be removed without prior notice.
 */
@Beta
@SuppressWarnings("unchecked")
public class Network extends Module {

  public static final Event<AuthRequiredParameters> AUTH_REQUIRED =
      new Event<>("network.authRequired", ConverterFunctions.fromMap(AuthRequiredParameters.class));

  public static final Event<BeforeRequestSentParameters> BEFORE_REQUEST_SENT =
      new Event<>("network.beforeRequestSent", ConverterFunctions.fromMap(BeforeRequestSentParameters.class));

  // ... 3 more events

  public Network(WebDriver driver) {
    super(driver);
  }

  public AddInterceptResult addIntercept(AddInterceptParameters params) {
    return send(new Command<>("network.addIntercept", params.toMap(), AddInterceptResult.class));
  }

  public void continueRequest(ContinueRequestParameters params) {
    send(new Command<>("network.continueRequest", params.toMap()));
  }

  // ... 11 more commands
}

Usage is symmetric regardless of whether a command has a result:

Network network = new Network(driver);

// command with a result
AddInterceptResult result = network.addIntercept(new AddInterceptParameters(List.of(InterceptPhase.BEFOREREQUESTSENT)));

// command with no result
network.removeIntercept(new RemoveInterceptParameters(result.getIntercept()));

// event: subscribe returns the browser-issued subscription id; unsubscribe takes it back
String subscriptionId = network.subscribe(Network.BEFORE_REQUEST_SENT, event -> { ... });
network.unsubscribe(subscriptionId);

🔧 Implementation Notes

Autogenerated code and not checked in

The build compiles the genrule's srcjar directly:

bazel build //java/src/org/openqa/selenium/bidi:bidi-generated

No generated .java is ever committed.

Trade-off accepted knowingly: less reviewable diff surface for generator changes. Given Java has a lot of generated classes, strict typing means one .java file per record/enum/union, not a handful of dynamically-typed modules like the JS or Python bindings. The actual generated code never appears in this PR's diff, only the generator that produces it.

How to see the generated output since it's not in the diff, pull it out locally:

bazel build //java/src/org/openqa/selenium/bidi:bidi-generated
 
mkdir -p /tmp/bidi-generated-src
unzip -o -q bazel-bin/java/src/org/openqa/selenium/bidi/bidi-generated.srcjar -d /tmp/bidi-generated-src
open /tmp/bidi-generated-src   # or just browse/grep it

298 files across 14 domains will land there. A few worth spot-checking first, each demonstrating a specific decision from the Implementation Notes above:

  • org/openqa/selenium/bidi/protocol/module/Network.java — a representative module class (commands + events)
  • org/openqa/selenium/bidi/protocol/browsingcontext/SetViewportParameters.java — explicit-null vs. omitted-key handling side by side (viewport/devicePixelRatio are nullable, context/userContexts aren't)
  • org/openqa/selenium/bidi/protocol/emulation/SetTimezoneOverrideParameters.java — required-but-nullable field on a sender type
  • org/openqa/selenium/bidi/protocol/script/PrimitiveProtocolValue.java and StringValue.java — the interface extends/implements chain for multi-parent unions
    If you'd rather regenerate directly without the Bazel packaging step (useful for diffing output after a generator change):
bazel build //java/src/org/openqa/selenium/bidi:bidi-client-generator
 
bazel run //java/src/org/openqa/selenium/bidi:bidi-client-generator -- \
    "$(bazel info bazel-genfiles)/javascript/selenium-webdriver/create-bidi-src_schema.json" \
    /tmp/bidi-out.srcjar
 
unzip -o -q /tmp/bidi-out.srcjar -d /tmp/bidi-generated-src

(Direct invocation of the built binary without bazel run fails with Cannot locate runfiles directory — it needs bazel run to set up its runfiles tree.)


Explicit-null vs. omitted-key tracking

Tracked per-field for fields the schema marks nullable, so toMap() can distinguish:

  • "field": null → clear it
  • key omitted → never touched
    This is gated strictly on the schema's own nullable flag, not applied blanket to every optional field.

Required-but-nullable fields

  • Get @Nullable on their constructor parameter (boxed, if the field would otherwise be a Java primitive).
  • The shared ConstructorCoercer was extended to honor jspecify's @Nullable (previously only Optional<T> was treated as nullable).

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s):
    • What was generated:
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

  1. Test coverage is not uniform across domains.
    Only network, log, script, and browsingContext have dedicated generated-code tests (unit and/or browser-integration) added in this PR.
    session, browser, emulation, storage, input, webExtension, permissions, speculation, bluetooth, userAgentClientHints have none yet.
    Essentially, the modules that are needed for implementing high-level APIs for Selenium 5 have tests.

  2. Extensible/unknown-key round-tripping (unrecognized JSON keys silently dropped) is a known, scoped gap, not implemented in this PR. Not implemented because ConstructorCoercer maps JSON keys 1:1 to constructor parameters and silently drops anything unmatched — supporting round-trip needs a genuinely different deserialization path (capturing unrecognized keys into a side bag alongside the typed fields), which is new mechanism work, and fairly big. So this is something we might not support in Java at all. We can see how much this is required and decide later.

Rest, will be added in a follow up PR.

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)

@selenium-ci selenium-ci added C-java Java Bindings B-build Includes scripting, bazel and CI integrations B-devtools Includes everything BiDi or Chrome DevTools related labels Jul 14, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add Bazel-based Java BiDi protocol code generator

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add a Java generator that emits typed BiDi modules/POJOs from the shared schema.
• Compile generated sources from a build-time srcjar; do not commit generated .java.
• Add unit and browser-integration tests for unions, nullability, and module events/commands.
Diagram

graph TD
  A["BiDi schema (JSON)"] --> B["Bazel genrule"] --> C["BiDiGenerator (java_binary)"] --> D[("bidi-generated.srcjar")]
  D --> E["bidi-generated (java_library)"] --> F["Generated protocol modules/POJOs"] --> G["Unit + integration tests"]
  E --> H["BiDi runtime (Command/Event/Module)"]
  subgraph Legend
    direction LR
    _file["Build input/output"] ~~~ _proc["Build step"] ~~~ _jar[("Artifact")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Check in generated sources + verification test
  • ➕ Much more reviewable diffs when generator changes
  • ➕ Easier for downstream users to browse protocol surface without building
  • ➖ Large churn in repo history and PR diffs (hundreds of files)
  • ➖ Requires CI enforcement to keep generated sources in sync
  • ➖ Adds friction when schema changes frequently
2. Use a codegen library (e.g., JavaPoet)
  • ➕ Reduces hand-rolled string-concatenation and formatting risk
  • ➕ Potentially simpler future refactors (imports, indentation, comments)
  • ➖ Adds/expands dependencies in the build
  • ➖ Does not remove the hard parts (schema modeling: unions, nullability, reachability)
  • ➖ May be harder to keep output stable if library formatting changes
3. Generate fewer files via runtime reflection / dynamic maps
  • ➕ Less generated surface area; smaller compile impact
  • ➕ Potentially simpler generator
  • ➖ Loses strong typing, IDE support, and compile-time safety
  • ➖ Harder to evolve safely as schema grows; more runtime errors

Recommendation: The PR’s build-time srcjar generation approach is appropriate for Selenium’s Bazel setup (and aligns with existing CDP/devtools precedent), given the very large Java type surface. Keep the generator-in-repo (not the output), but consider adding a lightweight CI job/target that regenerates and sanity-checks the srcjar contents (e.g., file count, package layout, compilation) to mitigate the reduced diff visibility without committing generated code.

Files changed (21) +2882 / -6

Enhancement (3) +1458 / -4
BiDiGenerator.javaAdd schema-driven Java BiDi protocol generator +1435/-0

Add schema-driven Java BiDi protocol generator

• Implements a CLI generator that reads the binding-neutral BiDi schema JSON, computes reachable and sender-only types, and emits Java module classes plus record/enum/union types into a srcjar. Handles multi-parent unions via interfaces, discriminated/structural union dispatch via fromMap(), nullable-vs-omitted outbound semantics for optional nullable fields, nested synthetic types as static inner classes, and reserved-word escaping.

java/src/org/openqa/selenium/bidi/BiDiGenerator.java

ConverterFunctions.javaAdd Map→POJO deserializer helper for event payloads +20/-0

Add Map→POJO deserializer helper for event payloads

• Introduces ConverterFunctions.fromMap(Class<T>) to deserialize Map<String,Object> event payloads into typed POJOs using Selenium’s JSON coercion. This becomes the default mapping strategy for generated Event constants (with special handling for union types).

java/src/org/openqa/selenium/bidi/ConverterFunctions.java

Module.javaExpose subscribe/unsubscribe publicly for generated modules +3/-4

Expose subscribe/unsubscribe publicly for generated modules

• Changes Module.subscribe(...) and Module.unsubscribe(...) from protected to public so generated protocol modules can expose event subscription APIs directly through the shared Module seam. Keeps command sending protected/final while making event lifecycle accessible to consumers.

java/src/org/openqa/selenium/bidi/Module.java

Tests (16) +1375 / -0
BUILD.bazelAdd unit test suite for generated browsingcontext types +14/-0

Add unit test suite for generated browsingcontext types

• Creates a small JUnit5 test suite target that depends on bidi-generated and json/assertj to validate generated browsingcontext POJOs and serialization/deserialization semantics.

java/test/org/openqa/selenium/bidi/protocol/browsingcontext/BUILD.bazel

InfoTest.javaTest required+nullable vs optional field deserialization semantics +94/-0

Test required+nullable vs optional field deserialization semantics

• Validates that required nullable fields accept explicit null but still require key presence, while optional nullable fields deserialize to Optional.empty() whether null or absent. Exercises Json/ConstructorCoercer behavior expected by the generated POJOs.

java/test/org/openqa/selenium/bidi/protocol/browsingcontext/InfoTest.java

SetViewportParametersTest.javaTest explicit-null vs omitted-key behavior in generated toMap() +84/-0

Test explicit-null vs omitted-key behavior in generated toMap()

• Asserts that unset optional fields are omitted from the wire payload, while nullable optional fields support explicit clearing by emitting a key with null. Also confirms that non-nullable optional fields cannot represent an explicit-null wire state and thus remain omitted when set to null.

java/test/org/openqa/selenium/bidi/protocol/browsingcontext/SetViewportParametersTest.java

BUILD.bazelAdd unit test suite for generated emulation types +14/-0

Add unit test suite for generated emulation types

• Adds a small JUnit5 test suite for generated emulation protocol types, depending on bidi-generated and JSON tooling.

java/test/org/openqa/selenium/bidi/protocol/emulation/BUILD.bazel

SetTimezoneOverrideParametersTest.javaTest required-but-nullable field handling in construction and toMap() +50/-0

Test required-but-nullable field handling in construction and toMap()

• Ensures required nullable fields can be constructed with null and still serialize as an explicit key with null, not omitted. Confirms generator’s boxed/@Nullable behavior for required nullable types.

java/test/org/openqa/selenium/bidi/protocol/emulation/SetTimezoneOverrideParametersTest.java

SetTouchOverrideParametersTest.javaTest required nullable primitive boxing and JSON round-trip +62/-0

Test required nullable primitive boxing and JSON round-trip

• Verifies required nullable primitive fields are represented as boxed types, serialize null explicitly, and deserialize explicit null without throwing. Confirms real-value deserialization works as expected.

java/test/org/openqa/selenium/bidi/protocol/emulation/SetTouchOverrideParametersTest.java

BUILD.bazelAdd unit test suite for generated log types +14/-0

Add unit test suite for generated log types

• Creates a small JUnit5 suite target for generated log protocol classes, pulling in bidi-generated and JSON/assertj.

java/test/org/openqa/selenium/bidi/protocol/log/BUILD.bazel

EntryTest.javaTest union dispatch for log.Entry fromMap() +47/-0

Test union dispatch for log.Entry fromMap()

• Parses a representative console log entry payload into a Map and asserts Entry.fromMap dispatches to the correct generated subtype (ConsoleLogEntry). Exercises generated union deserialization paths.

java/test/org/openqa/selenium/bidi/protocol/log/EntryTest.java

BUILD.bazelAdd browser integration suite for generated module classes +27/-0

Add browser integration suite for generated module classes

• Defines a large java_selenium_test_suite that runs against BiDi-capable browsers and depends on bidi-generated plus Selenium remote/test infrastructure. Provides end-to-end coverage for generated command and event wiring through WebDriver/BiDi.

java/test/org/openqa/selenium/bidi/protocol/module/BUILD.bazel

BrowsingContextModuleTest.javaIntegration tests for generated BrowsingContext commands and events +205/-0

Integration tests for generated BrowsingContext commands and events

• Exercises generated browsing context module commands (create, navigate, getTree, close) and verifies event subscriptions (context created/destroyed, navigation events) function correctly. Uses CompletableFuture synchronization to validate actual browser-driven payloads.

java/test/org/openqa/selenium/bidi/protocol/module/BrowsingContextModuleTest.java

LogModuleTest.javaIntegration tests for generated Log module event subscriptions +170/-0

Integration tests for generated Log module event subscriptions

• Validates receipt and typing of console/javascript log entries, stack trace presence, multiple subscriptions, and unsubscribe behavior. Confirms module-level subscribe/unsubscribe lifecycle works against real browser traffic.

java/test/org/openqa/selenium/bidi/protocol/module/LogModuleTest.java

NetworkEventsTest.javaIntegration tests for generated Network event payloads +179/-0

Integration tests for generated Network event payloads

• Subscribes to multiple network events and validates key fields (context, request/response metadata, cookies) against real navigation. Marks known-not-working auth/error events as NotYetImplemented for specific browsers.

java/test/org/openqa/selenium/bidi/protocol/module/NetworkEventsTest.java

NetworkModuleTest.javaIntegration tests for generated Network commands and event lifecycle +72/-0

Integration tests for generated Network commands and event lifecycle

• Verifies a representative command round-trip (addIntercept/removeIntercept) and confirms subscribe→receive→unsubscribe works for a generated event. Separates interception logic from event lifecycle to keep tests deterministic.

java/test/org/openqa/selenium/bidi/protocol/module/NetworkModuleTest.java

ScriptModuleTest.javaIntegration tests for generated Script commands and events +228/-0

Integration tests for generated Script commands and events

• Covers callFunction/evaluate success and exception results, preload script add/remove, realm queries, channel message events, and realm lifecycle events with browser-specific NotYetImplemented annotations. Validates union result types (EvaluateResult) and typed RemoteValue variants in practice.

java/test/org/openqa/selenium/bidi/protocol/module/ScriptModuleTest.java

BUILD.bazelAdd unit test suite for generated network types +14/-0

Add unit test suite for generated network types

• Creates a small JUnit5 suite for generated network protocol classes, depending on bidi-generated and JSON/assertj.

java/test/org/openqa/selenium/bidi/protocol/network/BUILD.bazel

ContinueWithAuthParametersTest.javaTest discriminated union defaulting and serialization for ContinueWithAuthParameters +101/-0

Test discriminated union defaulting and serialization for ContinueWithAuthParameters

• Validates selector-based union dispatch to the correct nested variant type, including schema-defined default handling for unrecognized discriminator values. Also asserts nested enum wire-value round-tripping and variant toMap() serialization shape.

java/test/org/openqa/selenium/bidi/protocol/network/ContinueWithAuthParametersTest.java

Other (2) +49 / -2
BUILD.bazelWire up BiDi codegen binary + genrule-produced generated library +48/-2

Wire up BiDi codegen binary + genrule-produced generated library

• Adds a Bazel java_binary for BiDiGenerator and a genrule that runs it over the shared JS-produced schema to emit a bidi-generated.srcjar. Introduces a bidi-generated java_library that compiles the srcjar directly (no generated .java checked in) and updates exclusions so generator sources aren’t compiled into the main bidi library.

java/src/org/openqa/selenium/bidi/BUILD.bazel

BUILD.bazelAdd generated BiDi library as a test dependency +1/-0

Add generated BiDi library as a test dependency

• Extends the BiDi test suite deps to include the new //java/src/org/openqa/selenium/bidi:bidi-generated target so tests can compile against generated protocol classes.

java/test/org/openqa/selenium/bidi/BUILD.bazel

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 18 rules

Grey Divider


Action required

1. Module.subscribe() missing Javadoc 📘 Rule violation ✧ Quality
Description
Module.subscribe(...) and Module.unsubscribe(...) are public methods but have no Javadoc
comments. This violates the requirement that public API methods include Javadoc documentation.
Code

java/src/org/openqa/selenium/bidi/Module.java[R48-57]

+  public final <X> String subscribe(Event<X> event, Consumer<X> handler) {
    return handle.subscribe(event, handler);
  }

-  protected final <X> String subscribe(
-      Event<X> event, Consumer<X> handler, SubscriptionScope scope) {
+  public final <X> String subscribe(Event<X> event, Consumer<X> handler, SubscriptionScope scope) {
    return handle.subscribe(event, handler, scope);
  }

-  protected final void unsubscribe(String subscriptionId) {
+  public final void unsubscribe(String subscriptionId) {
    handle.unsubscribe(subscriptionId);
Evidence
PR Compliance ID 330200 requires Javadoc for public API methods. In Module.java, the methods
subscribe(...) and unsubscribe(...) are public and have no preceding /** ... */ Javadoc
blocks.

Rule 330200: Require Javadoc for all public API types and methods
java/src/org/openqa/selenium/bidi/Module.java[48-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Module.subscribe(...)` and `Module.unsubscribe(...)` are public but lack required Javadoc.

## Issue Context
Compliance requires Javadoc for public API methods.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/Module.java[48-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. fromMap() Javadoc missing tags 📘 Rule violation ✧ Quality
Description
ConverterFunctions.fromMap(Class<T> type) has a Javadoc block but it lacks required @param and
@return tags. This violates the requirement for complete Javadoc on public methods.
Code

java/src/org/openqa/selenium/bidi/ConverterFunctions.java[R39-43]

+  /**
+   * Returns a function that deserializes a {@code Map<String, Object>} event payload into an
+   * instance of {@code type} via the Selenium JSON library (ConstructorCoercer).
+   */
+  public static <T> Function<Map<String, Object>, T> fromMap(Class<T> type) {
Evidence
PR Compliance ID 330201 requires public methods to have complete Javadoc, including @param for
each parameter and @return for non-void returns. fromMap(Class<T> type) has no @param or
@return tags in its Javadoc.

Rule 330201: Require complete Javadoc on public API methods
java/src/org/openqa/selenium/bidi/ConverterFunctions.java[39-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ConverterFunctions.fromMap(...)` is public and has Javadoc, but the Javadoc is missing required tags (`@param type` and `@return`).

## Issue Context
Compliance requires complete Javadoc on public methods including parameter/return tags.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/ConverterFunctions.java[39-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Reserved-key deserialization breaks 🐞 Bug ≡ Correctness
Description
BiDiGenerator escapes reserved JSON field names (e.g., wire key "this" becomes Java identifier
"this_"), but record deserialization uses ConstructorCoercer which matches JSON keys to constructor
parameter names. This causes required fields to fail deserialization (missing key) or optional
fields to be silently ignored whenever wire name != Java parameter name.
Code

java/src/org/openqa/selenium/bidi/BiDiGenerator.java[R1266-1276]

+    private FieldInfo parseField(Map<String, Object> raw) {
+      String rawName = str(raw, "name");
+      String wire = str(raw, "wire");
+      // Escape Java reserved words used as field names in the spec (e.g. "this").
+      String name = escapeReserved(rawName);
+      boolean required = Boolean.TRUE.equals(raw.get("required"));
+      @SuppressWarnings("unchecked")
+      Map<String, Object> type = (Map<String, Object>) raw.get("type");
+      // wire key stays as the original spec name for JSON serialization
+      return new FieldInfo(name, wire != null ? wire : rawName, required, type);
+    }
Evidence
The generator explicitly changes the Java name while preserving the wire key, but ConstructorCoercer
requires constructor parameter names to match JSON keys; protocol payloads include a reserved key
("this"), demonstrating this mismatch is reachable.

java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1266-1276]
java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1108-1113]
java/src/org/openqa/selenium/json/ConstructorCoercer.java[191-207]
java/src/org/openqa/selenium/json/Json.java[46-51]
javascript/selenium-webdriver/bidi/scriptManager.js[335-369]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Generated record classes escape reserved words in Java identifiers (e.g. `this` -> `this_`) while keeping the JSON wire key as the original (`"this"`). Selenium JSON deserialization via `ConstructorCoercer` requires JSON property keys to match constructor parameter names, so any field where `wire != name` will not deserialize correctly.

## Issue Context
- `ConstructorCoercer` uses `Parameter.getName()` and checks `properties.containsKey(parameter.getName())`, meaning it cannot map JSON key `"this"` to a parameter named `this_`.
- The BiDi protocol payloads do include a `"this"` key (e.g. Script callFunction params).

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1266-1276]
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1108-1113]

## Suggested fix
In the generator, for any **record** type that has at least one field where `f.wire` differs from `f.name` (currently happens via `escapeReserved`), emit a `static fromJson(Map<String,Object>)` factory for that record.
- The method name must be exactly `fromJson` (so `StaticInitializerCoercer` picks it up).
- Inside `fromJson`, read values by **wire key** (`f.wire`), coerce each value to the correct Java type (recursing into nested records/unions/enums/lists/maps as needed), and invoke the all-fields constructor.
- Do **not** call `ConverterFunctions.fromMap(<ThisClass>.class)` inside this method (would recurse back into `fromJson`).
- Keep `toMap()` using wire keys as-is.

This preserves the escaped Java identifiers for API ergonomics while keeping JSON deserialization correct.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Srcjar not reproducible 🐞 Bug ☼ Reliability
Description
packToJar writes bidi-generated.srcjar using Files.walkFileTree traversal order and default JarEntry
metadata, which can vary across filesystems/runs. This makes the generated srcjar non-deterministic,
reducing Bazel cache hits and harming reproducible builds.
Code

java/src/org/openqa/selenium/bidi/BiDiGenerator.java[R1365-1396]

+  private static void packToJar(Path tempDir, Path outputJar) throws IOException {
+    Files.createDirectories(outputJar.getParent());
+    try (OutputStream os = Files.newOutputStream(outputJar);
+        JarOutputStream jos = new JarOutputStream(os)) {
+      Files.walkFileTree(
+          tempDir,
+          new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+                throws IOException {
+              String rel = tempDir.relativize(dir).toString().replace('\\', '/');
+              if (!rel.isEmpty()) {
+                JarEntry e = new JarEntry(rel + "/");
+                jos.putNextEntry(e);
+                jos.closeEntry();
+              }
+              return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                throws IOException {
+              String rel = tempDir.relativize(file).toString().replace('\\', '/');
+              JarEntry e = new JarEntry(rel);
+              jos.putNextEntry(e);
+              try (InputStream is = Files.newInputStream(file)) {
+                is.transferTo(jos);
+              }
+              jos.closeEntry();
+              return FileVisitResult.CONTINUE;
+            }
+          });
Evidence
The generator currently walks the filesystem and emits JarEntry objects without ordering or
timestamp normalization, and the Bazel genrule uses this jar as the build output artifact.

java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1365-1396]
java/src/org/openqa/selenium/bidi/BUILD.bazel[59-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The generator packs generated sources into a srcjar in a potentially non-deterministic way:
- `Files.walkFileTree` does not guarantee a stable traversal order.
- `JarEntry` is created without normalizing timestamps/metadata.
This can cause `bidi-generated.srcjar` to differ between builds even when input schema is unchanged.

## Issue Context
The srcjar is produced by a Bazel `genrule` and consumed directly as compilation input, so nondeterminism impacts caching and reproducibility.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1365-1396]
- java/src/org/openqa/selenium/bidi/BUILD.bazel[59-65]

## Suggested fix
Update `packToJar` to:
1. Collect all files under `tempDir` with `Files.walk(tempDir)`.
2. Sort paths by their normalized jar entry name (`tempDir.relativize(p).toString().replace('\\','/')`).
3. For each entry, set deterministic metadata (at minimum `entry.setTime(0L)`; optionally also set method/extra fields deterministically).
4. Write entries in sorted order.

This yields stable srcjar bytes for the same generated content.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Map fields serialize wrong 🐞 Bug ≡ Correctness
Description
serializeExpr handles primitives/refs/lists but not map type refs, so toMap() will emit
Map<String,T> values without converting nested record/union/enum values to their wire form. Any
sender-side schema field with map values of complex types will generate malformed command payloads.
Code

java/src/org/openqa/selenium/bidi/BiDiGenerator.java[R1164-1199]

+    private String serializeExpr(String varName, Map<String, Object> typeRef, String domain) {
+      if (typeRef == null) return varName;
+      if (typeRef.containsKey("primitive") || typeRef.containsKey("const")) return varName;
+      if (typeRef.containsKey("ref")) {
+        String resolvedKind = resolvedKindOf(str(typeRef, "ref"));
+        if ("enum".equals(resolvedKind)) return varName + ".toString()";
+        if ("record".equals(resolvedKind) || "union".equals(resolvedKind)) {
+          return varName + ".toMap()";
+        }
+        // alias: recurse through
+        Map<String, Object> aliasNode = types.get(str(typeRef, "ref"));
+        if (aliasNode != null && "alias".equals(str(aliasNode, "kind"))) {
+          @SuppressWarnings("unchecked")
+          Map<String, Object> inner = (Map<String, Object>) aliasNode.get("type");
+          return serializeExpr(varName, inner, domain);
+        }
+        return varName;
+      }
+      if (typeRef.containsKey("list")) {
+        @SuppressWarnings("unchecked")
+        Map<String, Object> elem = (Map<String, Object>) typeRef.get("list");
+        String elemKind = resolvedKindFromTypeRef(elem);
+        if ("enum".equals(elemKind)) {
+          return varName
+              + ".stream().map(Object::toString)"
+              + ".collect(java.util.stream.Collectors.toList())";
+        }
+        if ("record".equals(elemKind) || "union".equals(elemKind)) {
+          return varName
+              + ".stream().map(e -> e.toMap())"
+              + ".collect(java.util.stream.Collectors.toList())";
+        }
+        return varName;
+      }
+      return varName;
+    }
Evidence
The generator can resolve map type refs into Java Map types, and the schema vocabulary includes map
refs, but serializeExpr has no corresponding logic to convert map values to wire-compatible forms.

java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1164-1199]
java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1053-1057]
javascript/selenium-webdriver/project_bidi_schema.mjs[24-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serializeExpr` is used by generated `toMap()` methods to convert nested values into wire-compatible shapes. It supports primitives/const, refs (enum/record/union), and lists, but it does not handle `map` typeRefs.
As a result, fields typed as `Map<String, SomeRecord>` / `Map<String, SomeUnion>` / `Map<String, SomeEnum>` will be put on the wire as Java objects rather than `Map<String,Object>` / strings, leading to malformed payloads.

## Issue Context
The shared schema vocabulary explicitly includes `map` type refs, and the generator already resolves them to `java.util.Map<String, ...>` types.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1164-1199]
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1053-1057]

## Suggested fix
Extend `serializeExpr` with a `typeRef.containsKey("map")` branch:
- Determine the resolved kind of the map value type.
- For enum values: map `v -> v.toString()`.
- For record/union values: map `v -> v.toMap()`.
- For nested list/map values: recurse similarly.
- Preserve key strings unchanged.

Example emitted expression shape (use LinkedHashMap to preserve order):
`varName.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> <serializedValueExpr>, (a,b)->b, LinkedHashMap::new))`

This makes `toMap()` correct for map-typed schema fields.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +48 to 57
public final <X> String subscribe(Event<X> event, Consumer<X> handler) {
return handle.subscribe(event, handler);
}

protected final <X> String subscribe(
Event<X> event, Consumer<X> handler, SubscriptionScope scope) {
public final <X> String subscribe(Event<X> event, Consumer<X> handler, SubscriptionScope scope) {
return handle.subscribe(event, handler, scope);
}

protected final void unsubscribe(String subscriptionId) {
public final void unsubscribe(String subscriptionId) {
handle.unsubscribe(subscriptionId);

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.

Action required

1. module.subscribe() missing javadoc 📘 Rule violation ✧ Quality

Module.subscribe(...) and Module.unsubscribe(...) are public methods but have no Javadoc
comments. This violates the requirement that public API methods include Javadoc documentation.
Agent Prompt
## Issue description
`Module.subscribe(...)` and `Module.unsubscribe(...)` are public but lack required Javadoc.

## Issue Context
Compliance requires Javadoc for public API methods.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/Module.java[48-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +39 to +43
/**
* Returns a function that deserializes a {@code Map<String, Object>} event payload into an
* instance of {@code type} via the Selenium JSON library (ConstructorCoercer).
*/
public static <T> Function<Map<String, Object>, T> fromMap(Class<T> type) {

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.

Action required

2. frommap() javadoc missing tags 📘 Rule violation ✧ Quality

ConverterFunctions.fromMap(Class<T> type) has a Javadoc block but it lacks required @param and
@return tags. This violates the requirement for complete Javadoc on public methods.
Agent Prompt
## Issue description
`ConverterFunctions.fromMap(...)` is public and has Javadoc, but the Javadoc is missing required tags (`@param type` and `@return`).

## Issue Context
Compliance requires complete Javadoc on public methods including parameter/return tags.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/ConverterFunctions.java[39-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1266 to +1276
private FieldInfo parseField(Map<String, Object> raw) {
String rawName = str(raw, "name");
String wire = str(raw, "wire");
// Escape Java reserved words used as field names in the spec (e.g. "this").
String name = escapeReserved(rawName);
boolean required = Boolean.TRUE.equals(raw.get("required"));
@SuppressWarnings("unchecked")
Map<String, Object> type = (Map<String, Object>) raw.get("type");
// wire key stays as the original spec name for JSON serialization
return new FieldInfo(name, wire != null ? wire : rawName, required, type);
}

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.

Action required

3. Reserved-key deserialization breaks 🐞 Bug ≡ Correctness

BiDiGenerator escapes reserved JSON field names (e.g., wire key "this" becomes Java identifier
"this_"), but record deserialization uses ConstructorCoercer which matches JSON keys to constructor
parameter names. This causes required fields to fail deserialization (missing key) or optional
fields to be silently ignored whenever wire name != Java parameter name.
Agent Prompt
## Issue description
Generated record classes escape reserved words in Java identifiers (e.g. `this` -> `this_`) while keeping the JSON wire key as the original (`"this"`). Selenium JSON deserialization via `ConstructorCoercer` requires JSON property keys to match constructor parameter names, so any field where `wire != name` will not deserialize correctly.

## Issue Context
- `ConstructorCoercer` uses `Parameter.getName()` and checks `properties.containsKey(parameter.getName())`, meaning it cannot map JSON key `"this"` to a parameter named `this_`.
- The BiDi protocol payloads do include a `"this"` key (e.g. Script callFunction params).

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1266-1276]
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1108-1113]

## Suggested fix
In the generator, for any **record** type that has at least one field where `f.wire` differs from `f.name` (currently happens via `escapeReserved`), emit a `static fromJson(Map<String,Object>)` factory for that record.
- The method name must be exactly `fromJson` (so `StaticInitializerCoercer` picks it up).
- Inside `fromJson`, read values by **wire key** (`f.wire`), coerce each value to the correct Java type (recursing into nested records/unions/enums/lists/maps as needed), and invoke the all-fields constructor.
- Do **not** call `ConverterFunctions.fromMap(<ThisClass>.class)` inside this method (would recurse back into `fromJson`).
- Keep `toMap()` using wire keys as-is.

This preserves the escaped Java identifiers for API ergonomics while keeping JSON deserialization correct.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1365 to +1396
private static void packToJar(Path tempDir, Path outputJar) throws IOException {
Files.createDirectories(outputJar.getParent());
try (OutputStream os = Files.newOutputStream(outputJar);
JarOutputStream jos = new JarOutputStream(os)) {
Files.walkFileTree(
tempDir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
String rel = tempDir.relativize(dir).toString().replace('\\', '/');
if (!rel.isEmpty()) {
JarEntry e = new JarEntry(rel + "/");
jos.putNextEntry(e);
jos.closeEntry();
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
String rel = tempDir.relativize(file).toString().replace('\\', '/');
JarEntry e = new JarEntry(rel);
jos.putNextEntry(e);
try (InputStream is = Files.newInputStream(file)) {
is.transferTo(jos);
}
jos.closeEntry();
return FileVisitResult.CONTINUE;
}
});

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.

Remediation recommended

4. Srcjar not reproducible 🐞 Bug ☼ Reliability

packToJar writes bidi-generated.srcjar using Files.walkFileTree traversal order and default JarEntry
metadata, which can vary across filesystems/runs. This makes the generated srcjar non-deterministic,
reducing Bazel cache hits and harming reproducible builds.
Agent Prompt
## Issue description
The generator packs generated sources into a srcjar in a potentially non-deterministic way:
- `Files.walkFileTree` does not guarantee a stable traversal order.
- `JarEntry` is created without normalizing timestamps/metadata.
This can cause `bidi-generated.srcjar` to differ between builds even when input schema is unchanged.

## Issue Context
The srcjar is produced by a Bazel `genrule` and consumed directly as compilation input, so nondeterminism impacts caching and reproducibility.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1365-1396]
- java/src/org/openqa/selenium/bidi/BUILD.bazel[59-65]

## Suggested fix
Update `packToJar` to:
1. Collect all files under `tempDir` with `Files.walk(tempDir)`.
2. Sort paths by their normalized jar entry name (`tempDir.relativize(p).toString().replace('\\','/')`).
3. For each entry, set deterministic metadata (at minimum `entry.setTime(0L)`; optionally also set method/extra fields deterministically).
4. Write entries in sorted order.

This yields stable srcjar bytes for the same generated content.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1164 to +1199
private String serializeExpr(String varName, Map<String, Object> typeRef, String domain) {
if (typeRef == null) return varName;
if (typeRef.containsKey("primitive") || typeRef.containsKey("const")) return varName;
if (typeRef.containsKey("ref")) {
String resolvedKind = resolvedKindOf(str(typeRef, "ref"));
if ("enum".equals(resolvedKind)) return varName + ".toString()";
if ("record".equals(resolvedKind) || "union".equals(resolvedKind)) {
return varName + ".toMap()";
}
// alias: recurse through
Map<String, Object> aliasNode = types.get(str(typeRef, "ref"));
if (aliasNode != null && "alias".equals(str(aliasNode, "kind"))) {
@SuppressWarnings("unchecked")
Map<String, Object> inner = (Map<String, Object>) aliasNode.get("type");
return serializeExpr(varName, inner, domain);
}
return varName;
}
if (typeRef.containsKey("list")) {
@SuppressWarnings("unchecked")
Map<String, Object> elem = (Map<String, Object>) typeRef.get("list");
String elemKind = resolvedKindFromTypeRef(elem);
if ("enum".equals(elemKind)) {
return varName
+ ".stream().map(Object::toString)"
+ ".collect(java.util.stream.Collectors.toList())";
}
if ("record".equals(elemKind) || "union".equals(elemKind)) {
return varName
+ ".stream().map(e -> e.toMap())"
+ ".collect(java.util.stream.Collectors.toList())";
}
return varName;
}
return varName;
}

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.

Remediation recommended

5. Map fields serialize wrong 🐞 Bug ≡ Correctness

serializeExpr handles primitives/refs/lists but not map type refs, so toMap() will emit
Map<String,T> values without converting nested record/union/enum values to their wire form. Any
sender-side schema field with map values of complex types will generate malformed command payloads.
Agent Prompt
## Issue description
`serializeExpr` is used by generated `toMap()` methods to convert nested values into wire-compatible shapes. It supports primitives/const, refs (enum/record/union), and lists, but it does not handle `map` typeRefs.
As a result, fields typed as `Map<String, SomeRecord>` / `Map<String, SomeUnion>` / `Map<String, SomeEnum>` will be put on the wire as Java objects rather than `Map<String,Object>` / strings, leading to malformed payloads.

## Issue Context
The shared schema vocabulary explicitly includes `map` type refs, and the generator already resolves them to `java.util.Map<String, ...>` types.

## Fix Focus Areas
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1164-1199]
- java/src/org/openqa/selenium/bidi/BiDiGenerator.java[1053-1057]

## Suggested fix
Extend `serializeExpr` with a `typeRef.containsKey("map")` branch:
- Determine the resolved kind of the map value type.
- For enum values: map `v -> v.toString()`.
- For record/union values: map `v -> v.toMap()`.
- For nested list/map values: recurse similarly.
- Preserve key strings unchanged.

Example emitted expression shape (use LinkedHashMap to preserve order):
`varName.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> <serializedValueExpr>, (a,b)->b, LinkedHashMap::new))`

This makes `toMap()` correct for map-typed schema fields.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@titusfortner

Copy link
Copy Markdown
Member

I started reviewing this and ended up creating #17786
It's slightly different from what I did and Ruby and Python as well so I need to go back and fix those if we agree to each of these

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

Labels

B-build Includes scripting, bazel and CI integrations B-devtools Includes everything BiDi or Chrome DevTools related C-java Java Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants