#!/usr/bin/env escript
%% -*- erlang -*-
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2020-2022. All Rights Reserved.
%%
%% Licensed 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.
%%
%% %CopyrightEnd%
%%
-mode(compile).
-compile(warnings_as_errors).
-import(lists, [foldl/3,sort/1]).
-record(st,
{functions = [],
types = [],
deprecations = #{}}).
main(["update",Top]) ->
St0 = summarize(Top),
St = check_deprecations(Top, St0),
emit(Top, St),
halt(0);
main(["make_xml",Type,Top,Outfile]) ->
St = summarize(Top),
make_xml(Top, Type, Outfile, St#st.functions),
halt(0).
ebin_directories(Top) ->
AppDirs0 = filelib:wildcard(filename:join(Top, "lib/*/ebin")),
%% Filter out erl_interface and jinterface since they lack Erlang code, and
%% ODBC because we can't build it on all platforms we develop on. This must
%% be fixed before we deprecate or remove functionality in ODBC.
AppDirs = [Dir || Dir <- AppDirs0,
not lists:suffix("erl_interface/ebin", Dir),
not lists:suffix("jinterface/ebin", Dir),
not lists:suffix("odbc/ebin", Dir)],
[filename:join(Top, "erts/preloaded/ebin")] ++ AppDirs.
summarize(Top) ->
Directories = ebin_directories(Top),
foldl(fun summarize_directory/2, #st{}, Directories).
summarize_directory(Dir, Acc) ->
Files = [filename:join(Dir, F) || F <- filelib:wildcard("*.beam", Dir)],
case Files of
[_|_] ->
foldl(fun summarize_file/2, Acc, Files);
[] ->
Msg = io_lib:format("~p doesn't appear to be built. Make sure to "
"build all OTP applications before updating "
"deprecations.\n", [Dir]),
io:put_chars(standard_error, [Msg]),
halt(1)
end.
summarize_file(File, Acc) ->
{ok, {Module, [Chunk]}} = beam_lib:chunks(File, [attributes]),
{attributes, Attributes} = Chunk,
summarize_attributes(Attributes, Module, Acc).
summarize_attributes([{deprecated, Ds} | As], Module, Acc0) ->
Fs = sa_1(Ds, deprecated, Module, Acc0#st.functions),
Acc = Acc0#st{ functions = Fs },
summarize_attributes(As, Module, Acc);
summarize_attributes([{removed, Rs} | As], Module, Acc0) ->
Fs = sa_1(Rs, removed, Module, Acc0#st.functions),
Acc = Acc0#st{ functions = Fs },
summarize_attributes(As, Module, Acc);
summarize_attributes([{deprecated_type, Ds} | As], Module, Acc0) ->
Ts = sa_1(Ds, deprecated, Module, Acc0#st.types),
Acc = Acc0#st{ types = Ts },
summarize_attributes(As, Module, Acc);
summarize_attributes([{removed_type, Rs} | As], Module, Acc0) ->
Ts = sa_1(Rs, removed, Module, Acc0#st.types),
Acc = Acc0#st{ types = Ts },
summarize_attributes(As, Module, Acc);
summarize_attributes([_ | As], Module, Acc) ->
summarize_attributes(As, Module, Acc);
summarize_attributes([], _Module, Acc) ->
Acc.
sa_1([{F, A, Info} | As], Tag, Module, Acc0) ->
sa_1(As, Tag, Module, [{Tag, Module, F, A, Info} | Acc0]);
sa_1([{F, A} | As], Tag, Module, Acc0) ->
sa_1(As, Tag, Module, [{Tag, Module, F, A, undefined} | Acc0]);
sa_1([module | As], Tag, Module, Acc0) ->
sa_1(As, Tag, Module, [{Tag, Module, '_', '_', undefined} | Acc0]);
sa_1([], _Tag, _Module, Acc) ->
Acc.
%%
emit(Top, #st{ functions = Fs0, types = Ts, deprecations = Depr }) ->
Fs = insert_removals(Fs0, Depr),
Name = filename:join(Top, "lib/stdlib/src/otp_internal.erl"),
Contents = ["%%\n"
"%% WARNING: DO NOT EDIT THIS FILE.\n"
"%%\n"
"%% This file was auto-generated from attributes in the source\n"
"%% code.\n"
"%%\n"
"%% To add a description to a deprecation or removal attribute,\n"
"%% write a string after the arity:\n"
"%%\n"
"%% -deprecated([{foo,1,\"use bar/1 instead\"}]).\n"
"%% -deprecated_type([{gadget,1,\"use widget/1 instead\"}]).\n"
"%% -removed([{hello,2,\"use there/2 instead\"}]).\n"
"%% -removed_type([{frobnitz,1,\"use grunka/1 instead\"}]).\n"
"%%\n"
"%% Descriptions cannot be given with the `f/1` shorthand, and\n"
"%% it will fall back to a generic description referring the\n"
"%% user to the documentation.\n"
"%%\n"
"%% Use `./otp_build update_deprecations` to update this file\n"
"%% after adding an attribute.\n"
"%%\n"
"-module(otp_internal).\n"
"-include(\"otp_internal.hrl\").\n"
"%%\n",
emit_function("obsolete", Fs),
emit_function("obsolete_type", Ts)],
ok = file:write_file(Name, Contents),
ok.
emit_function(FuncName, Entries) ->
[io_lib:format("-dialyzer({no_match, ~ts/3}).\n", [FuncName]),
[emit_clause(FuncName, E) || E <- sort_clauses(Entries)],
io_lib:format("~ts(_,_,_) -> no.\n\n", [FuncName])].
sort_clauses(Entries) ->
Tagged = [{clause_order(E), E} || E <- Entries],
[E || {_, E} <- sort(Tagged)].
clause_order({_Tag, _Module, F, A, _Info}=Entry) ->
{clause_order(F, A), Entry};
clause_order({_Tag, _Module, F, A, _Info, _Rel}) ->
{clause_order(F, A), {_Tag, _Module, F, A, _Info}}.
%% Wildcard matches must be emitted *after* specific matches to avoid
%% losing descriptions.
clause_order(F, A) when F =/= '_', A =/= '_' -> 0;
clause_order(F, '_') when F =/= '_' -> 1;
clause_order('_', A) when A =/= '_' -> 2;
clause_order('_', '_') -> 3.
emit_clause(FuncName, {Tag, M, F, A, Info}) ->
io_lib:format("~ts(~ts, ~ts, ~ts) ->\n"
" {~p, ~p};\n",
[FuncName, match_string(M), match_string(F), match_string(A),
Tag, info_string(Info)]);
emit_clause(FuncName, {Tag, M, F, A, Info, Rel}) ->
io_lib:format("~ts(~ts, ~ts, ~ts) ->\n"
" {~p, ~p, ~p};\n",
[FuncName, match_string(M), match_string(F), match_string(A),
Tag, info_string(Info), Rel]).
%%
info_string(undefined) ->
"see the documentation for details";
info_string(next_version) ->
"will be removed in the next version. "
"See the documentation for details";
info_string(next_major_release) ->
"will be removed in the next major release. "
"See the documentation for details";
info_string(eventually) ->
"will be removed in a future release. "
"See the documentation for details";
info_string(String) when is_list(String) ->
String.
match_string('_') -> "_";
match_string(Term) -> io_lib:format("~p", [Term]).
%%
insert_removals([{deprecated,M,F,A,Info}=Entry|T], Depr) ->
Key = {M,F,A},
case Depr of
#{Key := Ps} ->
case lists:keyfind(remove, 1, Ps) of
false ->
[Entry|insert_removals(T, Depr)];
{remove,Rel0} ->
Rel = lists:concat(["OTP ",Rel0]),
[{deprecated,M,F,A,Info,Rel}|insert_removals(T, Depr)]
end;
#{} ->
[Entry|insert_removals(T, Depr)]
end;
insert_removals([H|T], Depr) ->
[H|insert_removals(T, Depr)];
insert_removals([], _Depr) ->
[].
%%%
%%% Create XML files.
%%%
make_xml(Top, Type, OutFile, InfoText0) ->
DeprecationFile = deprecation_file(Top),
OutDir = filename:dirname(DeprecationFile),
Depr0 = read_deprecations(DeprecationFile),
Depr = maps:to_list(Depr0),
{RelKey, AttrTag} =
case Type of
"deprecations" ->
%% Group by 'since' in DEPRECATIONS, grab text from
%% 'deprecated' attributes.
{since, deprecated};
"scheduled_for_removal" ->
{remove, deprecated};
"removed" ->
{remove, removed}
end,
InfoTextMap = maps:from_list(make_xml_info(InfoText0, AttrTag)),
Collected = make_xml_collect(Depr, RelKey, InfoTextMap, []),
case Type of
"removed" ->
{ok, Vsn} = file:read_file(filename:join(Top,"OTP_VERSION")),
[Release|_] = string:split(Vsn,"."),
lists:foreach(
fun({Rel, Functions}) ->
case Rel > binary_to_integer(Release) of
true ->
io:format(standard_error,
"Some functions have been marked "
"as removed in the future: ~n~p~n",
[Functions]),
halt(1);
false ->
ok
end
end, Collected);
_ ->
ok
end,
All = make_xml_gen(lists:reverse(Collected), Type, OutDir),
ok = file:write_file(OutFile, All).
make_xml_info([{Tag,M,F,A,Text} | Attributes], Tag) ->
[{{M,F,A}, info_string(Text)} | make_xml_info(Attributes, Tag)];
make_xml_info([_ | Attributes], Tag) ->
make_xml_info(Attributes, Tag);
make_xml_info([], _Tag) ->
[].
%% Joins `DEPRECATIONS` with module attributes, grabbing the text from said
%% attributes and grouping them by the release version pointed out by `RelKey`
%% ('since' or 'remove').
make_xml_collect([{MFA, Ps} | T], RelKey, InfoTextMap, Acc0) ->
Acc = case lists:keyfind(RelKey, 1, Ps) of
{RelKey, Rel} ->
case InfoTextMap of
#{ MFA := Text } ->
[{Rel, {MFA,Text}} | Acc0];
#{} ->
Acc0
end;
false ->
Acc0
end,
make_xml_collect(T, RelKey, InfoTextMap, Acc);
make_xml_collect([], _RelKey, _InfoTextMap, Acc) ->
rel2fam(Acc).
make_xml_gen(Collected, Type, Dir) ->
Head = get_xml_template(Dir, Type, head),
Contents = make_xml_gen_list(Collected, Type, Dir),
Footer = "\n",
[Head,Contents,Footer].
make_xml_gen_list([{Rel,MFAs}|T], Type, Dir) ->
RelStr = lists:concat(["OTP ",Rel]),
RelMarker = lists:concat(["otp-",Rel]),
Head = ["\n",
"\n",
"",RelStr,"\n"],
Footer = "\n",
SubTitle = case Type of
"deprecations" ->
["Functions Deprecated in ",RelStr];
"scheduled_for_removal" ->
["Functions Scheduled for Removal in ",RelStr];
"removed" ->
["Functions Removed in ",RelStr]
end,
SubHead = ["\n",
"",SubTitle,"\n"],
SubFooter = "\n",
[Head, get_xml_template(Dir, Type, Rel),
SubHead, make_xml_gen_mfas(MFAs), SubFooter,
Footer | make_xml_gen_list(T, Type, Dir)];
make_xml_gen_list([], _, _) ->
[].
make_xml_gen_mfas(MFAs) ->
["\n",
[make_xml_item(MFA) || MFA <- MFAs],
"
\n"].
make_xml_item({{M,F,A},Text}) ->
["- ",lists:concat([M,":",F,"/",A]),"",
" (",Text,")
\n"].
get_xml_template(Dir, Prefix, Key) ->
Name = lists:concat([Prefix,"_",Key,".inc"]),
File = filename:join(Dir, Name),
case file:read_file(File) of
{ok,Contents} ->
Contents;
{error,enoent} ->
[]
end.
%%%
%%% Cross-checks deprecations against DEPRECATIONS file.
%%%
check_deprecations(Top, #st{functions = Fs} = St) ->
DeprFile = deprecation_file(Top),
Depr = read_deprecations(DeprFile),
Bad0 = [F || F <- Fs, not in_deprecations(F, Depr)],
case Bad0 of
[] ->
St#st{deprecations = Depr};
[_|_] ->
Msg = "The following function(s) have -deprecated() or "
"-removed() attributes, but are not present in the "
"DEPRECATIONS file:\n\n",
Bad = [io_lib:format(" ~w:~w/~w\n", [M,F,A]) ||
{_,M,F,A,_} <- Bad0],
Loc = ["\n","Please update ",DeprFile,".\n"],
io:put_chars(standard_error, [Msg,Bad,Loc]),
halt(1)
end.
read_deprecations(File) ->
{ok,Bin} = file:read_file(File),
Lines = binary:split(Bin, <<"\n">>, [global,trim_all]),
maps:from_list(parse_deprecations(Lines)).
deprecation_file(Root) ->
filename:join(Root, "system/doc/general_info/DEPRECATIONS").
in_deprecations({Tag,M,F,A,_}, Depr) when Tag =:= deprecated; Tag =:= removed ->
is_map_key({M,F,A}, Depr).
parse_deprecations([<<"#",_/binary>>|Lines]) ->
parse_deprecations(Lines);
parse_deprecations([Line|Lines]) ->
[parse_line(Line)|parse_deprecations(Lines)];
parse_deprecations([]) ->
[].
parse_line(Line) ->
[MFA0|Parts0] = binary:split(Line, <<" ">>, [global,trim_all]),
MFA = parse_mfa(MFA0),
Parts1 = [binary:split(Part, <<"=">>) || Part <- Parts0],
Parts = lists:sort([parse_part(Part) || Part <- Parts1]),
{MFA,Parts}.
parse_part([<<"mfa">>,MFA]) ->
{mfa,parse_mfa(MFA)};
parse_part([<<"since">>,Since]) ->
{since,parse_release(Since)};
parse_part([<<"remove">>,Remove]) ->
{remove,parse_release(Remove)}.
parse_release(Rel) ->
binary_to_integer(Rel).
parse_mfa(MFA) ->
{match,[M0,F0,A0]} = re:run(MFA, <<"^(\\w+):(\\w+)/([\\d_]+)$">>,
[{capture,all_but_first,binary}]),
A = case A0 of
<<"_">> -> '_';
_ -> binary_to_integer(A0)
end,
{bin_to_atom(M0),bin_to_atom(F0),A}.
bin_to_atom(Bin) ->
list_to_atom(binary_to_list(Bin)).
rel2fam(S0) ->
S1 = sofs:relation(S0),
S = sofs:rel2fam(S1),
sofs:to_external(S).