-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
.pr_agent_auto_best_practices
Pattern 1: When returning or storing collection-like state (maps/sets), return an immutable/defensive copy and avoid mutating caller-provided collections (especially in constructors) to prevent side effects and accidental external modification.
Example code before:
class Foo {
private final Map<String, String> labels;
Foo(Map<String, String> labels) {
this.labels = labels; // stores caller map
this.labels.put("x", "1"); // mutates caller map
}
Map<String, String> toMap() { return labels; } // leaks internals
}
Example code after:
class Foo {
private final Map<String, String> labels;
Foo(Map<String, String> labels) {
Map<String, String> copy = new HashMap<>(labels);
copy.put("x", "1");
this.labels = Collections.unmodifiableMap(copy);
}
Map<String, String> toMap() { return new HashMap<>(labels); }
}
Relevant past accepted suggestions:
Suggestion 1:
Return unmodifiable serialized map
Return an unmodifiable copy to prevent callers from mutating the returned map; this preserves immutability of serialized state.
java/src/org/openqa/selenium/bidi/emulation/ScreenOrientation.java [46-51]
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("natural", natural.toString());
map.put("type", type.toString());
- return map;
+ return java.util.Collections.unmodifiableMap(map);
}Suggestion 2:
Return an immutable set copy
In getSessionsByUri, replace new HashSet<>(result) with Set.copyOf(result) to return an immutable copy of the set, which is a safer programming practice.
java/src/org/openqa/selenium/grid/sessionmap/local/LocalSessionMap.java [293-297]
public Set<SessionId> getSessionsByUri(URI uri) {
Set<SessionId> result = sessionsByUri.get(uri);
// Return a copy to prevent concurrent modification issues
- return (result != null && !result.isEmpty()) ? new HashSet<>(result) : Set.of();
+ return (result != null && !result.isEmpty()) ? Set.copyOf(result) : Set.of();
}Suggestion 3:
Avoid mutating a constructor parameter
To avoid mutating a constructor parameter and prevent potential runtime exceptions, create a new mutable copy of the composeLabels map before adding the new label.
java/src/org/openqa/selenium/grid/node/docker/DockerSessionFactory.java [144-146]
-this.composeLabels = Require.nonNull("Docker Compose labels", composeLabels);
// Merge compose labels with oneoff=False to prevent triggering --exit-code-from dynamic grid
-this.composeLabels.put("com.docker.compose.oneoff", "False");
+Map<String, String> allLabels = new HashMap<>(Require.nonNull("Docker Compose labels", composeLabels));
+allLabels.put("com.docker.compose.oneoff", "False");
+this.composeLabels = Collections.unmodifiableMap(allLabels);Suggestion 4:
Avoid modifying nested map directly
To avoid side effects, create a defensive copy of the networkSettings map in adaptContainerInspectResponse before modifying it, ensuring the original response data structure remains unchanged.
java/src/org/openqa/selenium/docker/client/V148Adapter.java [165-205]
@SuppressWarnings("unchecked")
-Map<String, Object> networkSettings = (Map<String, Object>) adapted.get("NetworkSettings");
+Map<String, Object> originalNetworkSettings =
+ (Map<String, Object>) adapted.get("NetworkSettings");
-if (networkSettings != null) {
+if (originalNetworkSettings != null) {
+ // Create a mutable copy to avoid modifying the original map
+ Map<String, Object> networkSettings = new HashMap<>(originalNetworkSettings);
+
@SuppressWarnings("unchecked")
Map<String, Object> networks = (Map<String, Object>) networkSettings.get("Networks");
if (networks != null) {
for (Map.Entry<String, Object> entry : networks.entrySet()) {
if (entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> network = (Map<String, Object>) entry.getValue();
if (network.containsKey("GwPriority")) {
LOG.fine(
"Network '" + entry.getKey() + "' includes GwPriority (API v" + apiVersion + ")");
}
}
}
}
// Remove deprecated fields (should not be present in v1.48+)
String[] deprecatedFields = {
"HairpinMode",
"LinkLocalIPv6Address",
"LinkLocalIPv6PrefixLen",
"SecondaryIPAddresses",
"SecondaryIPv6Addresses",
"Bridge" // Deprecated in v1.51, removed in v1.52
};
for (String field : deprecatedFields) {
if (networkSettings.containsKey(field)) {
LOG.fine(
"Removing deprecated field '"
+ field
+ "' from NetworkSettings (deprecated in earlier API versions)");
networkSettings.remove(field);
}
}
+ adapted.put("NetworkSettings", networkSettings);
}Suggestion 5:
Return a defensive copy of map
Return a defensive copy of the internal map in the toMap() method to prevent external modification and maintain encapsulation.
java/src/org/openqa/selenium/bidi/emulation/AbstractOverrideParameters.java [57]
-return map;
+return new HashMap<>(map);Suggestion 6:
Create a defensive copy of parameters
To ensure the Command object is immutable, create a defensive, unmodifiable copy of the params map that allows null values.
java/src/org/openqa/selenium/bidi/Command.java [51]
-this.params = Require.nonNull("Command parameters", params);
+this.params = java.util.Collections.unmodifiableMap(new java.util.HashMap<>(Require.nonNull("Command parameters", params)));Pattern 2: In tests or code that creates external resources (temporary directories, browsing contexts, user contexts, sessions), wrap usage in try/finally and always perform cleanup/close/delete in the finally block so failures and assertions cannot leak resources.
Example code before:
Path dir = Files.createTempDirectory("test");
Context ctx = api.createContext();
api.doWork(ctx, dir);
api.closeContext(ctx);
Files.deleteIfExists(dir);
Example code after:
Path dir = Files.createTempDirectory("test");
Context ctx = api.createContext();
try {
api.doWork(ctx, dir);
} finally {
try { api.closeContext(ctx); } finally { Files.deleteIfExists(dir); }
}
Relevant past accepted suggestions:
Suggestion 1:
Clean up temporary test directories
Add cleanup logic to the finally block to delete the temporary directory created during the test. This prevents resource leaks on the file system.
java/test/org/openqa/selenium/bidi/browser/BrowserCommandsTest.java [113-142]
Path tmpDir = TemporaryFilesystem.getDefaultTmpFS().createTempDir("downloads", "test").toPath();
try {
browser.setDownloadBehavior(new SetDownloadBehaviorParameters(true, tmpDir));
BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle());
String url = appServer.whereIs("downloads/download.html");
context.navigate(url, ReadinessState.COMPLETE);
driver.findElement(By.id("file-1")).click();
new WebDriverWait(driver, Duration.ofSeconds(5))
.until(
d -> {
try {
return Files.list(tmpDir)
.anyMatch(path -> path.getFileName().toString().equals("file_1.txt"));
} catch (Exception e) {
return false;
}
});
List<String> fileNames =
Files.list(tmpDir)
.map(path -> path.getFileName().toString())
.collect(Collectors.toList());
assertThat(fileNames).contains("file_1.txt");
} finally {
browser.setDownloadBehavior(new SetDownloadBehaviorParameters(null, (Path) null));
+ TemporaryFilesystem.getDefaultTmpFS().deleteTempDir(tmpDir.toFile());
}Suggestion 2:
Cleanup created user contexts
Ensure created user contexts are cleaned up after the test by deleting them in a finally block to prevent resource leaks.
java/test/org/openqa/selenium/bidi/emulation/SetScriptingEnabledTest.java [82-113]
String userContext = browser.createUserContext();
-BrowsingContext context =
- new BrowsingContext(
- driver, new CreateContextParameters(WindowType.TAB).userContext(userContext));
-String contextId = context.getId();
-...
-// Clear the scripting override
-emulation.setScriptingEnabled(
- new SetScriptingEnabledParameters(null).userContexts(List.of(userContext)));
+try {
+ BrowsingContext context =
+ new BrowsingContext(
+ driver, new CreateContextParameters(WindowType.TAB).userContext(userContext));
+ String contextId = context.getId();
+ ...
+ emulation.setScriptingEnabled(
+ new SetScriptingEnabledParameters(null).userContexts(List.of(userContext)));
+} finally {
+ browser.deleteUserContext(userContext);
+}Suggestion 3:
Close context in finally
Wrap context creation/usage in try-finally and close the BrowsingContext in finally to avoid leaks if assertions throw.
java/test/org/openqa/selenium/bidi/emulation/SetGeolocationOverrideTest.java [201-235]
BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle());
-String contextId = context.getId();
-...
-// Error because there's no real geolocation available
-assertThat(r2.containsKey("error")).isTrue();
+try {
+ String contextId = context.getId();
+ String url = appServer.whereIsSecure("blank.html");
+ context.navigate(url, ReadinessState.COMPLETE);
+ driver.switchTo().window(context.getId());
+ String origin =
+ (String) ((JavascriptExecutor) driver).executeScript("return window.location.origin;");
+ Emulation emul = new Emulation(driver);
+ GeolocationCoordinates coords = new GeolocationCoordinates(37.7749, -122.4194);
+ emul.setGeolocationOverride(
+ new SetGeolocationOverrideParameters(coords).contexts(List.of(contextId)));
+ Object firstResult = getBrowserGeolocation(driver, null, origin);
+ Map<String, Object> r1 = ((Map<String, Object>) firstResult);
+ assertThat(r1.containsKey("error")).isFalse();
+ double latitude1 = ((Number) r1.get("latitude")).doubleValue();
+ double longitude1 = ((Number) r1.get("longitude")).doubleValue();
+ assertThat(abs(latitude1 - coords.getLatitude())).isLessThan(0.0001);
+ assertThat(abs(longitude1 - coords.getLongitude())).isLessThan(0.0001);
+ emul.setGeolocationOverride(
+ new SetGeolocationOverrideParameters((GeolocationCoordinates) null)
+ .contexts(List.of(contextId)));
+ Object secondResult = getBrowserGeolocation(driver, null, origin);
+ Map<String, Object> r2 = ((Map<String, Object>) secondResult);
+ assertThat(r2.containsKey("error")).isTrue();
+} finally {
+ context.close();
+}Suggestion 4:
Add robust test cleanup
Ensure the tab and user context are cleaned up even if assertions fail by wrapping them in try/finally blocks.
py/test/selenium/webdriver/common/bidi_emulation_tests.py [347-363]
def test_set_locale_override_with_user_contexts(driver, pages, value):
"""Test setting locale override with user contexts."""
user_context = driver.browser.create_user_context()
+ try:
+ context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context)
+ try:
+ driver.switch_to.window(context_id)
+ driver.emulation.set_locale_override(locale=value, user_contexts=[user_context])
+ driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete")
+ current_locale = get_browser_locale(driver)
+ assert current_locale == value, f"Expected locale {value}, got {current_locale}"
+ finally:
+ driver.browsing_context.close(context_id)
+ finally:
+ driver.browser.remove_user_context(user_context)
- context_id = driver.browsing_context.create(type=WindowTypes.TAB, user_context=user_context)
-
- driver.switch_to.window(context_id)
-
- driver.emulation.set_locale_override(locale=value, user_contexts=[user_context])
-
- driver.browsing_context.navigate(context_id, pages.url("formPage.html"), wait="complete")
-
- current_locale = get_browser_locale(driver)
- assert current_locale == value, f"Expected locale {value}, got {current_locale}"
-
- driver.browsing_context.close(context_id)
- driver.browser.remove_user_context(user_context)
-Pattern 3: Add explicit validation and null/availability guards at integration boundaries (GitHub Actions contexts, environment variables, URL parameters, JSON/message fields, browser APIs) by trimming inputs, checking presence/type, and encoding/sanitizing before use.
Example code before:
name = params["name"] # may be nil/blank
url = "/api/create?name=" + name
mode = window.localStorage.getItem("mode")
path = Path(os.getenv("TOOL_PATH"))
Example code after:
name = params["name"]
if (name != null) {
name = name.trim();
if (!name.isEmpty()) {
url = "/api/create?name=" + URLEncoder.encode(name, UTF_8);
}
}
if (typeof window !== "undefined") {
try { mode = window.localStorage.getItem("mode"); } catch {}
}
env = os.getenv("TOOL_PATH")
if (env != null && Path(env.strip()).is_file()) { path = Path(env.strip()) }
Relevant past accepted suggestions:
Suggestion 1:
Make fork guard null-safe
For schedule/workflow_dispatch, github.event.repository may be absent, so comparing it to false can incorrectly skip jobs; use != true and keep the owner check separate.
.github/workflows/nightly.yml [37]
-if: (github.event.repository.fork == false == 'seleniumhq') && (inputs.language == 'ruby' || inputs.language == 'all' || github.event_name == 'schedule')
+if: (github.repository_owner == 'seleniumhq' && github.event.repository.fork != true) && (inputs.language == 'ruby' || inputs.language == 'all' || github.event_name == 'schedule')Suggestion 2:
Validate environment variable path
Validate that SE_MANAGER_PATH points to an existing executable file and ignore or error out if it does not.
py/selenium/webdriver/common/selenium_manager.py [81-83]
if (env_path := os.getenv("SE_MANAGER_PATH")) is not None:
logger.debug(f"Selenium Manager set by env SE_MANAGER_PATH to: {env_path}")
- path = Path(env_path)
+ env_path = env_path.strip()
+ candidate = Path(env_path)
+ if candidate.is_file():
+ path = candidate
+ else:
+ logger.warning(f"SE_MANAGER_PATH does not point to a file: {env_path}")Suggestion 3:
Validate and encode URL parameter
Validate and URL-encode the optional container name to avoid invalid requests and injection issues. Reject empty/whitespace-only names and percent-encode the value.
java/src/org/openqa/selenium/docker/client/CreateContainer.java [66-69]
String url = String.format("/v%s/containers/create", apiVersion);
-if (info.getName() != null && !info.getName().isEmpty()) {
- url += "?name=" + info.getName();
+String name = info.getName();
+if (name != null) {
+ name = name.trim();
+ if (!name.isEmpty()) {
+ String encoded = java.net.URLEncoder.encode(name, java.nio.charset.StandardCharsets.UTF_8);
+ url += "?name=" + encoded;
+ }
}Suggestion 4:
Guard message fields before use
Add defensive checks to ensure message['params'] is a Hash before use and safely duplicate callbacks to prevent nil errors and races when handlers are removed concurrently.
rb/lib/selenium/webdriver/common/websocket_connection.rb [137-147]
while (frame = incoming_frame.next)
break if @closing
message = process_frame(frame)
- next unless message['method']
+ method = message['method']
+ next unless method.is_a?(String)
params = message['params']
- @messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
+ next unless params.is_a?(Hash)
+
+ handlers = @callbacks_mtx.synchronize { callbacks[method].dup }
+ handlers.each do |callback|
@callback_threads.add(callback_thread(params, &callback))
end
endSuggestion 5:
Default Content-Type when unknown
Ensure a valid Content-Type is always sent by defaulting to a sensible MIME type when guessing fails. This avoids sending a None header value.
py/test/selenium/webdriver/common/webserver.py [104-108]
elif os.path.isfile(file_path):
content_type, _ = mimetypes.guess_type(file_path)
+ if not content_type:
+ content_type = "application/octet-stream"
content = self._serve_file(file_path)
self._send_response(content_type)
self.wfile.write(content)Suggestion 6:
Guard browser API accesses
Add environment checks before using window, localStorage, and matchMedia to avoid runtime errors in SSR/tests and ensure safe access. Fallback gracefully when unavailable.
javascript/grid-ui/src/contexts/ThemeContext.tsx [40-55]
useEffect(() => {
- const saved = localStorage.getItem('theme-mode') as ThemeMode
- if (saved) setThemeMode(saved)
- setSystemPrefersDark(window.matchMedia('(prefers-color-scheme: dark)').matches)
+ if (typeof window !== 'undefined') {
+ try {
+ const saved = window.localStorage.getItem('theme-mode') as ThemeMode
+ if (saved) setThemeMode(saved)
+ const mq = window.matchMedia?.('(prefers-color-scheme: dark)')
+ setSystemPrefersDark(!!mq && mq.matches)
+ } catch {
+ // ignore storage access errors
+ }
+ }
}, [])
useEffect(() => {
- localStorage.setItem('theme-mode', themeMode)
+ if (typeof window !== 'undefined') {
+ try {
+ window.localStorage.setItem('theme-mode', themeMode)
+ } catch {
+ // ignore storage access errors
+ }
+ }
}, [themeMode])
useEffect(() => {
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
- const handler = (e: MediaQueryListEvent) => setSystemPrefersDark(e.matches)
- mediaQuery.addEventListener('change', handler)
- return () => mediaQuery.removeEventListener('change', handler)
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+ const handler = (e: MediaQueryListEvent) => setSystemPrefersDark(e.matches)
+ mediaQuery.addEventListener?.('change', handler)
+ return () => mediaQuery.removeEventListener?.('change', handler)
+ }
+ return
}, [])Pattern 4: Make lifecycle/concurrency-sensitive code robust by removing hidden side effects, ensuring idempotent disposal, and using thread-safe synchronization/initialization patterns (granular locks, atomic removal, Interlocked/CompareExchange, etc.).
Example code before:
Resource _r;
public Resource R => _r ??= new Resource(); // not thread-safe
public void Dispose() { pool.Return(buf); } // can run twice
callbacks.reject! { |cb| cb.id == id } // non-atomic with other reads
Example code after:
private volatile Resource? _r;
public Resource R =>
_r ?? Interlocked.CompareExchange(ref _r, new Resource(), null) ?? _r!;
private bool _disposed;
public void Dispose() { if (_disposed) return; pool.Return(buf); _disposed = true; }
mutex.synchronize { callbacks.reject! { |cb| cb.id == id } or raise "missing" }
Relevant past accepted suggestions:
Suggestion 1:
Remove side-effectful mutation
Avoid mutating SessionId in a helper; instead create a new value or event payload carrying the reason to prevent hidden side effects.
java/src/org/openqa/selenium/grid/data/SessionClosedEvent.java [36-46]
public SessionClosedEvent(SessionId id, SessionClosedReason reason) {
- super(SESSION_CLOSED, markSessionId(id, reason));
Require.nonNull("Session ID", id);
Require.nonNull("Reason", reason);
+ super(SESSION_CLOSED, id.withCloseReason(reason.getReasonText()));
}
-// Helper method to mark the SessionId before passing to Event constructor
-private static SessionId markSessionId(SessionId id, SessionClosedReason reason) {
- id.setCloseReason(reason.getReasonText());
- return id;
-}
-Suggestion 2:
Fix race condition with thread-safe initialization
The lazy initialization using ??= is not thread-safe and can cause a race condition. To fix this, use Interlocked.CompareExchange for a lock-free, thread-safe initialization and mark the backing fields as volatile.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs [34-58]
-private BrowsingContextLogModule? _logModule;
-private BrowsingContextNetworkModule? _networkModule;
-private BrowsingContextScriptModule? _scriptModule;
-private BrowsingContextStorageModule? _storageModule;
-private BrowsingContextInputModule? _inputModule;
+private volatile BrowsingContextLogModule? _logModule;
+private volatile BrowsingContextNetworkModule? _networkModule;
+private volatile BrowsingContextScriptModule? _scriptModule;
+private volatile BrowsingContextStorageModule? _storageModule;
+private volatile BrowsingContextInputModule? _inputModule;
internal string Id { get; }
[JsonIgnore]
public BiDi BiDi { get; }
[JsonIgnore]
-public BrowsingContextLogModule Log => _logModule ??= new BrowsingContextLogModule(this, BiDi.Log);
+public BrowsingContextLogModule Log => _logModule ?? Interlocked.CompareExchange(ref _logModule, new BrowsingContextLogModule(this, BiDi.Log), null) ?? _logModule!;
[JsonIgnore]
-public BrowsingContextNetworkModule Network => _networkModule ??= new BrowsingContextNetworkModule(this, BiDi.Network);
+public BrowsingContextNetworkModule Network => _networkModule ?? Interlocked.CompareExchange(ref _networkModule, new BrowsingContextNetworkModule(this, BiDi.Network), null) ?? _networkModule!;
[JsonIgnore]
-public BrowsingContextScriptModule Script => _scriptModule ??= new BrowsingContextScriptModule(this, BiDi.Script);
+public BrowsingContextScriptModule Script => _scriptModule ?? Interlocked.CompareExchange(ref _scriptModule, new BrowsingContextScriptModule(this, BiDi.Script), null) ?? _scriptModule!;
[JsonIgnore]
-public BrowsingContextStorageModule Storage => _storageModule ??= new BrowsingContextStorageModule(this, BiDi.Storage);
+public BrowsingContextStorageModule Storage => _storageModule ?? Interlocked.CompareExchange(ref _storageModule, new BrowsingContextStorageModule(this, BiDi.Storage), null) ?? _storageModule!;
[JsonIgnore]
-public BrowsingContextInputModule Input => _inputModule ??= new BrowsingContextInputModule(this, BiDi.InputModule);
+public BrowsingContextInputModule Input => _inputModule ?? Interlocked.CompareExchange(ref _inputModule, new BrowsingContextInputModule(this, BiDi.InputModule), null) ?? _inputModule!;Suggestion 3:
Make the Dispose method idempotent
Make the Dispose method idempotent by using a private flag. This prevents returning the same buffer to the ArrayPool multiple times, which would otherwise cause a critical error.
dotnet/src/webdriver/BiDi/WebSocketTransport.cs [91-97]
+private bool _disposed;
+
public void Dispose()
{
+ if (_disposed)
+ {
+ return;
+ }
+
_webSocket.Dispose();
_sharedMemoryStream.Dispose();
_socketSendSemaphoreSlim.Dispose();
ArrayPool<byte>.Shared.Return(_receiveBuffer);
+ _disposed = true;
}Suggestion 4:
Refactor to use multiple specialized mutexes
**payload) @@ -98,12 +106,10 @@ begin socket.write(out_frame.to_s) rescue *CONNECTION_ERRORS => e
-
raise Error::WebDriverError, "WebSocket is closed (#{e.class}: #{e.message})" -
end -
wait.until do -
@mtx.synchronize { messages.delete(id) } -
end
-
raise e, "WebSocket is closed (#{e.class}: #{e.message})" -
end -
wait.until { @messages_mtx.synchronize { messages.delete(id) } } end private
@@ -132,9 +138,8 @@ message = process_frame(frame) next unless message['method']
-
params = message['params'] -
@mtx.synchronize { callbacks[message['method']].dup }.each do |callback| -
@callback_threads.add(callback_thread(params, &callback))
-
@messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback| -
@callback_threads.add(callback_thread(message['params'], &callback)) end end end
@@ -154,7 +159,7 @@ return {} if message.empty?
msg = JSON.parse(message)
-
@mtx.synchronize { messages[msg['id']] = msg if msg.key?('id') }
-
@messages_mtx.synchronize { messages[msg['id']] = msg if msg.key?('id') } WebDriver.logger.debug "WebSocket <- #{msg}"[...MAX_LOG_MESSAGE_SIZE], id: :ws msg
@@ -170,7 +175,8 @@ rescue Error::WebDriverError, *CONNECTION_ERRORS => e WebDriver.logger.debug "Callback aborted: #{e.class}: #{e.message}", id: :ws rescue StandardError => e
-
# Unexpected handler failure; log with a short backtrace.
-
return if @closing -
bt = Array(e.backtrace).first(5).join("\n") WebDriver.logger.error "Callback error: #{e.class}: #{e.message}\n#{bt}", id: :ws end
**Replace the single, coarse-grained mutex with multiple, more granular mutexes, one for each shared resource (@callbacks, @messages, @closing). This will reduce lock contention and improve concurrency.**
### Examples:
rb/lib/selenium/webdriver/common/websocket_connection.rb [40]
```ruby
@mtx = Mutex.new
rb/lib/selenium/webdriver/common/websocket_connection.rb [77-80]
@mtx.synchronize do
callbacks[event] << block
block.object_id
endclass WebSocketConnection
def initialize(url:)
@mtx = Mutex.new
@closing = false
@callbacks = {}
@messages = {}
# ...
end
def add_callback(event, █)
@mtx.synchronize do
# ... access @callbacks
end
end
def send_cmd(**payload)
# ...
wait.until do
@mtx.synchronize { messages.delete(id) }
end
end
def process_frame(frame)
# ...
@mtx.synchronize { messages[msg['id']] = msg }
# ...
end
endclass WebSocketConnection
def initialize(url:)
@callbacks_mtx = Mutex.new
@messages_mtx = Mutex.new
@closing_mtx = Mutex.new
@closing = false
@callbacks = {}
@messages = {}
# ...
end
def add_callback(event, █)
@callbacks_mtx.synchronize do
# ... access @callbacks
end
end
def send_cmd(**payload)
# ...
wait.until do
@messages_mtx.synchronize { messages.delete(id) }
end
end
def process_frame(frame)
# ...
@messages_mtx.synchronize { messages[msg['id']] = msg }
# ...
end
endSuggestion 5:
Fix race condition in callback removal
Refactor the remove_callback method to use a single synchronize block. This will prevent a race condition by ensuring the callback removal and error message data collection are performed atomically.
rb/lib/selenium/webdriver/common/websocket_connection.rb [84-88]
-removed = @mtx.synchronize { callbacks[event].reject! { |cb| cb.object_id == id } }
-return if removed || @closing
+@mtx.synchronize do
+ return if @closing
-ids = @mtx.synchronize { callbacks[event]&.map(&:object_id) }
-raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
+ callbacks_for_event = callbacks[event]
+ if callbacks_for_event.reject! { |cb| cb.object_id == id }
+ return
+ end
+ ids = callbacks_for_event.map(&:object_id)
+ raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
+end
+Pattern 5: Reduce duplication and remove redundant/dead logic by centralizing shared behavior (helpers, base types, shared utilities, linked single-source files) and deleting code paths that are ineffective or already handled elsewhere.
Example code before:
// repeated in many types
class A { string Type => "a"; }
class B { string Type => "b"; }
// redundant filtering already applied upstream
if (event.context == ctx) handler(event);
// unused helper left behind
expr; // no-op
Example code after:
abstract class Typed(string discriminator) { string Type => discriminator; }
class A : Typed("a") {}
class B : Typed("b") {}
// rely on upstream filter
handler(event);
// delete no-op/unused code and link to a single shared source file
Relevant past accepted suggestions:
Suggestion 1:
Remove ineffective window style setting
Remove the ineffective WindowStyle assignment, as it has no effect when UseShellExecute is false. The existing CreateNoWindow property already handles hiding the window.
dotnet/src/webdriver/DriverService.cs [254-260]
this.driverServiceProcess.StartInfo.UseShellExecute = false;
this.driverServiceProcess.StartInfo.CreateNoWindow = this.HideCommandPromptWindow;
-if (this.HideCommandPromptWindow)
-{
- this.driverServiceProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
-}
-Suggestion 2:
Centralize type discriminator logic
Avoid repeating the discriminator property across all records; centralize it in a shared base/helper to prevent drift and reduce boilerplate.
dotnet/src/webdriver/BiDi/Script/LocalValue.cs [284-362]
-public sealed record NumberLocalValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolLocalValue
+public abstract record TypedLocalValue(string Discriminator) : LocalValue
{
[JsonInclude]
- internal string Type { get; } = "number";
+ internal string Type { get; } = Discriminator;
+}
+public abstract record TypedPrimitiveLocalValue(string Discriminator) : PrimitiveProtocolLocalValue
+{
+ [JsonInclude]
+ internal string Type { get; } = Discriminator;
+}
+
+public sealed record NumberLocalValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value)
+ : TypedPrimitiveLocalValue("number")
+{
public static explicit operator NumberLocalValue(double n) => new NumberLocalValue(n);
}
-public sealed record StringLocalValue(string Value) : PrimitiveProtocolLocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "string";
-}
+public sealed record StringLocalValue(string Value) : TypedPrimitiveLocalValue("string");
+public sealed record NullLocalValue() : TypedPrimitiveLocalValue("null");
+public sealed record UndefinedLocalValue() : TypedPrimitiveLocalValue("undefined");
+public sealed record BooleanLocalValue(bool Value) : TypedPrimitiveLocalValue("boolean");
+public sealed record BigIntLocalValue(string Value) : TypedPrimitiveLocalValue("bigint");
-public sealed record NullLocalValue : PrimitiveProtocolLocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "null";
-}
+public sealed record ChannelLocalValue(ChannelProperties Value) : TypedLocalValue("channel");
+public sealed record ArrayLocalValue(IEnumerable<LocalValue> Value) : TypedLocalValue("array");
+public sealed record DateLocalValue(string Value) : TypedLocalValue("date");
+public sealed record MapLocalValue(IEnumerable<IEnumerable<LocalValue>> Value) : TypedLocalValue("map");
+public sealed record ObjectLocalValue(IEnumerable<IEnumerable<LocalValue>> Value) : TypedLocalValue("object");
+public sealed record RegExpLocalValue(RegExpValue Value) : TypedLocalValue("regexp");
+public sealed record SetLocalValue(IEnumerable<LocalValue> Value) : TypedLocalValue("set");
-public sealed record UndefinedLocalValue : PrimitiveProtocolLocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "undefined";
-}
-
-public sealed record BooleanLocalValue(bool Value) : PrimitiveProtocolLocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "boolean";
-}
-
-public sealed record BigIntLocalValue(string Value) : PrimitiveProtocolLocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "bigint";
-}
-
-public sealed record ChannelLocalValue(ChannelProperties Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "channel";
-}
-
-public sealed record ArrayLocalValue(IEnumerable<LocalValue> Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "array";
-}
-
-public sealed record DateLocalValue(string Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "date";
-}
-
-public sealed record MapLocalValue(IEnumerable<IEnumerable<LocalValue>> Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "map";
-}
-
-public sealed record ObjectLocalValue(IEnumerable<IEnumerable<LocalValue>> Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "object";
-}
-
-public sealed record RegExpLocalValue(RegExpValue Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "regexp";
-}
-
-public sealed record SetLocalValue(IEnumerable<LocalValue> Value) : LocalValue
-{
- [JsonInclude]
- internal string Type { get; } = "set";
-}
-Suggestion 3:
Eliminate dead JSON options
Remove the now-unused JsonSerializerOptions method and inline config; prefer the new source-generated contexts to avoid dead code and duplication.
dotnet/src/webdriver/BiDi/BiDi.cs [95-117]
-return new JsonSerializerOptions
-{
- //PropertyNameCaseInsensitive = true,
- //PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- //DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+// Remove GetJsonOptions() entirely and references to it.
+// Modules now use source-generated contexts (e.g., BrowsingContextJsonSerializerContext.Default)
- // BiDi returns special numbers such as "NaN" as strings
- // Additionally, -0 is returned as a string "-0"
- NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString,
- Converters =
- {
- //new BrowsingContextConverter(),
- //new BrowserUserContextConverter(),
- //new CollectorConverter(),
- //new InterceptConverter(),
- //new HandleConverter(),
- //new InternalIdConverter(),
- //new PreloadScriptConverter(),
- //new RealmConverter(),
- new DateTimeOffsetConverter(),
- //new WebExtensionConverter(),
- }
-};
-Suggestion 4:
Remove statement with no effect
In the generate_from_json method, remove the line expr which is a statement with no effect and likely a debugging leftover.
def generate_from_json(self, dict_):
"""Generate the code that creates an instance from a JSON dict named `dict_`."""
if self.items:
if self.items.ref:
py_ref = ref_to_python(self.items.ref)
expr = f"[{py_ref}.from_json(i) for i in {dict_}['{self.name}']]"
- expr
else:
cons = CdpPrimitiveType.get_constructor(self.items.type, "i")
expr = f"[{cons} for i in {dict_}['{self.name}']]"
else:
if self.ref:
py_ref = ref_to_python(self.ref)
expr = f"{py_ref}.from_json({dict_}['{self.name}'])"
else:
expr = CdpPrimitiveType.get_constructor(self.type, f"{dict_}['{self.name}']")
if self.optional:
expr = f"{expr} if '{self.name}' in {dict_} else None"
return exprSuggestion 5:
Avoid code duplication by linking files
Remove the duplicated StringSyntaxAttribute.cs file and instead link to the single source file from the project file to avoid code duplication.
dotnet/src/support/Internal/StringSyntaxAttribute.cs [1-88]
-// <copyright file="StringSyntaxAttribute.cs" company="Selenium Committers">
-// Licensed to the Software Freedom Conservancy (SFC) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The SFC licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-// </copyright>
+// This file should be deleted.
-#if !NET8_0_OR_GREATER
-
-namespace System.Diagnostics.CodeAnalysis;
-
-/// <summary>Specifies the syntax used in a string.</summary>
-[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
-internal sealed class StringSyntaxAttribute : Attribute
-{
- /// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
- /// <param name="syntax">The syntax identifier.</param>
- public StringSyntaxAttribute(string syntax)
- {
- Syntax = syntax;
- Arguments = [];
- }
-
- /// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
- /// <param name="syntax">The syntax identifier.</param>
- /// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
- public StringSyntaxAttribute(string syntax, params object?[] arguments)
- {
- Syntax = syntax;
- Arguments = arguments;
- }
-
- /// <summary>Gets the identifier of the syntax used.</summary>
- public string Syntax { get; }
-
- /// <summary>Optional arguments associated with the specific syntax employed.</summary>
- public object?[] Arguments { get; }
-
- /// <summary>The syntax identifier for strings containing composite formats for string formatting.</summary>
- public const string CompositeFormat = nameof(CompositeFormat);
-
- /// <summary>The syntax identifier for strings containing date format specifiers.</summary>
- public const string DateOnlyFormat = nameof(DateOnlyFormat);
-
- /// <summary>The syntax identifier for strings containing date and time format specifiers.</summary>
- public const string DateTimeFormat = nameof(DateTimeFormat);
-
- /// <summary>The syntax identifier for strings containing <see cref="Enum"/> format specifiers.</summary>
- public const string EnumFormat = nameof(EnumFormat);
-
- /// <summary>The syntax identifier for strings containing <see cref="Guid"/> format specifiers.</summary>
- public const string GuidFormat = nameof(GuidFormat);
-
- /// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary>
- public const string Json = nameof(Json);
-
- /// <summary>The syntax identifier for strings containing numeric format specifiers.</summary>
- public const string NumericFormat = nameof(NumericFormat);
-
- /// <summary>The syntax identifier for strings containing regular expressions.</summary>
- public const string Regex = nameof(Regex);
-
- /// <summary>The syntax identifier for strings containing time format specifiers.</summary>
- public const string TimeOnlyFormat = nameof(TimeOnlyFormat);
-
- /// <summary>The syntax identifier for strings containing <see cref="TimeSpan"/> format specifiers.</summary>
- public const string TimeSpanFormat = nameof(TimeSpanFormat);
-
- /// <summary>The syntax identifier for strings containing URIs.</summary>
- public const string Uri = nameof(Uri);
-
- /// <summary>The syntax identifier for strings containing XML.</summary>
- public const string Xml = nameof(Xml);
-}
-
-#endif
-Suggestion 6:
Remove redundant client-side context filtering
Remove the redundant client-side context check in OnEntryAddedAsync methods. The event subscription is already filtered by context at the protocol level, making the if statement unnecessary.
dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs [28-48]
public Task<Subscription> OnEntryAddedAsync(Func<Log.LogEntry, Task> handler, ContextSubscriptionOptions? options = null)
{
- return logModule.OnEntryAddedAsync(async args =>
- {
- if (args.Source.Context?.Equals(context) is true)
- {
- await handler(args).ConfigureAwait(false);
- }
- }, options.WithContext(context));
+ return logModule.OnEntryAddedAsync(handler, options.WithContext(context));
}
public Task<Subscription> OnEntryAddedAsync(Action<Log.LogEntry> handler, ContextSubscriptionOptions? options = null)
{
- return logModule.OnEntryAddedAsync(args =>
- {
- if (args.Source.Context?.Equals(context) is true)
- {
- handler(args);
- }
- }, options.WithContext(context));
+ return logModule.OnEntryAddedAsync(handler, options.WithContext(context));
}Suggestion 7:
Refactor duplicated code into a utility method
Extract the duplicated configureHeartbeat method from BoundZmqEventBus and UnboundZmqEventBus into a static helper method in a new utility class to avoid code repetition and improve maintainability.
java/src/org/openqa/selenium/events/zeromq/BoundZmqEventBus.java [84-97]
-private void configureHeartbeat(ZMQ.Socket socket, Duration heartbeatPeriod, String socketType) {
- if (heartbeatPeriod != null && !heartbeatPeriod.isZero() && !heartbeatPeriod.isNegative()) {
- int heartbeatIvl = (int) heartbeatPeriod.toMillis();
- socket.setHeartbeatIvl(heartbeatIvl);
- // Set heartbeat timeout to 3x the interval
- socket.setHeartbeatTimeout(heartbeatIvl * 3);
- // Set heartbeat TTL to 6x the interval
- socket.setHeartbeatTtl(heartbeatIvl * 6);
- LOG.info(
- String.format(
- "Event bus %s socket heartbeat configured: interval=%dms, timeout=%dms, ttl=%dms",
- socketType, heartbeatIvl, heartbeatIvl * 3, heartbeatIvl * 6));
+// In a new file: java/src/org/openqa/selenium/events/zeromq/ZmqUtils.java
+package org.openqa.selenium.events.zeromq;
+
+import java.time.Duration;
+import java.util.logging.Logger;
+import org.zeromq.ZMQ;
+
+class ZmqUtils {
+ private static final Logger LOG = Logger.getLogger(ZmqUtils.class.getName());
+
+ private ZmqUtils() {
+ // Utility class
+ }
+
+ static void configureHeartbeat(ZMQ.Socket socket, Duration heartbeatPeriod, String socketType) {
+ if (heartbeatPeriod != null && !heartbeatPeriod.isZero() && !heartbeatPeriod.isNegative()) {
+ long periodMillis = heartbeatPeriod.toMillis();
+ if (periodMillis > Integer.MAX_VALUE) {
+ LOG.warning(
+ String.format(
+ "Heartbeat period %dms is too large. Capping at %dms.",
+ periodMillis, Integer.MAX_VALUE));
+ periodMillis = Integer.MAX_VALUE;
+ }
+ int heartbeatIvl = (int) periodMillis;
+ socket.setHeartbeatIvl(heartbeatIvl);
+ // Set heartbeat timeout to 3x the interval
+ socket.setHeartbeatTimeout(heartbeatIvl * 3);
+ // Set heartbeat TTL to 6x the interval
+ socket.setHeartbeatTtl(heartbeatIvl * 6);
+ LOG.info(
+ String.format(
+ "Event bus %s socket heartbeat configured: interval=%dms, timeout=%dms, ttl=%dms",
+ socketType, heartbeatIvl, heartbeatIvl * 3, heartbeatIvl * 6));
+ }
}
}
+// In BoundZmqEventBus.java, replace the configureHeartbeat method with:
+private void configureHeartbeat(ZMQ.Socket socket, Duration heartbeatPeriod, String socketType) {
+ ZmqUtils.configureHeartbeat(socket, heartbeatPeriod, socketType);
+}
+Suggestion 8:
Remove redundant and unused code
Remove the redundant file javascript/grid-ui/src/hooks/useTheme.tsx. Its logic is already implemented in javascript/grid-ui/src/contexts/ThemeContext.tsx, and it is not used by the application.
javascript/grid-ui/src/hooks/useTheme.tsx [1-61]
-// Licensed to the Software Freedom Conservancy (SFC) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The SFC licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
+// This file should be deleted.
-import { useState, useEffect } from 'react'
-import { lightTheme, darkTheme } from '../theme/themes'
-
-type ThemeMode = 'light' | 'dark' | 'system'
-
-export const useTheme = () => {
- const [themeMode, setThemeMode] = useState<ThemeMode>('system')
- const [systemPrefersDark, setSystemPrefersDark] = useState(false)
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('theme-mode') as ThemeMode
- if (saved) setThemeMode(saved)
- setSystemPrefersDark(window.matchMedia('(prefers-color-scheme: dark)').matches)
- }
- }, [])
-
-
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- localStorage.setItem('theme-mode', themeMode)
- }
- }, [themeMode])
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
- const handler = (e: MediaQueryListEvent) => setSystemPrefersDark(e.matches)
- mediaQuery.addEventListener('change', handler)
- return () => mediaQuery.removeEventListener('change', handler)
- }
- }, [])
-
- const isDark = themeMode === 'dark' || (themeMode === 'system' && systemPrefersDark)
- const currentTheme = isDark ? darkTheme : lightTheme
-
- return {
- themeMode,
- setThemeMode,
- currentTheme,
- isDark
- }
-}
-[Auto-generated best practices - 2025-12-28]
This wiki is not where you want to be! Visit the Wiki Home for more useful links
Getting Involved
Triaging Issues
Releasing Selenium
Ruby Development
Python Bindings
Ruby Bindings
WebDriverJs
This content is being evaluated for where it belongs
Architectural Overview
Automation Atoms
HtmlUnitDriver
Lift Style API
LoadableComponent
Logging
PageFactory
RemoteWebDriver
Xpath In WebDriver
Moved to Official Documentation
Bot Style Tests
Buck
Continuous Integration
Crazy Fun Build
Design Patterns
Desired Capabilities
Developer Tips
Domain Driven Design
Firefox Driver
Firefox Driver Internals
Focus Stealing On Linux
Frequently Asked Questions
Google Summer Of Code
Grid Platforms
History
Internet Explorer Driver
InternetExplorerDriver Internals
Next Steps
PageObjects
RemoteWebDriverServer
Roadmap
Scaling WebDriver
SeIDE Release Notes
Selenium Emulation
Selenium Grid 4
Selenium Help
Shipping Selenium 3
The Team
TLC Meetings
Untrusted SSL Certificates
WebDriver For Mobile Browsers
Writing New Drivers