Skip to content

Commit 221b663

Browse files
Switch CLI tools to an Erlang JSON module; re-enable Dialyzer for 4 plugins
Elixir 1.18+ introduces a standard library JSON module which conflicts with the `json` library we've been using in CLI tools for `--formatter=json` and such. Since the two Elixir versions are not compatible and we have reasons to not bump Elixir too aggressively just yet, the most pragmatic option was to switch CLI tools to use `thoas`, the Erlang JSON parser RabbitMQ core and HTTP API use. This unblocks Dialyzer runs in four plugins disabled about 10 months ago and reveals a `rabbitmq_mqtt` Dialyzer failure due to a field that was removed from a record but not its type spec. Due to the MQTT plugin issue above, this can only be backported to `v4.3.x`, a `v4.2.x` version will need some tweaks.
1 parent 32d4158 commit 221b663

24 files changed

Lines changed: 157 additions & 136 deletions

.github/workflows/peer-discovery-aws.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
uses: erlef/setup-beam@v1
5252
with:
5353
otp-version: ${{ env.OTP_VERSION }}
54-
elixir-version: "1.18"
54+
elixir-version: "1.19"
5555
- name: SETUP ecs-cli
5656
if: steps.authorized.outputs.authorized == 'true'
5757
env:

.github/workflows/test-authnz.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- chrome
3636
include:
3737
- erlang_version: "27.3"
38-
elixir_version: 1.17.3
38+
elixir_version: 1.18
3939
env:
4040
SELENIUM_DIR: selenium
4141
DOCKER_NETWORK: rabbitmq_net

.github/workflows/test-make-type-check.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
plugin:
1818
# These are using plugin-specific test jobs.
1919
- rabbit
20-
# - rabbitmq_mqtt # disabled due to Elixir 1.18 JSON conficts
20+
- rabbitmq_mqtt
2121
- rabbitmq_peer_discovery_aws
2222
# These are from the test-plugin test job.
2323
- amqp10_client
@@ -57,14 +57,14 @@ jobs:
5757
- rabbitmq_shovel
5858
- rabbitmq_shovel_management
5959
- rabbitmq_shovel_prometheus
60-
# - rabbitmq_stomp # disabled due to Elixir 1.18 JSON conficts
61-
# - rabbitmq_stream # disabled due to Elixir 1.18 JSON conficts
60+
- rabbitmq_stomp
61+
- rabbitmq_stream
6262
- rabbitmq_stream_common
6363
- rabbitmq_stream_management
6464
- rabbitmq_tracing
6565
- rabbitmq_trust_store
6666
- rabbitmq_web_dispatch
67-
# - rabbitmq_web_mqtt # disabled due to Elixir 1.18 JSON conficts
67+
- rabbitmq_web_mqtt
6868
- rabbitmq_web_stomp
6969
# This one we do not want to run tests so no corresponding test job.
7070
- rabbitmq_ct_helpers

.github/workflows/test-make.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- '27'
2626
- '28'
2727
elixir_version:
28-
- '1.18'
28+
- '1.19'
2929
# @todo Add macOS and Windows.
3030
runs-on: ubuntu-latest
3131
timeout-minutes: 60
@@ -64,7 +64,7 @@ jobs:
6464
erlang_version:
6565
- '28'
6666
elixir_version:
67-
- '1.18'
67+
- '1.19'
6868
uses: ./.github/workflows/test-make-tests.yaml
6969
with:
7070
erlang_version: ${{ matrix.erlang_version }}
@@ -79,7 +79,7 @@ jobs:
7979
erlang_version:
8080
- '28'
8181
elixir_version:
82-
- '1.18'
82+
- '1.19'
8383
uses: ./.github/workflows/test-make-tests.yaml
8484
with:
8585
erlang_version: ${{ matrix.erlang_version }}
@@ -94,7 +94,7 @@ jobs:
9494
erlang_version: # Latest OTP
9595
- '28'
9696
elixir_version: # Latest Elixir
97-
- '1.18'
97+
- '1.19'
9898
uses: ./.github/workflows/test-make-type-check.yaml
9999
with:
100100
erlang_version: ${{ matrix.erlang_version }}

.github/workflows/test-management-ui-for-pr.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- chrome
2727
include:
2828
- erlang_version: "27.3"
29-
elixir_version: 1.17
29+
elixir_version: 1.18
3030
env:
3131
SELENIUM_DIR: selenium
3232
DOCKER_NETWORK: rabbitmq_net

.github/workflows/test-management-ui.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- chrome
3434
include:
3535
- erlang_version: "27.3"
36-
elixir_version: 1.17.3
36+
elixir_version: 1.18
3737
env:
3838
SELENIUM_DIR: selenium
3939
DOCKER_NETWORK: rabbitmq_net

.github/workflows/test-upgrades.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
uses: erlef/setup-beam@v1
6363
with: # Versions repeated later in this file.
6464
otp-version: '27'
65-
elixir-version: '1.18'
65+
elixir-version: '1.19'
6666
hexpm-mirrors: |
6767
https://builds.hex.pm
6868
https://cdn.jsdelivr.net/hex
@@ -123,7 +123,7 @@ jobs:
123123
uses: erlef/setup-beam@v1
124124
with:
125125
otp-version: '27'
126-
elixir-version: '1.18'
126+
elixir-version: '1.19'
127127
hexpm-mirrors: |
128128
https://builds.hex.pm
129129
https://cdn.jsdelivr.net/hex

Makefile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ XREF_SCOPE = app deps
3737
# protocols directly.
3838
XREF_IGNORE = [ \
3939
{'Elixir.CSV.Encode',impl_for,1}, \
40-
{'Elixir.JSON.Decoder',impl_for,1}, \
41-
{'Elixir.JSON.Encoder',impl_for,1}, \
4240
{'Elixir.RabbitMQ.CLI.Core.DataCoercion',impl_for,1}]
4341

4442
# Include Elixir libraries in the Xref checks.

deps/rabbitmq_cli/Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ define PROJECT_ENV
1313
endef
1414

1515
BUILD_DEPS = rabbit_common
16-
DEPS = csv json stdout_formatter
16+
DEPS = csv stdout_formatter
1717
LOCAL_DEPS = elixir
1818

1919
TEST_DEPS = amqp amqp_client temp x509 rabbit
2020

2121
dep_amqp = hex 3.3.0
2222
dep_csv = hex 3.2.1
23-
dep_json = hex 1.4.1
2423
dep_temp = hex 0.4.9
2524
dep_x509 = hex 0.9.2
2625

@@ -141,6 +140,12 @@ endif
141140
dialyzer:: escript
142141
MIX_ENV=test mix dialyzer
143142

143+
# rabbitmq_cli is a pure Elixir project, so erlang.mk's `dialyze` target
144+
# has no .erl files to analyze. Override it as a successful no-op; use
145+
# `gmake dialyzer` (above) for an Elixir Dialyzer run via Mix.
146+
dialyze:
147+
@:
148+
144149
.PHONY: install
145150

146151
install: $(ESCRIPT_FILE)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
## This Source Code Form is subject to the terms of the Mozilla Public
2+
## License, v. 2.0. If a copy of the MPL was not distributed with this
3+
## file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
##
5+
## Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
7+
defmodule RabbitMQ.CLI.Core.JSON do
8+
@moduledoc """
9+
Thin JSON facade used by the CLI tools.
10+
11+
Wraps `:thoas` so the rest of the codebase does not depend on a specific
12+
backend, and so the module name does not collide with the `JSON` module
13+
added to Elixir's standard library in 1.18.
14+
15+
`encode/1` returns `{:ok, binary}` and `decode/1` returns
16+
`{:ok, term} | {:error, term}`, matching the shape that callers throughout
17+
`rabbitmq_cli` already rely on.
18+
"""
19+
20+
@spec encode(term()) :: {:ok, binary()}
21+
def encode(term) do
22+
{:ok, :thoas.encode(normalize(term))}
23+
end
24+
25+
@spec decode(iodata()) :: {:ok, term()} | {:error, term()}
26+
def decode(bin) do
27+
:thoas.decode(bin)
28+
end
29+
30+
# Convert Erlang strings (lists of integers) to binaries for proper JSON
31+
# encoding and convert other Erlang-specific terms to readable strings.
32+
defp normalize(data) when is_function(data) do
33+
"Fun()"
34+
end
35+
36+
defp normalize(data) when is_pid(data) do
37+
"Pid(#{inspect(data)})"
38+
end
39+
40+
defp normalize(data) when is_port(data) do
41+
"Port(#{inspect(data)})"
42+
end
43+
44+
defp normalize(data) when is_reference(data) do
45+
"Ref(#{inspect(data)})"
46+
end
47+
48+
defp normalize(data) when is_binary(data) do
49+
convert_binary(data)
50+
end
51+
52+
defp normalize([]), do: []
53+
54+
# Likely a value like [5672], which we don't want to convert to the
55+
# equivalent unicode codepoint.
56+
defp normalize([val] = data) when is_integer(val) and val > 255 do
57+
data
58+
end
59+
60+
# Likely a value like [5672, 5682], which we don't want to convert to
61+
# the equivalent unicode codepoint.
62+
defp normalize([v0, v1] = data)
63+
when is_integer(v0) and v0 > 255 and is_integer(v1) and v1 > 255 do
64+
data
65+
end
66+
67+
defp normalize([b | rest]) when is_binary(b) do
68+
[convert_binary(b) | normalize(rest)]
69+
end
70+
71+
defp normalize(data) when is_list(data) do
72+
try do
73+
case :unicode.characters_to_binary(data, :utf8) do
74+
binary when is_binary(binary) ->
75+
binary
76+
77+
_ ->
78+
Enum.map(data, &normalize/1)
79+
end
80+
rescue
81+
ArgumentError ->
82+
Enum.map(data, &normalize/1)
83+
end
84+
end
85+
86+
defp normalize(data) when is_tuple(data) do
87+
data
88+
|> Tuple.to_list()
89+
|> Enum.map(&normalize/1)
90+
|> List.to_tuple()
91+
end
92+
93+
defp normalize(data) when is_map(data) do
94+
Enum.into(data, %{}, fn {k, v} -> {normalize(k), normalize(v)} end)
95+
end
96+
97+
defp normalize(data), do: data
98+
99+
defp convert_binary(data) when is_binary(data) do
100+
try do
101+
case :unicode.characters_to_binary(data, :utf8) do
102+
binary when is_binary(binary) ->
103+
binary
104+
105+
_ ->
106+
Base.encode64(data)
107+
end
108+
rescue
109+
ArgumentError ->
110+
Base.encode64(data)
111+
end
112+
end
113+
end

0 commit comments

Comments
 (0)