THRIFT-5635 Update erlang client for Erlang 23-25
Client: erl
Patch: Sergey Yelin

This closes #2677

Summary of changes:
 - Add useful compiler options
 - Format sources using erlfmt
 - Switch to modern callbacks in thrift_* modules
 - Add static analysis (dialyzer), disabled by default
 - Add/fix types for API calls

NOTE: Enabling static analysis requires additional tweaks in multiplexer module.
diff --git a/lib/erl/src/thrift_protocol.erl b/lib/erl/src/thrift_protocol.erl
index 2fe10d6..c75712a 100644
--- a/lib/erl/src/thrift_protocol.erl
+++ b/lib/erl/src/thrift_protocol.erl
@@ -19,45 +19,67 @@
 
 -module(thrift_protocol).
 
--export([new/2,
-         write/2,
-         read/2,
-         read/3,
-         skip/2,
-         flush_transport/1,
-         close_transport/1,
-         typeid_to_atom/1
-        ]).
-
--export([behaviour_info/1]).
+-export([
+    new/2,
+    write/2,
+    read/2,
+    read/3,
+    skip/2,
+    flush_transport/1,
+    close_transport/1,
+    typeid_to_atom/1
+]).
 
 -include("thrift_constants.hrl").
 -include("thrift_protocol.hrl").
 
--record(protocol, {module, data}).
+-record(protocol, {
+    module :: module(),
+    data :: term()
+}).
 
-behaviour_info(callbacks) ->
-    [
-     {read, 2},
-     {write, 2},
-     {flush_transport, 1},
-     {close_transport, 1}
-    ];
-behaviour_info(_Else) -> undefined.
+%%%=========================================================================
+%%%  API
+%%%=========================================================================
+-type state() :: term().
+-export_type([state/0]).
+-type reason() :: term().
+-export_type([reason/0]).
+
+%% NOTE: keep this in sync with read/2 spec
+-callback read
+    (state(), {struct, _Info}) -> {state(), {ok, tuple()} | {error, reason()}};
+    (state(), tprot_cont_tag()) -> {state(), {ok, any()} | {error, reason()}};
+    (state(), tprot_empty_tag()) -> {state(), ok | {error, reason()}};
+    (state(), tprot_header_tag()) -> {state(), tprot_header_val() | {error, reason()}};
+    (state(), tprot_data_tag()) -> {state(), {ok, any()} | {error, reason()}}.
+
+-callback write(state(), any()) -> {state(), ok | {error, reason()}}.
+
+-callback flush_transport(state()) -> {state(), ok | {error, reason()}}.
+-callback close_transport(state()) -> {state(), ok | {error, reason()}}.
 
 new(Module, Data) when is_atom(Module) ->
-    {ok, #protocol{module = Module,
-                   data = Data}}.
+    {ok, #protocol{
+        module = Module,
+        data = Data
+    }}.
 
 -spec flush_transport(#protocol{}) -> {#protocol{}, ok}.
-flush_transport(Proto = #protocol{module = Module,
-                                  data = Data}) ->
+flush_transport(
+    Proto = #protocol{
+        module = Module,
+        data = Data
+    }
+) ->
     {NewData, Result} = Module:flush_transport(Data),
     {Proto#protocol{data = NewData}, Result}.
 
 -spec close_transport(#protocol{}) -> ok.
-close_transport(#protocol{module = Module,
-                          data = Data}) ->
+close_transport(#protocol{
+    module = Module,
+    data = Data
+}) ->
     Module:close_transport(Data).
 
 typeid_to_atom(?tType_STOP) -> field_stop;
@@ -91,103 +113,123 @@
 %% Structure is like:
 %%    [{Fid, Type}, ...]
 -spec read(#protocol{}, {struct, _StructDef}, atom()) -> {#protocol{}, {ok, tuple()}}.
-read(IProto0, {struct, Structure}, Tag)
-  when is_list(Structure), is_atom(Tag) ->
-
+read(IProto0, {struct, Structure}, Tag) when
+    is_list(Structure), is_atom(Tag)
+->
     % If we want a tagged tuple, we need to offset all the tuple indices
     % by 1 to avoid overwriting the tag.
-    Offset = if Tag =/= undefined -> 1; true -> 0 end,
-    IndexList = case length(Structure) of
-                    N when N > 0 -> lists:seq(1 + Offset, N + Offset);
-                    _ -> []
-                end,
+    Offset =
+        if
+            Tag =/= undefined -> 1;
+            true -> 0
+        end,
+    IndexList =
+        case length(Structure) of
+            N when N > 0 -> lists:seq(1 + Offset, N + Offset);
+            _ -> []
+        end,
 
-    SWithIndices = [{Fid, {Type, Index}} ||
-                       {{Fid, Type}, Index} <-
-                           lists:zip(Structure, IndexList)],
+    SWithIndices = [
+        {Fid, {Type, Index}}
+     || {{Fid, Type}, Index} <-
+            lists:zip(Structure, IndexList)
+    ],
     % Fid -> {Type, Index}
     SDict = dict:from_list(SWithIndices),
 
     {IProto1, ok} = read(IProto0, struct_begin),
     RTuple0 = erlang:make_tuple(length(Structure) + Offset, undefined),
-    RTuple1 = if Tag =/= undefined -> setelement(1, RTuple0, Tag);
-                 true              -> RTuple0
-              end,
+    RTuple1 =
+        if
+            Tag =/= undefined -> setelement(1, RTuple0, Tag);
+            true -> RTuple0
+        end,
 
     {IProto2, RTuple2} = read_struct_loop(IProto1, SDict, RTuple1),
     {IProto2, {ok, RTuple2}}.
 
-
-%% NOTE: Keep this in sync with thrift_protocol_behaviour:read
+%% NOTE: Keep this in sync with read callback
 -spec read
-        (#protocol{}, {struct, _Info}) ->    {#protocol{}, {ok, tuple()}      | {error, _Reason}};
-        (#protocol{}, tprot_cont_tag()) ->   {#protocol{}, {ok, any()}        | {error, _Reason}};
-        (#protocol{}, tprot_empty_tag()) ->  {#protocol{},  ok                | {error, _Reason}};
-        (#protocol{}, tprot_header_tag()) -> {#protocol{}, tprot_header_val() | {error, _Reason}};
-        (#protocol{}, tprot_data_tag()) ->   {#protocol{}, {ok, any()}        | {error, _Reason}}.
+    (#protocol{}, {struct, _Info}) -> {#protocol{}, {ok, tuple()} | {error, _Reason}};
+    (#protocol{}, tprot_cont_tag()) -> {#protocol{}, {ok, any()} | {error, _Reason}};
+    (#protocol{}, tprot_empty_tag()) -> {#protocol{}, ok | {error, _Reason}};
+    (#protocol{}, tprot_header_tag()) -> {#protocol{}, tprot_header_val() | {error, _Reason}};
+    (#protocol{}, tprot_data_tag()) -> {#protocol{}, {ok, any()} | {error, _Reason}}.
 
-read(IProto, {struct, {Module, StructureName}}) when is_atom(Module),
-                                                     is_atom(StructureName) ->
+read(IProto, {struct, {Module, StructureName}}) when
+    is_atom(Module),
+    is_atom(StructureName)
+->
     read(IProto, Module:struct_info(StructureName), StructureName);
-
-read(IProto, S={struct, Structure}) when is_list(Structure) ->
+read(IProto, S = {struct, Structure}) when is_list(Structure) ->
     read(IProto, S, undefined);
-
 read(IProto0, {list, Type}) ->
     {IProto1, #protocol_list_begin{etype = EType, size = Size}} =
         read(IProto0, list_begin),
     {EType, EType} = {term_to_typeid(Type), EType},
-    {List, IProto2} = lists:mapfoldl(fun(_, ProtoS0) ->
-                                             {ProtoS1, {ok, Item}} = read(ProtoS0, Type),
-                                             {Item, ProtoS1}
-                                     end,
-                                     IProto1,
-                                     lists:duplicate(Size, 0)),
+    {List, IProto2} = lists:mapfoldl(
+        fun(_, ProtoS0) ->
+            {ProtoS1, {ok, Item}} = read(ProtoS0, Type),
+            {Item, ProtoS1}
+        end,
+        IProto1,
+        lists:duplicate(Size, 0)
+    ),
     {IProto3, ok} = read(IProto2, list_end),
     {IProto3, {ok, List}};
-
 read(IProto0, {map, KeyType, ValType}) ->
     {IProto1, #protocol_map_begin{size = Size, ktype = KType, vtype = VType}} =
         read(IProto0, map_begin),
-    _ = case Size of
-      0 -> 0;
-      _ ->
-        {KType, KType} = {term_to_typeid(KeyType), KType},
-        {VType, VType} = {term_to_typeid(ValType), VType}
-    end,
-    {List, IProto2} = lists:mapfoldl(fun(_, ProtoS0) ->
-                                             {ProtoS1, {ok, Key}} = read(ProtoS0, KeyType),
-                                             {ProtoS2, {ok, Val}} = read(ProtoS1, ValType),
-                                             {{Key, Val}, ProtoS2}
-                                     end,
-                                     IProto1,
-                                     lists:duplicate(Size, 0)),
+    _ =
+        case Size of
+            0 ->
+                0;
+            _ ->
+                {KType, KType} = {term_to_typeid(KeyType), KType},
+                {VType, VType} = {term_to_typeid(ValType), VType}
+        end,
+    {List, IProto2} = lists:mapfoldl(
+        fun(_, ProtoS0) ->
+            {ProtoS1, {ok, Key}} = read(ProtoS0, KeyType),
+            {ProtoS2, {ok, Val}} = read(ProtoS1, ValType),
+            {{Key, Val}, ProtoS2}
+        end,
+        IProto1,
+        lists:duplicate(Size, 0)
+    ),
     {IProto3, ok} = read(IProto2, map_end),
     {IProto3, {ok, dict:from_list(List)}};
-
 read(IProto0, {set, Type}) ->
     {IProto1, #protocol_set_begin{etype = EType, size = Size}} =
         read(IProto0, set_begin),
     {EType, EType} = {term_to_typeid(Type), EType},
-    {List, IProto2} = lists:mapfoldl(fun(_, ProtoS0) ->
-                                             {ProtoS1, {ok, Item}} = read(ProtoS0, Type),
-                                             {Item, ProtoS1}
-                                     end,
-                                     IProto1,
-                                     lists:duplicate(Size, 0)),
+    {List, IProto2} = lists:mapfoldl(
+        fun(_, ProtoS0) ->
+            {ProtoS1, {ok, Item}} = read(ProtoS0, Type),
+            {Item, ProtoS1}
+        end,
+        IProto1,
+        lists:duplicate(Size, 0)
+    ),
     {IProto3, ok} = read(IProto2, set_end),
     {IProto3, {ok, sets:from_list(List)}};
-
 read(Protocol, ProtocolType) ->
     read_specific(Protocol, ProtocolType).
 
-%% NOTE: Keep this in sync with thrift_protocol_behaviour:read
+%% NOTE: Keep this in sync with read/2 spec
 -spec read_specific
-        (#protocol{}, tprot_empty_tag()) ->  {#protocol{},  ok                | {error, _Reason}};
-        (#protocol{}, tprot_header_tag()) -> {#protocol{}, tprot_header_val() | {error, _Reason}};
-        (#protocol{}, tprot_data_tag()) ->   {#protocol{}, {ok, any()}        | {error, _Reason}}.
-read_specific(Proto = #protocol{module = Module,
-                                data = ModuleData}, ProtocolType) ->
+    (#protocol{}, {struct, _Info}) -> {#protocol{}, {ok, tuple()} | {error, _Reason}};
+    (#protocol{}, tprot_cont_tag()) -> {#protocol{}, {ok, any()} | {error, _Reason}};
+    (#protocol{}, tprot_empty_tag()) -> {#protocol{}, ok | {error, _Reason}};
+    (#protocol{}, tprot_header_tag()) -> {#protocol{}, tprot_header_val() | {error, _Reason}};
+    (#protocol{}, tprot_data_tag()) -> {#protocol{}, {ok, any()} | {error, _Reason}}.
+read_specific(
+    Proto = #protocol{
+        module = Module,
+        data = ModuleData
+    },
+    ProtocolType
+) ->
     {NewData, Result} = Module:read(ModuleData, ProtocolType),
     {Proto#protocol{data = NewData}, Result}.
 
@@ -209,8 +251,9 @@
                             read_struct_loop(IProto3, SDict, NewRTuple);
                         Expected ->
                             error_logger:info_msg(
-                              "Skipping field ~p with wrong type (~p != ~p)~n",
-                              [Fid, FType, Expected]),
+                                "Skipping field ~p with wrong type (~p != ~p)~n",
+                                [Fid, FType, Expected]
+                            ),
                             skip_field(FType, IProto1, SDict, RTuple)
                     end;
                 _Else2 ->
@@ -230,30 +273,25 @@
     {Proto2, ok} = skip_struct_loop(Proto1),
     {Proto3, ok} = read(Proto2, struct_end),
     {Proto3, ok};
-
 skip(Proto0, map) ->
     {Proto1, Map} = read(Proto0, map_begin),
     {Proto2, ok} = skip_map_loop(Proto1, Map),
     {Proto3, ok} = read(Proto2, map_end),
     {Proto3, ok};
-
 skip(Proto0, set) ->
     {Proto1, Set} = read(Proto0, set_begin),
     {Proto2, ok} = skip_set_loop(Proto1, Set),
     {Proto3, ok} = read(Proto2, set_end),
     {Proto3, ok};
-
 skip(Proto0, list) ->
     {Proto1, List} = read(Proto0, list_begin),
     {Proto2, ok} = skip_list_loop(Proto1, List),
     {Proto3, ok} = read(Proto2, list_end),
     {Proto3, ok};
-
 skip(Proto0, Type) when is_atom(Type) ->
     {Proto1, _Ignore} = read(Proto0, Type),
     {Proto1, ok}.
 
-
 skip_struct_loop(Proto0) ->
     {Proto1, #protocol_field_begin{type = Type}} = read(Proto0, field_begin),
     case Type of
@@ -265,39 +303,62 @@
             skip_struct_loop(Proto3)
     end.
 
-skip_map_loop(Proto0, Map = #protocol_map_begin{ktype = Ktype,
-                                                vtype = Vtype,
-                                                size = Size}) ->
+skip_map_loop(
+    Proto0,
+    Map = #protocol_map_begin{
+        ktype = Ktype,
+        vtype = Vtype,
+        size = Size
+    }
+) ->
     case Size of
         N when N > 0 ->
             {Proto1, ok} = skip(Proto0, typeid_to_atom(Ktype)),
             {Proto2, ok} = skip(Proto1, typeid_to_atom(Vtype)),
-            skip_map_loop(Proto2,
-                          Map#protocol_map_begin{size = Size - 1});
-        0 -> {Proto0, ok}
+            skip_map_loop(
+                Proto2,
+                Map#protocol_map_begin{size = Size - 1}
+            );
+        0 ->
+            {Proto0, ok}
     end.
 
-skip_set_loop(Proto0, Map = #protocol_set_begin{etype = Etype,
-                                                size = Size}) ->
+skip_set_loop(
+    Proto0,
+    Map = #protocol_set_begin{
+        etype = Etype,
+        size = Size
+    }
+) ->
     case Size of
         N when N > 0 ->
             {Proto1, ok} = skip(Proto0, typeid_to_atom(Etype)),
-            skip_set_loop(Proto1,
-                          Map#protocol_set_begin{size = Size - 1});
-        0 -> {Proto0, ok}
+            skip_set_loop(
+                Proto1,
+                Map#protocol_set_begin{size = Size - 1}
+            );
+        0 ->
+            {Proto0, ok}
     end.
 
-skip_list_loop(Proto0, Map = #protocol_list_begin{etype = Etype,
-                                                  size = Size}) ->
+skip_list_loop(
+    Proto0,
+    Map = #protocol_list_begin{
+        etype = Etype,
+        size = Size
+    }
+) ->
     case Size of
         N when N > 0 ->
             {Proto1, ok} = skip(Proto0, typeid_to_atom(Etype)),
-            skip_list_loop(Proto1,
-                           Map#protocol_list_begin{size = Size - 1});
-        0 -> {Proto0, ok}
+            skip_list_loop(
+                Proto1,
+                Map#protocol_list_begin{size = Size - 1}
+            );
+        0 ->
+            {Proto0, ok}
     end.
 
-
 %%--------------------------------------------------------------------
 %% Function: write(OProto, {Type, Data}) -> ok
 %%
@@ -318,95 +379,112 @@
 %%--------------------------------------------------------------------
 -spec write(#protocol{}, any()) -> {#protocol{}, ok | {error, _Reason}}.
 
-write(Proto0, {{struct, StructDef}, Data})
-  when is_list(StructDef), is_tuple(Data), length(StructDef) == size(Data) - 1 ->
-
+write(Proto0, {{struct, StructDef}, Data}) when
+    is_list(StructDef), is_tuple(Data), length(StructDef) == size(Data) - 1
+->
     [StructName | Elems] = tuple_to_list(Data),
     {Proto1, ok} = write(Proto0, #protocol_struct_begin{name = StructName}),
     {Proto2, ok} = struct_write_loop(Proto1, StructDef, Elems),
     {Proto3, ok} = write(Proto2, struct_end),
     {Proto3, ok};
-
-write(Proto, {{struct, {Module, StructureName}}, Data})
-  when is_atom(Module),
-       is_atom(StructureName),
-       element(1, Data) =:= StructureName ->
+write(Proto, {{struct, {Module, StructureName}}, Data}) when
+    is_atom(Module),
+    is_atom(StructureName),
+    element(1, Data) =:= StructureName
+->
     write(Proto, {Module:struct_info(StructureName), Data});
-
-write(_, {{struct, {Module, StructureName}}, Data})
-  when is_atom(Module),
-       is_atom(StructureName) ->
-    erlang:error(struct_unmatched, {{provided, element(1, Data)},
-                             {expected, StructureName}});
-
-write(Proto0, {{list, Type}, Data})
-  when is_list(Data) ->
-    {Proto1, ok} = write(Proto0,
-               #protocol_list_begin{
-                 etype = term_to_typeid(Type),
-                 size = length(Data)
-                }),
-    Proto2 = lists:foldl(fun(Elem, ProtoIn) ->
-                                 {ProtoOut, ok} = write(ProtoIn, {Type, Elem}),
-                                 ProtoOut
-                         end,
-                         Proto1,
-                         Data),
+write(_, {{struct, {Module, StructureName}}, Data}) when
+    is_atom(Module),
+    is_atom(StructureName)
+->
+    erlang:error(struct_unmatched, {{provided, element(1, Data)}, {expected, StructureName}});
+write(Proto0, {{list, Type}, Data}) when
+    is_list(Data)
+->
+    {Proto1, ok} = write(
+        Proto0,
+        #protocol_list_begin{
+            etype = term_to_typeid(Type),
+            size = length(Data)
+        }
+    ),
+    Proto2 = lists:foldl(
+        fun(Elem, ProtoIn) ->
+            {ProtoOut, ok} = write(ProtoIn, {Type, Elem}),
+            ProtoOut
+        end,
+        Proto1,
+        Data
+    ),
     {Proto3, ok} = write(Proto2, list_end),
     {Proto3, ok};
-
 write(Proto0, {{map, KeyType, ValType}, Data}) ->
-    {Proto1, ok} = write(Proto0,
-                         #protocol_map_begin{
-                           ktype = term_to_typeid(KeyType),
-                           vtype = term_to_typeid(ValType),
-                           size  = dict:size(Data)
-                          }),
-    Proto2 = dict:fold(fun(KeyData, ValData, ProtoS0) ->
-                               {ProtoS1, ok} = write(ProtoS0, {KeyType, KeyData}),
-                               {ProtoS2, ok} = write(ProtoS1, {ValType, ValData}),
-                               ProtoS2
-                       end,
-                       Proto1,
-                       Data),
+    {Proto1, ok} = write(
+        Proto0,
+        #protocol_map_begin{
+            ktype = term_to_typeid(KeyType),
+            vtype = term_to_typeid(ValType),
+            size = dict:size(Data)
+        }
+    ),
+    Proto2 = dict:fold(
+        fun(KeyData, ValData, ProtoS0) ->
+            {ProtoS1, ok} = write(ProtoS0, {KeyType, KeyData}),
+            {ProtoS2, ok} = write(ProtoS1, {ValType, ValData}),
+            ProtoS2
+        end,
+        Proto1,
+        Data
+    ),
     {Proto3, ok} = write(Proto2, map_end),
     {Proto3, ok};
-
 write(Proto0, {{set, Type}, Data}) ->
     true = sets:is_set(Data),
-    {Proto1, ok} = write(Proto0,
-                         #protocol_set_begin{
-                           etype = term_to_typeid(Type),
-                           size  = sets:size(Data)
-                          }),
-    Proto2 = sets:fold(fun(Elem, ProtoIn) ->
-                               {ProtoOut, ok} = write(ProtoIn, {Type, Elem}),
-                               ProtoOut
-                       end,
-                       Proto1,
-                       Data),
+    {Proto1, ok} = write(
+        Proto0,
+        #protocol_set_begin{
+            etype = term_to_typeid(Type),
+            size = sets:size(Data)
+        }
+    ),
+    Proto2 = sets:fold(
+        fun(Elem, ProtoIn) ->
+            {ProtoOut, ok} = write(ProtoIn, {Type, Elem}),
+            ProtoOut
+        end,
+        Proto1,
+        Data
+    ),
     {Proto3, ok} = write(Proto2, set_end),
     {Proto3, ok};
-
-write(Proto = #protocol{module = Module,
-                        data = ModuleData}, Data) ->
+write(
+    Proto = #protocol{
+        module = Module,
+        data = ModuleData
+    },
+    Data
+) ->
     {NewData, Result} = Module:write(ModuleData, Data),
     {Proto#protocol{data = NewData}, Result}.
 
 struct_write_loop(Proto0, [{Fid, Type} | RestStructDef], [Data | RestData]) ->
-    NewProto = case Data of
-                   undefined ->
-                       Proto0; % null fields are skipped in response
-                   _ ->
-                       {Proto1, ok} = write(Proto0,
-                                           #protocol_field_begin{
-                                             type = term_to_typeid(Type),
-                                             id = Fid
-                                            }),
-                       {Proto2, ok} = write(Proto1, {Type, Data}),
-                       {Proto3, ok} = write(Proto2, field_end),
-                       Proto3
-               end,
+    NewProto =
+        case Data of
+            undefined ->
+                % null fields are skipped in response
+                Proto0;
+            _ ->
+                {Proto1, ok} = write(
+                    Proto0,
+                    #protocol_field_begin{
+                        type = term_to_typeid(Type),
+                        id = Fid
+                    }
+                ),
+                {Proto2, ok} = write(Proto1, {Type, Data}),
+                {Proto3, ok} = write(Proto2, field_end),
+                Proto3
+        end,
     struct_write_loop(NewProto, RestStructDef, RestData);
 struct_write_loop(Proto, [], []) ->
     write(Proto, field_stop).