From 5c40fe23c2bf4b19537a7110bc3c041ca9985b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 14 May 2025 12:51:21 +0200 Subject: [PATCH 01/15] Remove line_history from user_drv crash state --- lib/kernel/src/group.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index b613e3709d4f..a6c9163a4ab2 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -41,7 +41,7 @@ xterm/3, dumb/3, handle_info/3]). %% gen statem callbacks --export([init/1, callback_mode/0]). +-export([init/1, callback_mode/0, format_status/1]). %% Logger report format fun -export([format_io_request_log/1, log_io_request/3]). @@ -155,6 +155,9 @@ init([Drv, Shell, Options]) -> {ok, init, State, {next_event, internal, [Shell, Options]}}. +format_status(#{state := State}) -> + State#state{ line_history = undefined }. + init(internal, [Shell, Options], State = #state{ dumb = Dumb }) -> StartedShell = start_shell(Shell), From 19800e8f8edb6bb8e1b1a2e6ab75b7c6c71a9a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Mon, 9 Jun 2025 20:58:05 +0200 Subject: [PATCH 02/15] prim_tty: Fix CSI parsing --- lib/kernel/src/prim_tty.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index a9457801710b..f29243b2d6bb 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -1305,7 +1305,7 @@ insert_buf(State, Bin, LineAcc, Acc) -> NewState}; [$\t | Rest] -> insert_buf(State, Rest, [State#state.tab | LineAcc], Acc); - [$\e | Rest] -> + [CSI | Rest] when CSI =:= $\e; CSI =:= 155 -> case ansi_sgr(Bin) of none -> case re:run(Bin, State#state.ansi_regexp) of @@ -1390,7 +1390,7 @@ insert_buf(State, Bin, LineAcc, Acc) -> %% This function replicates this regex pattern <<"^[\e",194,155,"]\\[[0-9;:]*m">> %% calling re:run/2 nif on compiled regex_pattern was significantly %% slower than this implementation. -ansi_sgr(<> = Bin) when N =:= $\e; N =:= 194; N =:= 155 -> +ansi_sgr(<> = Bin) when N =:= $\e; N =:= 155 -> case ansi_sgr(Rest, 2) of {ok, Size} -> <> = Bin, From 9baa61ef3318e762cd8542afcab99ef8455320ba Mon Sep 17 00:00:00 2001 From: Lukas Larsson Date: Fri, 24 Nov 2023 13:09:11 +0100 Subject: [PATCH 03/15] stdlib: Add io_ansi --- bootstrap/lib/kernel/ebin/prim_tty.beam | Bin 25276 -> 28328 bytes erts/emulator/nifs/common/prim_tty_nif.c | 174 +++- lib/kernel/src/group.erl | 2 + lib/kernel/src/prim_tty.erl | 102 +- lib/kernel/src/standard_error.erl | 3 +- lib/kernel/src/user_drv.erl | 64 +- lib/stdlib/src/Makefile | 1 + lib/stdlib/src/io_ansi.erl | 1184 ++++++++++++++++++++++ lib/stdlib/src/io_lib.erl | 39 +- lib/stdlib/src/io_lib_format.erl | 30 +- lib/stdlib/test/Makefile | 1 + lib/stdlib/test/io_ansi_SUITE.erl | 71 ++ 12 files changed, 1506 insertions(+), 165 deletions(-) create mode 100644 lib/stdlib/src/io_ansi.erl create mode 100644 lib/stdlib/test/io_ansi_SUITE.erl diff --git a/bootstrap/lib/kernel/ebin/prim_tty.beam b/bootstrap/lib/kernel/ebin/prim_tty.beam index 1d9fbd99a1d4df60553da125ca6ca5cad505249c..6e7e7a8152f62554d1ba6e2e53534fa8ca41cc53 100644 GIT binary patch delta 15647 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 K@8AvFO!j}sVB?Jd diff --git a/erts/emulator/nifs/common/prim_tty_nif.c b/erts/emulator/nifs/common/prim_tty_nif.c index af618a6243f1..6e0e2013ae17 100644 --- a/erts/emulator/nifs/common/prim_tty_nif.c +++ b/erts/emulator/nifs/common/prim_tty_nif.c @@ -44,14 +44,10 @@ #include #include #include -#if defined(HAVE_TERMCAP) && (defined(HAVE_TERMCAP_H) || (defined(HAVE_CURSES_H) && defined(HAVE_TERM_H))) +#if defined(HAVE_TERMCAP) && defined(HAVE_CURSES_H) && defined(HAVE_TERM_H) #include -#ifdef HAVE_TERMCAP_H -#include -#else /* !HAVE_TERMCAP_H */ #include #include -#endif #else /* We detected TERMCAP support, but could not find the correct headers to include */ #undef HAVE_TERMCAP @@ -142,11 +138,12 @@ static ERL_NIF_TERM wcwidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM arg static ERL_NIF_TERM wcswidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); static ERL_NIF_TERM sizeof_wchar_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); static ERL_NIF_TERM tty_window_size_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); -static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); -static ERL_NIF_TERM tty_tgetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); -static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); -static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); -static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_setupterm_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tigetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tigetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tinfo_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tigetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); +static ERL_NIF_TERM tty_tputs_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); static ErlNifFunc nif_funcs[] = { {"isatty", 1, isatty_nif}, @@ -163,13 +160,12 @@ static ErlNifFunc nif_funcs[] = { {"wcwidth", 1, wcwidth_nif}, {"wcswidth", 1, wcswidth_nif}, {"sizeof_wchar", 0, sizeof_wchar_nif}, - {"tgetent_nif", 1, tty_tgetent_nif}, - {"tgetnum_nif", 1, tty_tgetnum_nif}, - {"tgetflag_nif", 1, tty_tgetflag_nif}, - {"tgetstr_nif", 1, tty_tgetstr_nif}, - {"tgoto_nif", 1, tty_tgoto_nif}, - {"tgoto_nif", 2, tty_tgoto_nif}, - {"tgoto_nif", 3, tty_tgoto_nif} + {"setupterm_nif", 0, tty_setupterm_nif}, + {"tigetnum_nif", 1, tty_tigetnum_nif}, + {"tigetflag_nif", 1, tty_tigetflag_nif}, + {"tinfo_nif", 0, tty_tinfo_nif}, + {"tigetstr_nif", 1, tty_tigetstr_nif}, + {"tputs_nif", 2, tty_tputs_nif} }; /* NIF interface declarations */ @@ -771,13 +767,11 @@ static ERL_NIF_TERM setlocale_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM a #endif } -static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +static ERL_NIF_TERM tty_setupterm_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { #ifdef HAVE_TERMCAP - ErlNifBinary TERM; - if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) - return enif_make_badarg(env); - if (tgetent((char *)NULL /* ignored */, (char *)TERM.data) <= 0) { - return make_errno_error(env, "tgetent"); + int errret; + if (setupterm(NULL, -1, &errret) < 0) { + return make_errno_error(env, "setupterm"); } return atom_ok; #else @@ -785,23 +779,69 @@ static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM #endif } -static ERL_NIF_TERM tty_tgetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +static ERL_NIF_TERM tty_tinfo_make_map(ErlNifEnv* env, + const char * const* names, + const char * const* codes, + const char * const* fnames) { + ERL_NIF_TERM res = enif_make_list(env, 0); + ERL_NIF_TERM ks[3] = { + enif_make_atom(env, "name"), + enif_make_atom(env, "code"), + enif_make_atom(env, "full_name") + }; + for (int i = 0; names[i] && codes[i] && fnames[i]; i++) { + ERL_NIF_TERM map; + ERL_NIF_TERM vs[3] = { + enif_make_string(env, names[i], ERL_NIF_LATIN1), + enif_make_string(env, codes[i], ERL_NIF_LATIN1), + enif_make_string(env, fnames[i], ERL_NIF_LATIN1) + }; + enif_make_map_from_arrays(env, ks, vs, 3, &map); + res = enif_make_list_cell(env, map, res); + } + return res; +} + +static ERL_NIF_TERM tty_tinfo_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { #ifdef HAVE_TERMCAP - ErlNifBinary TERM; - if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + ERL_NIF_TERM ks[3] = { + enif_make_atom(env, "bool"), + enif_make_atom(env, "num"), + enif_make_atom(env, "str") + }; + + ERL_NIF_TERM vs[3] = { + tty_tinfo_make_map(env, boolnames, boolcodes, boolfnames), + tty_tinfo_make_map(env, numnames, numcodes, numfnames), + tty_tinfo_make_map(env, strnames, strcodes, strfnames) + }; + ERL_NIF_TERM res; + + enif_make_map_from_arrays(env, ks, vs, 3, &res); + + return res; +#else + return make_enotsup(env); +#endif +} + +static ERL_NIF_TERM tty_tigetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +#ifdef HAVE_TERMCAP + ErlNifBinary CAP; + if (!enif_inspect_iolist_as_binary(env, argv[0], &CAP)) return enif_make_badarg(env); - return enif_make_int(env, tgetnum((char*)TERM.data)); + return enif_make_int(env, tgetnum((char*)CAP.data)); #else return make_enotsup(env); #endif } -static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +static ERL_NIF_TERM tty_tigetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { #ifdef HAVE_TERMCAP - ErlNifBinary TERM; - if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + ErlNifBinary CAP; + if (!enif_inspect_iolist_as_binary(env, argv[0], &CAP)) return enif_make_badarg(env); - if (tgetflag((char*)TERM.data)) + if (tgetflag((char*)CAP.data)) return atom_true; return atom_false; #else @@ -809,19 +849,15 @@ static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TER #endif } -static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +static ERL_NIF_TERM tty_tigetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { #ifdef HAVE_TERMCAP - ErlNifBinary TERM, ret; - /* tgetstr seems to use a lot of stack buffer space, - so buff needs to be relatively "small" */ - char *str = NULL; - char buff_area[BUFSIZ] = {0}; - char *buff = (char*)buff_area; - - if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM)) + ErlNifBinary CAP, ret; + char *str; + if (!enif_inspect_iolist_as_binary(env, argv[0], &CAP)) return enif_make_badarg(env); - str = tgetstr((char*)TERM.data, &buff); - if (!str) return atom_false; + str = tigetstr((char*)CAP.data); + if (!str) return atom_false; /* Not supported by terminal */ + if (str == (char*)-1) return enif_make_badarg(env); /* Invalid capability */ enif_alloc_binary(strlen(str), &ret); memcpy(ret.data, str, strlen(str)); return enif_make_tuple2( @@ -832,6 +868,7 @@ static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM } #ifdef HAVE_TERMCAP +static ErlNifMutex *tputs_mutex; static int tputs_buffer_index; static unsigned char tputs_buffer[1024]; @@ -845,21 +882,50 @@ static int tty_puts_putc(int c) { } #endif -static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { +static ERL_NIF_TERM tty_tputs_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { #ifdef HAVE_TERMCAP - ErlNifBinary TERM; + ErlNifBinary cap; ERL_NIF_TERM ret; char *ent; - int value1 = 0, value2 = 0; unsigned char *buff; + long params[9] = { 0 }; + int slot = 0; - if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM) || - (argc > 1 && !enif_get_int(env, argv[1], &value1)) || - (argc > 2 && !enif_get_int(env, argv[2], &value2)) - ) + if (!enif_inspect_iolist_as_binary(env, argv[0], &cap)) return enif_make_badarg(env); - ent = tgoto((char*)TERM.data, value1, value2); - if (!ent) return make_errno_error(env, "tgoto"); + + { + ERL_NIF_TERM head, tail = argv[1]; + while(enif_get_list_cell(env, tail, &head, &tail)) { + if (!enif_get_long(env, head, params + slot)) { + return enif_make_badarg(env); + } + slot++; + } + if (!enif_is_empty_list(env, tail)) { + enif_make_badarg(env); + } + } + + /* Neither tparm nor tputs are thread safe.. */ + enif_mutex_lock(tputs_mutex); + + /* If the capability has arguments, we call tparm */ + if (slot) { + /* The https://linux.die.net/man/3/tparm specifies that although tparm uses + a vararg prototype on Linux, to be portable we should always call it with + 9 arguments as some implementation have a static prototype. + */ + ent = tparm((char*)cap.data, params[0], params[1], params[2], params[3], + params[4], params[5], params[6], params[7], params[8]); + + if (!ent) { + enif_mutex_unlock(tputs_mutex); + return make_errno_error(env, "tparm"); + } + } else { + ent = (char*)cap.data; + } tputs_buffer_index = 0; (void)tputs(ent, 1, tty_puts_putc); /* tputs only fails if ent is null, @@ -867,6 +933,9 @@ static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM a buff = enif_make_new_binary(env, tputs_buffer_index, &ret); memcpy(buff, tputs_buffer, tputs_buffer_index); + + enif_mutex_unlock(tputs_mutex); + return enif_make_tuple2(env, atom_ok, ret); #else return make_enotsup(env); @@ -1146,6 +1215,9 @@ static void load_resources(ErlNifEnv* env, ErlNifResourceFlags rt_flags) { static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) { *priv_data = NULL; +#ifdef HAVE_TERMCAP + tputs_mutex = enif_mutex_create("tputs_muex"); +#endif load_resources(env, ERL_NIF_RT_CREATE); return 0; } diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index a6c9163a4ab2..19b702bdd6e8 100644 --- a/lib/kernel/src/group.erl +++ b/lib/kernel/src/group.erl @@ -611,6 +611,8 @@ putc_request(Req, From, ReplyAs, State) -> %% %% These put requests have to be synchronous to the driver as otherwise %% there is no guarantee that the data has actually been printed. +putc_request({put_ansi, Opts, Ansi}, Drv, From) -> + send_drv(Drv, {put_ansi_sync, Opts, Ansi, From}); putc_request({put_chars,unicode,Chars}, Drv, From) -> case catch unicode:characters_to_binary(Chars,utf8) of Binary when is_binary(Binary) -> diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index f29243b2d6bb..430740d6ab32 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -115,17 +115,21 @@ -export([reader_stop/1, disable_reader/1, enable_reader/1, read/1, read/2, is_reader/2, is_writer/2, output_mode/1]). +%% Export to io_ansi +-export([tigetstr/1, tputs/2, tigetflag/1, tigetnum/1, tinfo/0]). + -nifs([isatty/1, tty_create/1, tty_init/2, setlocale/1, tty_select/2, tty_window_size/1, tty_encoding/1, tty_is_open/2, write_nif/2, read_nif/3, isprint/1, wcwidth/1, wcswidth/1, - sizeof_wchar/0, tgetent_nif/1, tgetnum_nif/1, tgetflag_nif/1, tgetstr_nif/1, - tgoto_nif/1, tgoto_nif/2, tgoto_nif/3]). + sizeof_wchar/0, setupterm_nif/0, tigetnum_nif/1, tigetflag_nif/1, + tigetstr_nif/1, tinfo_nif/0, tputs_nif/2 + ]). -export([reader_loop/2, writer_loop/2]). %% Exported in order to remove "unused function" warning --export([sizeof_wchar/0, wcswidth/1, tgoto/1, tgoto/2, tgoto/3, tty_is_open/2]). +-export([sizeof_wchar/0, wcswidth/1, tty_is_open/2]). %% proc_lib exports -export([reader/1, writer/1]). @@ -151,6 +155,7 @@ buffer_expand_row = 1, buffer_expand_limit = 0 :: non_neg_integer(), putc_buffer = <<>>, %% Buffer for putc containing the last row of characters + have_termcap = false :: boolean(), cols = 80, rows = 24, xn = false, @@ -267,7 +272,12 @@ init(UserOptions) when is_map(UserOptions) -> true -> UnicodeSupported end, {ok, ANSI_RE_MP} = re:compile(?ANSI_REGEXP, [unicode]), - init_term(#state{ tty = TTY, unicode = UnicodeMode, options = Options, ansi_regexp = ANSI_RE_MP }). + + HaveTermCap = setupterm() =:= ok, + + init_term(#state{ tty = TTY, have_termcap = HaveTermCap, + unicode = UnicodeMode, options = Options, + ansi_regexp = ANSI_RE_MP }). init_term(State = #state{ tty = TTY, options = Options }) -> TTYState = case maps:get(input, Options) of @@ -358,66 +368,60 @@ init(State, ssh) -> State#state{ xn = true }; init(State, {unix,_}) -> - case os:getenv("TERM") of - false -> - error(enotsup); - Term -> - case tgetent(Term) of - ok -> ok; - {error,_} -> error(enotsup) - end - end, + State#state.have_termcap orelse error(enotsup), + %% See https://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html#SEC23 %% for a list of all possible termcap capabilities - Clear = case tgetstr("clear") of + Clear = case tigetstr("clear") of {ok, C} -> C; false -> (#state{})#state.clear end, - Cols = case tgetnum("co") of + + Cols = case tigetnum("co") of {ok, Cs} -> Cs; _ -> (#state{})#state.cols end, - Up = case tgetstr("up") of + Up = case tigetstr("cuu1") of {ok, U} -> U; false -> error(enotsup) end, - Down = case tgetstr("do") of + Down = case tigetstr("cud1") of false -> (#state{})#state.down; {ok, D} -> D end, - Left = case {tgetflag("bs"),tgetstr("bc")} of + Left = case {tigetflag("OTbs"),tigetstr("OTbc")} of {true,_} -> (#state{})#state.left; {_,false} -> (#state{})#state.left; {_,{ok, L}} -> L end, - Right = case tgetstr("nd") of + Right = case tigetstr("cuf1") of {ok, R} -> R; false -> error(enotsup) end, Insert = - case tgetstr("IC") of + case tigetstr("ich") of {ok, IC} -> IC; false -> (#state{})#state.insert end, - Tab = case tgetstr("ta") of + Tab = case tigetstr("ht") of {ok, TA} -> TA; false -> (#state{})#state.tab end, - Delete = case tgetstr("DC") of + Delete = case tigetstr("dch") of {ok, DC} -> DC; false -> (#state{})#state.delete end, - Position = case tgetstr("u7") of + Position = case tigetstr("u7") of {ok, <<"\e[6n">> = U7} -> %% User 7 should contain the codes for getting %% cursor position. %% User 6 should contain how to parse the reply - {ok, <<"\e[%i%d;%dR">>} = tgetstr("u6"), + {ok, <<"\e[%i%d;%dR">>} = tigetstr("u6"), <<"\e[6n">> = U7; false -> (#state{})#state.position end, @@ -425,7 +429,7 @@ init(State, {unix,_}) -> %% According to the manual this should only be issued when the cursor %% is at position 0, but until we encounter such a console we keep things %% simple and issue this with the cursor anywhere - DeleteAfter = case tgetstr("cd") of + DeleteAfter = case tigetstr("ed") of {ok, DA} -> DA; false -> @@ -435,7 +439,7 @@ init(State, {unix,_}) -> State#state{ cols = Cols, clear = Clear, - xn = tgetflag("xn"), + xn = tigetflag("xn"), up = Up, down = Down, left = Left, @@ -1498,36 +1502,28 @@ sizeof_wchar() -> erlang:nif_error(undef). wcswidth(_Char) -> erlang:nif_error(undef). -tgetent(Char) -> - tgetent_nif([Char,0]). -tgetnum(Char) -> - tgetnum_nif([Char,0]). -tgetflag(Char) -> - tgetflag_nif([Char,0]). -tgetstr(Char) -> - case tgetstr_nif([Char,0]) of - {ok, Str} -> - {ok, re:replace(Str, "\\$<[^>]*>","")}; - Error -> - Error - end. -tgoto(Char) -> - tgoto_nif([Char,0]). -tgoto(Char, Arg) -> - tgoto_nif([Char,0], Arg). -tgoto(Char, Arg1, Arg2) -> - tgoto_nif([Char,0], Arg1, Arg2). -tgetent_nif(_Char) -> - erlang:nif_error(undef). -tgetnum_nif(_Char) -> +setupterm() -> + setupterm_nif(). +tigetnum(Char) -> + tigetnum_nif([Char,0]). +tigetflag(Char) -> + tigetflag_nif([Char,0]). +tigetstr(Char) -> + tigetstr_nif([Char,0]). +tinfo() -> + tinfo_nif(). +tputs(Char, Args) -> + tputs_nif([Char,0], Args). + +setupterm_nif() -> erlang:nif_error(undef). -tgetflag_nif(_Char) -> +tigetnum_nif(_Char) -> erlang:nif_error(undef). -tgetstr_nif(_Char) -> +tigetflag_nif(_Char) -> erlang:nif_error(undef). -tgoto_nif(_Ent) -> +tinfo_nif() -> erlang:nif_error(undef). -tgoto_nif(_Ent, _Arg) -> +tigetstr_nif(_Char) -> erlang:nif_error(undef). -tgoto_nif(_Ent, _Arg1, _Arg2) -> +tputs_nif(_Ent, _Args) -> erlang:nif_error(undef). diff --git a/lib/kernel/src/standard_error.erl b/lib/kernel/src/standard_error.erl index c887ac70c638..cb09c2b10f6f 100644 --- a/lib/kernel/src/standard_error.erl +++ b/lib/kernel/src/standard_error.erl @@ -261,7 +261,8 @@ getopts() -> Uni = {encoding,get(encoding)}, Onlcr = {onlcr, get(onlcr)}, Log = {log, get(log)}, - {reply,[Uni, Onlcr, Log]}. + Terminal = {terminal, get(onlcr)}, + {reply,[Uni, Onlcr, Log, Terminal]}. wrap_characters_to_binary(Chars,From,To) -> TrNl = get(onlcr), diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl index 7e998ced1b2f..b265859cab1b 100644 --- a/lib/kernel/src/user_drv.erl +++ b/lib/kernel/src/user_drv.erl @@ -57,6 +57,8 @@ %% Output raw binary, should only be called if output mode is set to raw %% and encoding set to latin1. {put_chars_sync, latin1, binary(), {From :: pid(), Reply :: term()}} | + %% Print a terminal special character + {put_ansi, list(), io_ansi:code()} | %% Put text in expansion area {put_expand, unicode, binary(), integer()} | {move_expand, -32768..32767} | @@ -358,8 +360,7 @@ init_shell(State, Slogan) -> {next_state, server, State#state{ current_group = gr_cur_pid(State#state.groups) }, {next_event, info, {gr_cur_pid(State#state.groups), - {put_chars, unicode, - unicode:characters_to_binary(io_lib:format("~ts", [Slogan]))}}}}. + {put_chars, unicode, io_lib:bformat("~ts", [Slogan])}}}}. %% start_user() %% Start a group leader process and register it as 'user', unless, @@ -544,7 +545,8 @@ server(info, {'DOWN', MonitorRef, _, _, Reason}, Origin ! {reply, Reply, {error, Reason}}, ?LOG_INFO("Failed to write to standard out (~p)", [Reason]), stop; -server(info,{Requester, {put_chars_sync, _, _, Reply}}, _State) -> +server(info,{Requester, {Request, _, _, Reply}}, _State) + when Request =:= put_chars_sync; Request =:= put_ansi_sync -> %% This is a sync request from an unknown or inactive group. %% We need to ack the Req otherwise originating process will hang forever. %% We discard the output to non visible shells @@ -590,15 +592,15 @@ server(info,{'EXIT', Group, Reason}, State) -> Group when Reason =/= die, Reason =/= terminated -> % current shell exited Reqs = [if Reason =/= normal -> - {put_chars,unicode,<<"*** ERROR: ">>}; + {put_chars,unicode,~"*** ERROR: "}; true -> % exit not caused by error - {put_chars,unicode,<<"*** ">>} + {put_chars,unicode,~"*** "} end, - {put_chars,unicode,<<"Shell process terminated! ">>}], + {put_chars,unicode,~"Shell process terminated! "}], Gr1 = gr_del_pid(State#state.groups, Group), case GroupInfo of {Ix,{shell,start,Params}} -> % 3-tuple == local shell - NewTTyState = io_requests(Reqs ++ [{put_chars,unicode,<<"***\n">>}], + NewTTyState = io_requests(Reqs ++ [{put_chars,unicode,~"***\n"}], State#state.tty), %% restart group leader and shell, same index NewGroup = group:start(self(), {shell,start,Params}), @@ -609,7 +611,7 @@ server(info,{'EXIT', Group, Reason}, State) -> groups = Gr2 }}; _ -> % remote shell NewTTYState = io_requests( - Reqs ++ [{put_chars,unicode,<<"(^G to start new job) ***\n">>}], + Reqs ++ [{put_chars,unicode,~"(^G to start new job) ***\n"}], State#state.tty), {keep_state, State#state{ tty = NewTTYState, groups = Gr1 }} end; @@ -646,13 +648,13 @@ switch_loop(internal, init, State) -> end end, NewGroup = group:start(self(), {shell,start,[]}), - NewTTYState = io_requests([{insert_chars,unicode,<<"\n">>}], State#state.tty), + NewTTYState = io_requests([{insert_chars,unicode,~"\n"}], State#state.tty), {next_state, server, State#state{ tty = NewTTYState, groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}}; jcl -> NewTTYState = - io_requests([{insert_chars,unicode,<<"\nUser switch command (enter 'h' for help)\n">>}], + io_requests([{insert_chars,unicode,~"\nUser switch command (enter 'h' for help)\n"}], State#state.tty), %% init edlin used by switch command and have it copy the %% text buffer from current group process @@ -673,22 +675,22 @@ switch_loop(internal, {line, Line}, State) -> Curr ! {self(), activate}, {next_state, server, State#state{ current_group = Curr, groups = Groups, - tty = io_requests([{insert_chars, unicode, <<"\n">>},new_prompt], State#state.tty)}}; + tty = io_requests([{insert_chars, unicode, ~"\n"},new_prompt], State#state.tty)}}; {retry, Requests} -> - {keep_state, State#state{ tty = io_requests([{insert_chars, unicode, <<"\n">>},new_prompt|Requests], State#state.tty) }, + {keep_state, State#state{ tty = io_requests([{insert_chars, unicode, ~"\n"},new_prompt|Requests], State#state.tty) }, {next_event, internal, line}}; {retry, Requests, Groups} -> Curr = gr_cur_pid(Groups), put(current_group, Curr), {keep_state, State#state{ - tty = io_requests([{insert_chars, unicode, <<"\n">>},new_prompt|Requests], State#state.tty), + tty = io_requests([{insert_chars, unicode, ~"\n"},new_prompt|Requests], State#state.tty), current_group = Curr, groups = Groups }, {next_event, internal, line}} end; {error, _, _} -> NewTTYState = - io_requests([{insert_chars,unicode,<<"Illegal input\n">>}], State#state.tty), + io_requests([{insert_chars,unicode,~"Illegal input\n"}], State#state.tty), {keep_state, State#state{ tty = NewTTYState }, {next_event, internal, line}} end; @@ -756,8 +758,7 @@ switch_cmd({i, I}, Gr, _Dumb) -> exit(Pid, interrupt), {retry, [{put_chars, unicode, - unicode:characters_to_binary( - io_lib:format("Interrupted job ~p, enter 'c' to resume.~n",[I]))}]}; + io_lib:bformat("Interrupted job ~p, enter 'c' to resume.~n",[I])}]}; undefined -> unknown_group() end; @@ -794,7 +795,7 @@ switch_cmd(r, Gr0, _Dumb) -> Gr = gr_add_cur(Gr0, Pid, {Node,shell,start,[]}), {retry, [], Gr}; false -> - {retry, [{put_chars,unicode,<<"Node is not alive\n">>}]} + {retry, [{put_chars,unicode,~"Node is not alive\n"}]} end; switch_cmd({r, Node}, Gr, Dumb) when is_atom(Node)-> switch_cmd({r, Node, shell}, Gr, Dumb); @@ -807,16 +808,16 @@ switch_cmd({r,Node,Shell}, Gr0, Dumb) when is_atom(Node), is_atom(Shell) -> Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}), {retry, [], Gr}; false -> - {retry, [{put_chars,unicode,<<"Could not connect to node\n">>}]} + {retry, [{put_chars,unicode,~"Could not connect to node\n"}]} end; false -> - {retry, [{put_chars,unicode,"Node is not alive\n"}]} + {retry, [{put_chars,unicode,~"Node is not alive\n"}]} end; switch_cmd(q, _Gr, _Dumb) -> case erlang:system_info(break_ignored) of true -> % noop - {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}; + {retry, [{put_chars,unicode,~"Unknown command\n"}]}; false -> halt() end; @@ -825,26 +826,26 @@ switch_cmd(h, _Gr, _Dumb) -> switch_cmd([], _Gr, _Dumb) -> {retry,[]}; switch_cmd(_Ts, _Gr, _Dumb) -> - {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}. + {retry, [{put_chars,unicode,~"Unknown command\n"}]}. unknown_group() -> - {retry,[{put_chars,unicode,<<"Unknown job\n">>}]}. + {retry,[{put_chars,unicode,~"Unknown job\n"}]}. list_commands() -> QuitReq = case erlang:system_info(break_ignored) of true -> []; false -> - [{put_chars, unicode,<<" q - quit erlang\n">>}] + [{put_chars, unicode,~" q - quit erlang\n"}] end, - [{put_chars, unicode,<<" c [nn] - connect to job\n">>}, - {put_chars, unicode,<<" i [nn] - interrupt job\n">>}, - {put_chars, unicode,<<" k [nn] - kill job\n">>}, - {put_chars, unicode,<<" j - list all jobs\n">>}, - {put_chars, unicode,<<" s [shell] - start local shell\n">>}, - {put_chars, unicode,<<" r [node [shell]] - start remote shell\n">>}] ++ + [{put_chars, unicode,~" c [nn] - connect to job\n"}, + {put_chars, unicode,~" i [nn] - interrupt job\n"}, + {put_chars, unicode,~" k [nn] - kill job\n"}, + {put_chars, unicode,~" j - list all jobs\n"}, + {put_chars, unicode,~" s [shell] - start local shell\n"}, + {put_chars, unicode,~" r [node [shell]] - start remote shell\n"}] ++ QuitReq ++ - [{put_chars, unicode,<<" ? | h - this message\n">>}]. + [{put_chars, unicode,~" ? | h - this message\n"}]. group_opts(Node) -> VersionString = erpc:call(Node, erlang, system_info, [otp_release]), @@ -1080,6 +1081,5 @@ gr_list(#gr{ current = Current, groups = Groups}) -> (#group{ index = I, shell = S }) -> Marker = ["*" || Current =:= I], [{put_chars, unicode, - unicode:characters_to_binary( - io_lib:format("~4w~.1ts ~w\n", [I,Marker,S]))}] + io_lib:bformat("~4w~.1ts ~w\n", [I,Marker,S])}] end, Groups). diff --git a/lib/stdlib/src/Makefile b/lib/stdlib/src/Makefile index d2517bb7c6fc..a9aa44dc265c 100644 --- a/lib/stdlib/src/Makefile +++ b/lib/stdlib/src/Makefile @@ -96,6 +96,7 @@ MODULES= \ gen_server \ gen_statem \ io \ + io_ansi \ io_lib \ io_lib_format \ io_lib_fread \ diff --git a/lib/stdlib/src/io_ansi.erl b/lib/stdlib/src/io_ansi.erl new file mode 100644 index 000000000000..8dbae5caa9b6 --- /dev/null +++ b/lib/stdlib/src/io_ansi.erl @@ -0,0 +1,1184 @@ +%% +%% %CopyrightBegin% +%% +%% SPDX-License-Identifier: Apache-2.0 +%% +%% Copyright Ericsson AB 2025. 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% +%% + +%% +%% https://hexdocs.pm/elixir/IO.ANSI.html +%% https://en.wikipedia.org/wiki/ANSI_escape_code +%% https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +%% https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences +%% + +-module(io_ansi). +-moduledoc """ +Controlling the terminal using virtual terminal sequences (aka [ANSI escape codes]). + +This module provides an interface to emit and parse virtual terminal sequences (VTS), +also known as [ANSI escape codes]. VTS can be used to: + +- change the style of text or background in the terminal by adding color or emphasis. +- delete printed characters or lines. +- move, hide or show the cursor + +and more things. As different terminals are interpret VTSs slightly +differently, `m:io_ansi` uses the local [terminfo] database together with +predefined sequences to emit the correct sequence for the terminal that is +currently used. To fetch values directly from the [terminfo] database you can use +`tput/2`, `tigetnum/1` and `tigetflag/1`. + +`m:io_ansi` provides two interfaces to emit sequences. You can either call the +function representing the sequence you want to emit, for example `io_ansi:blue()` +and it will return the sequence representing blue. + +```erlang +1> io_ansi:blue(). +<<"\e[34m">> +``` + +This will use the [terminfo] database locally where the call is made, so it may +not be correct if used across nodes. + +You can also use the [`io_ansi:format/1,2,3`](`io_ansi:format/3`) functions +which works just as `io_lib:bformat/3`, except that it also accepts atoms and +tuples that represent VTSs. For example: + +```erlang +1> io_ansi:format([blue,"~p"], [red]). +<<"\e[34mred\e(B\e[m">> +``` + +`io_ansi:format/3` will automatically reset the terminal to its original state +and strip any VTSs that are not supported by the terminal. It can also be disabled +through an option. For example: + +```erlang +1> io_ansi:format([blue,"~p"], [red], [{enabled, false}]). +<<"red">> +``` + +Finally there is [`io_ansi:fwrite/1,2,3,4`](`io_ansi:fwrite/4`) which does not +return the string to be printed, but instead sends it to the `t:io:device/0` +that should handle it. `io_ansi:fwrite/4` works across nodes and will use the +[terminfo] database where the data is outputted to decide what to emit. + +[terminfo]: https://man7.org/linux/man-pages/man5/terminfo.5.html +[ANSI escape codes]: https://en.wikipedia.org/wiki/ANSI_escape_code +""". + + +-export([tput/1, tput/2, tigetnum/1, tigetflag/1, tinfo/0]). +-export([format/1, format/2, format/3, fwrite/1, fwrite/2, fwrite/3, fwrite/4, + enabled/0, enabled/1, scan/1]). + +-export([black/0, blue/0, cyan/0, green/0, magenta/0, red/0, white/0, yellow/0, + color/1, color/3, default_color/0]). +-export([black_background/0, red_background/0, green_background/0, yellow_background/0, + blue_background/0, magenta_background/0, cyan_background/0, white_background/0, + background/1, background/3, default_background/0]). +-export([light_black/0, light_red/0, light_green/0, light_yellow/0, light_blue/0, + light_magenta/0, light_cyan/0, light_white/0]). +-export([light_black_background/0, light_red_background/0, light_green_background/0, + light_yellow_background/0, light_blue_background/0, light_magenta_background/0, + light_cyan_background/0, light_white_background/0]). +-export([modify_color/4]). +-export([bold/0, bold_off/0, underline/0, underline_off/0, negative/0, negative_off/0]). +-export([hyperlink_start/1, hyperlink_start/2, hyperlink_reset/0]). +-export([clear/0, erase_display/0, insert_character/1, delete_character/0, + delete_character/1, erase_character/1, insert_line/0, insert_line/1, + delete_line/0, delete_line/1, erase_line/0]). +-export([alternate_character_set_mode/0, alternate_character_set_mode_off/0]). +-export([cursor/2, cursor_up/0, cursor_up/1, cursor_down/0, cursor_down/1, + cursor_forward/0, cursor_forward/1, cursor_backward/0, cursor_backward/1, + cursor_home/0, reverse_index/0, cursor_save/0, cursor_restore/0, + cursor_show/0, cursor_hide/0, + cursor_next_line/0, cursor_previous_line/0, cursor_horizontal_absolute/1, + cursor_vertical_absolute/1, cursor_horizontal_vertical/2, cursor_report_position/0]). +-export([alternate_screen/0, alternate_screen_off/0, + scroll_forward/0, scroll_forward/1, scroll_backward/0, scroll_backward/1, + scroll_change_region/2]). +-export([tab/0, tab_backward/0, tab_set/0, tab_clear/0, tab_clear_all/0]). +-export([keypad_transmit_mode/0, keypad_transmit_mode_off/0]). +-export([reset/0, device_report_attributes/0]). + +-doc "The format string that can be passed to `format/3` and `fwrite/4`". +-type format() :: [string() | code()]. + +-doc "Virtual terminal codes that control the foreground (aka text) color.". +-type foreground_color() :: black | blue | cyan | green | magenta | red | white | yellow | + light_black | light_blue | light_cyan | light_green | + light_magenta | light_red | light_white | light_yellow | + {color, 0..255} | {color, R :: 0..255, G :: 0..255, B :: 0..255} | + default_color. +-doc "Virtual terminal codes that control the background color.". +-type background_color() :: black_background | blue_background | cyan_background | + green_background | magenta_background | red_background | + white_background | yellow_background | default_background | + light_black_background | light_blue_background | + light_cyan_background | light_green_background | + light_magenta_background | light_red_background | + light_white_background | light_yellow_background | + {color_background, 0..255} | + {color_background, R :: 0..255, G :: 0..255, B :: 0..255}. +-doc "Virtual terminal codes that control color.". +-type color() :: foreground_color() | background_color() | + {modify_color, Index :: 0..255, R :: 0..255, G :: 0..255, B :: 0..255}. + +-doc "Virtual terminal codes that control text style.". +-type style() :: bold | bold_off | underline | underline_off | negative | negative_off. + +-type hyperlink_params() :: [{Key :: unicode:chardata(), Value :: unicode:chardata()}]. + +-doc "Virtual terminal codes that control whether emitted text shall be a hyper link or not.". +-type hyperlink() :: {hyperlink, URL :: uri_string:uri_string(), Text :: unicode:chardata()} | + {hyperlink, URL :: uri_string:uri_string(), hyperlink_params(), Text :: unicode:chardata()} | + {hyperlink_start, URL :: uri_string:uri_string()} | + {hyperlink_start, URL :: uri_string:uri_string(), hyperlink_params()} | + hyperlink_reset. + +-doc "Virtual terminal codes that control text formatting.". +-type text_formatting() :: color() | style() | hyperlink(). +-doc "Virtual terminal codes that can erase or owerwrite text.". +-type text_modification() :: clear | erase_display | + insert_character | delete_character | erase_character | + insert_line | delete_line | erase_line. +-doc "Virtual terminal codes that works on text.". +-type text() :: text_formatting() | text_modification() | + alternate_character_set_mode | alternate_character_set_mode_off. +-doc "Virtual terminal codes that controls the cursor.". +-type cursor() :: + {cursor, Line :: non_neg_integer(), Column :: non_neg_integer()} | + cursor_down | cursor_up | cursor_backward | cursor_forward | + {cursor_down | cursor_backward | cursor_forward | cursor_up, N :: non_neg_integer()} | + cursor_home | reverse_index | cursor_save | cursor_restore | + cursor_show | cursor_hide | + cursor_next_line | cursor_previous_line | cursor_horizontal_absolute | + cursor_vertical_absolute | cursor_horizontal_vertical | cursor_report_position. +-doc "Virtual terminal codes that controls the screen.". +-type window() :: alternate_screen | alternate_screen_off | + scroll_forward | scroll_backward | scroll_change_region. +-doc "Virtual terminal codes that works with tabs.". +-type tab() :: tab | tab_backward | tab_set | tab_clear | tab_clear_all. +-doc "Virtual terminal codes for cursor input.". +-type input() :: keypad_transmit_mode | keypad_transmit_mode_off | + kcursor_down | kcursor_up | kcursor_backward | kcursor_forward | + kcursor_home | kcursor_end. +-doc "Virtual terminal codes.". +-type code() :: text() | cursor() | window() | tab() | input() | reset | device_report_attributes. + +-type option() :: {reset, boolean()} | { enabled, boolean()} | io_lib:format_options(). +-type options() :: [option()]. + +-export_type([code/0]). + +-doc #{ equiv => tput(TermInfoCap, []) }. +-doc #{ group => ~"Functions: terminfo" }. +-spec tput(TermInfoCap :: string()) -> unicode:unicode_binary(). +tput(TermInfoCap) -> + tput(TermInfoCap, []). + +-doc """ +Returns the string representing the action taken by the given terminal capability. + +The names of the terminal capabilities can be found in the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html) +documentation, or by calling `tinfo/0`. `tput/2` will use the terminfo definition +associated with the `TERM` environment variable when the Erlang VM is started. +It is not possible to change after startup. + +If the given capability is not defined in the terminfo database an `enotsup` +error is generated, if the given capability is invalid a `badarg` error is +generated. + +This function does not work on Windows and will always generate a `badarg` +exception. + +Example: + +``` +%% Set the foreground color to 3 +1> io_ansi:tput("setaf",[3]). +<<"\e[33m">> +%% Move the cursor up 2 spaces +2> io_ansi:tput("cuu",[2]). +<<"\e[2A">> +%% Move the cursor down 1 space +3> io_ansi:tput("cud1"). +<<"\n">> +%% unsupported capability +4> io_ansi:tput("slm"). +** exception error: {enotsup,"slm"} + in function io_ansi:tput/2 +%% unknown capability +5> io_ansi:tput("foobar"). +** exception error: {einval,"foobar",[]} + in function io_ansi:tput/2 +``` +""". +-doc #{ group => ~"Functions: terminfo" }. +-spec tput(TermInfoCapName :: string(), Args :: [integer()]) -> + unicode:unicode_binary(). +tput(TermInfoCap, Args) -> + try prim_tty:tigetstr(TermInfoCap) of + {ok, TermInfoStr} -> + try prim_tty:tputs(TermInfoStr, Args) of + {ok, S} -> S + catch error:badarg -> + erlang:error({badarg, TermInfoCap, Args}) + end; + false -> + erlang:error({enotsup, TermInfoCap}) + catch error:badarg -> + erlang:error({einval, TermInfoCap, Args}) + end. + +-doc """ +Returns the number representing a terminfo capability. + +The names of the terminal capabilities can be found in the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html) +documentation, or by calling `tinfo/0`. `tigetnum/1` will use the terminfo +definition associated with the `TERM` environment variable when the Erlang VM is +started. It is not possible to change after startup. + +Returns `-1` if the capability is not available. + +Example: + +```erlang +1> io_ansi:tigetnum("co"). +80 +2> io_ansi:tigetnum("foobar"). +-1 +``` +""". +-doc #{ group => ~"Functions: terminfo" }. +-spec tigetnum(TermInfoCapName :: string()) -> -1 | non_neg_integer(). +tigetnum(TermInfoCap) -> + prim_tty:tigetnum(TermInfoCap). + +-doc """ +Returns the true if the terminfo capability is available, otherwise false. + +The names of the terminal capabilities can be found in the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html) +documentation, or by calling `tinfo/0`. `tigetflag/1` will use the terminfo +definition associated with the `TERM` environment variable when the Erlang VM is +started. It is not possible to change after startup. + +Example: + +```erlang +1> io_ansi:tigetflag("xn"). +true +2> io_ansi:tigetflag("foobar"). +false +``` +""". +-doc #{ group => ~"Functions: terminfo" }. +-spec tigetflag(TermInfoCapName :: string()) -> boolean(). +tigetflag(TermInfoCap) -> + prim_tty:tigetflag(TermInfoCap). + +-doc """ +Returns information about all available terminfo capabilities. See +the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html) +documentation for details on each. + +`tinfo/0` will use the terminfo definition associated with the `TERM` environment +variable when the Erlang VM is started. It is not possible to change after startup. + +When calling `tput/2`, `tigetnum/1` and `tigetflag/1` you should provide the `name` +of the capability you want. +""". +-doc #{ group => ~"Functions: terminfo" }. +-spec tinfo() -> #{ bool := [#{ code := string(), name := string(), full_name := string()}]}. +tinfo() -> + prim_tty:tinfo(). + +-define(FUNCTION(NAME), + NAME() -> Fun = lookup(NAME, 0), Fun()). +-define(FUNCTION(NAME, ARG1), + NAME(ARG1) -> Fun = lookup(NAME, 1), Fun(ARG1)). +-define(FUNCTION(NAME, ARG1, ARG2), + NAME(ARG1, ARG2) -> Fun = lookup(NAME, 2), Fun(ARG1, ARG2)). +-define(FUNCTION(NAME, ARG1, ARG2, ARG3), + NAME(ARG1, ARG2, ARG3) -> Fun = lookup(NAME, 3), Fun(ARG1, ARG2, ARG3)). +-define(FUNCTION(NAME, ARG1, ARG2, ARG3, ARG4), + NAME(ARG1, ARG2, ARG3, ARG4) -> Fun = lookup(NAME, 4), Fun(ARG1, ARG2, ARG3, ARG4)). + +-define(SPEC(NAME), + -spec NAME() -> unicode:chardata()). +-define(SPEC(NAME, ARG1), + -spec NAME(ARG1 :: integer()) -> unicode:chardata()). +-define(SPEC(NAME, ARG1, ARG2), + -spec NAME(ARG1 :: integer(), ARG2 :: integer()) -> unicode:chardata()). + +-doc """ +Change foreground (aka text) color to black. +""". +?SPEC(black). +?FUNCTION(black). +-doc """ +Change foreground (aka text) color to red. +""". +?SPEC(red). +?FUNCTION(red). +-doc """ +Change foreground (aka text) color to gren. +""". +?SPEC(green). +?FUNCTION(green). +-doc """ +Change foreground (aka text) color to yellow. +""". +?SPEC(yellow). +?FUNCTION(yellow). +-doc """ +Change foreground (aka text) color to blue. +""". +?SPEC(blue). +?FUNCTION(blue). +-doc """ +Change foreground (aka text) color to magenta. +""". +?SPEC(magenta). +?FUNCTION(magenta). +-doc """ +Change foreground (aka text) color to cyan. +""". +?SPEC(cyan). +?FUNCTION(cyan). +-doc """ +Change foreground (aka text) color to white. +""". +?SPEC(white). +?FUNCTION(white). +-doc """ +Change foreground (aka text) color to index color. `Index` 0-15 are equivilant to +the named colors in `t:foreground_color/0` in the order that they are listed. +""". +-spec color(Index :: 0..255 | 0..87) -> unicode:chardata(). +?FUNCTION(color, Index). +-doc """ +Change foreground (aka text) color to RGB color. +""". +-spec color(0..255, 0..255, 0..255) -> unicode:chardata(). +?FUNCTION(color, Red, Green, Blue). +-doc """ +Change foreground (aka text) color to the default color. +""". +?SPEC(default_color). +?FUNCTION(default_color). + +-doc """ +Change background color to black. +""". +?SPEC(black_background). +?FUNCTION(black_background). +-doc """ +Change background color to ref. +""". +?SPEC(red_background). +?FUNCTION(red_background). +-doc """ +Change background color to green. +""". +?SPEC(green_background). +?FUNCTION(green_background). +-doc """ +Change background color to yellow. +""". +?SPEC(yellow_background). +?FUNCTION(yellow_background). +-doc """ +Change background color to blue. +""". +?SPEC(blue_background). +?FUNCTION(blue_background). +-doc """ +Change background color to magenta. +""". +?SPEC(magenta_background). +?FUNCTION(magenta_background). +-doc """ +Change background color to cyan. +""". +?SPEC(cyan_background). +?FUNCTION(cyan_background). +-doc """ +Change background color to white. +""". +?SPEC(white_background). +?FUNCTION(white_background). +-doc """ +Change background color to index color. `Index` 0-15 are equivilant to +the named colors in `t:backround_color/0` in the order that they are listed. +""". +-spec background(Index :: 0..255 | 0..87) -> unicode:chardata(). +?FUNCTION(background, Index). +-doc """ +Change background color to RGB color. +""". +-spec background(0..255, 0..255, 0..255) -> unicode:chardata(). +?FUNCTION(background, Red, Green, Blue). +-doc """ +Change background color to the default color. +""". +?SPEC(default_background). +?FUNCTION(default_background). + +-doc """ +Change foreground (aka text) color to light black. +""". +?SPEC(light_black). +?FUNCTION(light_black). +-doc """ +Change foreground (aka text) color to light red. +""". +?SPEC(light_red). +?FUNCTION(light_red). +-doc """ +Change foreground (aka text) color to light green. +""". +?SPEC(light_green). +?FUNCTION(light_green). +-doc """ +Change foreground (aka text) color to light yellow. +""". +?SPEC(light_yellow). +?FUNCTION(light_yellow). +-doc """ +Change foreground (aka text) color to light magenta. +""". +?SPEC(light_magenta). +?FUNCTION(light_magenta). +-doc """ +Change foreground (aka text) color to light blue. +""". +?SPEC(light_blue). +?FUNCTION(light_blue). +-doc """ +Change foreground (aka text) color to light cyan. +""". +?SPEC(light_cyan). +?FUNCTION(light_cyan). +-doc """ +Change foreground (aka text) color to light white. +""". +?SPEC(light_white). +?FUNCTION(light_white). + +-doc """ +Change background color to light black. +""". +?SPEC(light_black_background). +?FUNCTION(light_black_background). +-doc """ +Change background color to light red. +""". +?SPEC(light_red_background). +?FUNCTION(light_red_background). +-doc """ +Change background color to light green. +""". +?SPEC(light_green_background). +?FUNCTION(light_green_background). +-doc """ +Change background color to light yellow. +""". +?SPEC(light_yellow_background). +?FUNCTION(light_yellow_background). +-doc """ +Change background color to light magenta. +""". +?SPEC(light_magenta_background). +?FUNCTION(light_magenta_background). +-doc """ +Change background color to light blue. +""". +?SPEC(light_blue_background). +?FUNCTION(light_blue_background). +-doc """ +Change background color to light cyan. +""". +?SPEC(light_cyan_background). +?FUNCTION(light_cyan_background). +-doc """ +Change background color to light white. +""". +?SPEC(light_white_background). +?FUNCTION(light_white_background). + +-doc """ +Modify the color referenced by `Index` to be RGB. + +Calling this function for `Index` 0-15 will change the color of the named colors +in `t:foreground_color/0` and `t:background_color/0`. +""". +-spec modify_color(Index :: 0..255, R :: 0..255, G :: 0..255, B :: 0..255) -> unicode:chardata(). +?FUNCTION(modify_color, Index, R, G, B). + +-doc """ +Turn on bold text style. +""". +?SPEC(bold). +?FUNCTION(bold). +-doc """ +Turn off bold text style. +""". +?SPEC(bold_off). +?FUNCTION(bold_off). +-doc """ +Turn on underline text style. +""". +?SPEC(underline). +?FUNCTION(underline). +-doc """ +Turn off underline text style. +""". +?SPEC(underline_off). +?FUNCTION(underline_off). +-doc """ +Turn on negative text style. +""". +?SPEC(negative). +?FUNCTION(negative). +-doc """ +Turn off negative text style. +""". +?SPEC(negative_off). +?FUNCTION(negative_off). + + +?SPEC(clear). +?FUNCTION(clear). +?SPEC(erase_display). +?FUNCTION(erase_display). +?SPEC(insert_character, Chars). +?FUNCTION(insert_character, Chars). +?SPEC(delete_character). +?FUNCTION(delete_character). +?SPEC(delete_character, Chars). +?FUNCTION(delete_character, Chars). +?SPEC(erase_character, Chars). +?FUNCTION(erase_character, Chars). +?SPEC(insert_line). +?FUNCTION(insert_line). +?SPEC(insert_line, Lines). +?FUNCTION(insert_line, Lines). +?SPEC(delete_line). +?FUNCTION(delete_line). +?SPEC(delete_line, Lines). +?FUNCTION(delete_line, Lines). +?SPEC(erase_line). +?FUNCTION(erase_line). + +?SPEC(alternate_character_set_mode). +?FUNCTION(alternate_character_set_mode). +?SPEC(alternate_character_set_mode_off). +?FUNCTION(alternate_character_set_mode_off). + +-doc """ +Move the cursor to the given position. Position 0,0 is at the top left of the +terminal. +""". +?SPEC(cursor, Line, Column). +?FUNCTION(cursor, Line, Column). + +?SPEC(cursor_up). +?FUNCTION(cursor_up). +?SPEC(cursor_up, Lines). +?FUNCTION(cursor_up, Lines). + +?SPEC(cursor_down). +?FUNCTION(cursor_down). +?SPEC(cursor_down, Lines). +?FUNCTION(cursor_down, Lines). + +?SPEC(cursor_forward). +?FUNCTION(cursor_forward). +?SPEC(cursor_forward, Lines). +?FUNCTION(cursor_forward, Lines). + +?SPEC(cursor_backward). +?FUNCTION(cursor_backward). +?SPEC(cursor_backward, Lines). +?FUNCTION(cursor_backward, Lines). + +?SPEC(cursor_home). +?FUNCTION(cursor_home). + +?SPEC(reverse_index). +?FUNCTION(reverse_index). +?SPEC(cursor_save). +?FUNCTION(cursor_save). +?SPEC(cursor_restore). +?FUNCTION(cursor_restore). + +?SPEC(cursor_show). +?FUNCTION(cursor_show). +?SPEC(cursor_hide). +?FUNCTION(cursor_hide). + +?SPEC(cursor_next_line). +?FUNCTION(cursor_next_line). +?SPEC(cursor_previous_line). +?FUNCTION(cursor_previous_line). +?SPEC(cursor_horizontal_absolute, X). +?FUNCTION(cursor_horizontal_absolute, X). +?SPEC(cursor_vertical_absolute, Y). +?FUNCTION(cursor_vertical_absolute, Y). +?SPEC(cursor_horizontal_vertical, X, Y). +?FUNCTION(cursor_horizontal_vertical, X, Y). +?SPEC(cursor_report_position). +?FUNCTION(cursor_report_position). + +?SPEC(alternate_screen). +?FUNCTION(alternate_screen). +?SPEC(alternate_screen_off). +?FUNCTION(alternate_screen_off). + +?SPEC(scroll_forward). +?FUNCTION(scroll_forward). +?SPEC(scroll_forward, Steps). +?FUNCTION(scroll_forward, Steps). +?SPEC(scroll_backward). +?FUNCTION(scroll_backward). +?SPEC(scroll_backward, Steps). +?FUNCTION(scroll_backward, Steps). +?SPEC(scroll_change_region, Line1, Line2). +?FUNCTION(scroll_change_region, Line1, Line2). + +?SPEC(tab). +?FUNCTION(tab). +?SPEC(tab_backward). +?FUNCTION(tab_backward). +?SPEC(tab_set). +?FUNCTION(tab_set). +?SPEC(tab_clear). +?FUNCTION(tab_clear). +?SPEC(tab_clear_all). +?FUNCTION(tab_clear_all). + +?SPEC(keypad_transmit_mode). +?FUNCTION(keypad_transmit_mode). +?SPEC(keypad_transmit_mode_off). +?FUNCTION(keypad_transmit_mode_off). + +?SPEC(reset). +?FUNCTION(reset). + +?SPEC(device_report_attributes). +?FUNCTION(device_report_attributes). + +%% See https://gcc.gnu.org/cgit/gcc/commit/?id=458c8d6459c4005fc9886b6e25d168a6535ac415 for +%% details on how to check whether we can use terminal URLs. +%% The specification for how to ANSI URLs work is here: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +-spec hyperlink_start(uri_string:uri_string()) -> unicode:chardata(). +?FUNCTION(hyperlink_start, URL). +-spec hyperlink_start(uri_string:uri_string(), + [{Key :: unicode:chardata(), Value :: unicode:chardata()}]) -> unicode:chardata(). +?FUNCTION(hyperlink_start, URL, Params). +?SPEC(hyperlink_reset). +?FUNCTION(hyperlink_reset). + +-doc """ +Check if `t:io:user/0` can interpret ANSI escape sequences. +""". +-spec enabled() -> boolean(). +enabled() -> + enabled(user). + +-doc """ +Check if `Device` can interpret ANSI escape sequences. + +This is done by checking if `Device` represents a terminal and if the `TERM` +environment variable is set to a terminal type that supports virtual terminal +sequences. +""". +-spec enabled(io:device()) -> boolean(). +enabled(Device) -> + IsTerminal = + case io:getopts(Device) of + {error, _} -> false; + Opts -> + proplists:get_value(terminal, Opts, false) + end, + IsSmartTerminal = + case os:type() of + {win32, _} -> true; + _ -> + prim_tty:tigetstr("sgr0") =/= false + end, + IsTerminal andalso IsSmartTerminal. + +-doc """ +Scan the string for virtial terminal sequences. + +The recognized VTSs will be converted into the corresponding `t:code/0`. + +If you intend to parse arrow keys it is recommended that you first set the terminal in +application mode by using `io_ansi:format(standard_out, [keypad_transmit_mode], [], [])`. +This will make it easier for io_ansi to correctly detect arrow keys. + +Any unrecognized [control sequence introducers](https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands), +will be placed in a tuple tagged with `csi`. + +Example: + +```erlang +1> io_ansi:scan("\eOA"). +[kcursor_up] +2> io_ansi:scan("\eOB"). +[kcursor_down] +3> io_ansi:scan(io_ansi:format([bold, "text"])). +[bold, ~"text", reset] +4> io_ansi:scan(io_ansi:format([{cursor, 0, 0}])). +[{csi, ~"\e[1;1H"}, reset] +``` + +""". +-spec scan(unicode:chardata()) -> [unicode:unicode_binary() | code() | {csi, unicode:unicode_binary()}]. +scan(Data) -> + scan_binary(unicode:characters_to_binary(Data), <<>>, []). + +scan_binary(<> = Data, Bin, Acc) when CSI =:= $\e; CSI =:= 155 -> + case lookup_vts(Data) of + undefined -> + case re:run(Data, prim_tty:ansi_regexp(), [unicode]) of + {match, [{0, N}]} -> + <> = Data, + scan_binary(AnsiRest, <<>>, [{csi, Ansi}, Bin | Acc]); + nomatch -> + scan_binary(R, <>, Acc) + end; + {Code, _, Rest} -> + scan_binary(Rest, <<>>, [Code, Bin | Acc]) + end; +scan_binary(<>, Bin, Acc) -> + scan_binary(R, <>, Acc); +scan_binary(<<>>, Bin, Acc) -> + [NonEmpty || NonEmpty <- lists:reverse([Bin | Acc]), NonEmpty =/= <<>>]. + +lookup_vts(Data) -> + try + [case Data of + <> -> + throw({Key, Value, Rest}); + _ -> + ok + end || Key := Values <- get_vts_mappings(), + Value <- Values], + undefined + catch throw:KeyValueRest -> + KeyValueRest + end. + +-doc #{ equiv => format(Format, []) }. +-spec format(format()) -> unicode:unicode_binary(). +format(Format) -> + format(Format, []). + +-doc #{ equiv => format(Format, [], []) }. +-spec format(format(), Data :: [term()]) -> unicode:unicode_binary(). +format(Format, Data) -> + format(Format, Data, []). + +-doc """ +Returns a character list that represents `Data` formatted in accordance with +`Format`. + +This function works just as `io_lib:bformat/2`, except that it also allows +atoms and tuples represeting virtual terminal sequences as part of the +`Format` string. + +Example: + +```erlang +1> io_ansi:format([blue, underline, "Hello world"]). +~"\e[34m\e[4mHello world\e(B\e[m" +``` + +For a detailed description of the available formatting options, see `io:fwrite/3`. +""". +-spec format(format(), Data :: [term()], options()) -> unicode:unicode_binary(). +format(Format, Data, Options) -> + format_internal(Format, Data, Options). + +format_internal(Format, Data, Options) -> + UseAnsi = case proplists:get_value(enabled, Options) of + undefined -> enabled(); + Enabled -> Enabled + end, + %% Only to be used by fwrite + FormatOnly = proplists:get_value(format_only, Options, false), + AppendReset = [reset || proplists:get_value(reset, Options, true)], + Mappings = get_mappings(), + try lists:foldl( + fun(Ansi, {Acc, Args}) when is_atom(Ansi) orelse is_tuple(Ansi), + UseAnsi, FormatOnly -> + {[Ansi | Acc], Args}; + (Ansi, {Acc, Args}) when is_atom(Ansi), UseAnsi -> + AnsiFun = lookup(Mappings, Ansi, 0), + {[AnsiFun() | Acc], Args}; + (Ansi, {Acc, Args}) when is_tuple(Ansi), UseAnsi -> + [AnsiCode | AnsiArgs] = tuple_to_list(Ansi), + AnsiFun = lookup(Mappings, AnsiCode, length(AnsiArgs)), + {[apply(AnsiFun, AnsiArgs) | Acc], Args}; + (Ansi, {Acc, Args}) when is_atom(Ansi); is_tuple(Ansi) -> + {Acc, Args}; + (Fmt, {Acc, Args}) -> + {Scanned, Rest} = io_lib_format:scan(Fmt, Args), + {[io_lib_format:build_bin(Scanned) | Acc], Rest} + end, {[], Data}, group(lists:flatten([Format,AppendReset]))) of + {Scanned, []} -> + if FormatOnly -> + lists:flatten(lists:reverse(Scanned)); + not FormatOnly -> + unicode:characters_to_binary(lists:reverse(Scanned)) + end; + _ -> + erlang:error(badarg, [Format, Data, Options]) + catch E:R:ST -> + erlang:raise(E,R,ST) + %% erlang:error(badarg, [Format, Data, Options]) + end. + +-doc #{ equiv => fwrite(standard_io, Format, [], []) }. +-spec fwrite(Format :: format()) -> ok. +fwrite(Format) -> + fwrite(standard_io, Format, [], []). + +-doc #{ equiv => fwrite(standard_io, Format, Data, []) }. +-spec fwrite(Format :: format(), [term()]) -> ok. +fwrite(Format, Data) -> + fwrite(standard_io, Format, Data, []). + +-doc #{ equiv => fwrite(standard_io, Format, Data, Options) }. +-spec fwrite(Format :: format(), [term()], options()) -> ok. +fwrite(Format, Data, Options) -> + fwrite(standard_io, Format, Data, Options). +-doc """ +Writes the items in `Data` on the [`IoDevice`](`t:device/0`) in accordance with `Format`. + +This function works just as `io:fwrite/2`, except that it also allows atoms and +tuples representing virtual terminal sequences (VTS) as part of the `Format` string. + +Example: + +```erlang +1> io_ansi:fwrite([blue, "%% Hello world\n"]). +%% Hello world +ok +``` + +The decision what each VTS should be converted to is done by the destination I/O +device. This means that if the I/O device is on a remote node, the terminfo +database loaded into that remote node will be used. + +All VTSs are stripped if the target I/O device does not support handling VTSs, +either because it is not implemented by the device (for example if the device +is a `t:file:io_server/0`) or if the device does not support a certain VTS. +If you want to force usage of VTSs you can pass `{enabled,true}` and that will +use the local defintions to translate. +""". +-spec fwrite(IODevice :: io:device(), Format :: format(), [term()], options()) -> ok. +fwrite(Device, Format, Data, Options) -> + Ref = make_ref(), + try + lists:foldl( + fun F(Chars, ok) when is_binary(Chars) -> + io:request(Device, {put_chars, unicode, Chars}); + F(Ansi, ok) when is_atom(Ansi); is_tuple(Ansi) -> + case io:request(Device,{put_ansi, Options, Ansi}) of + {error, request} -> + %% The IO server did not support printing ansi. + case proplists:get_value(enabled, Options, Ref) of + true -> + %% If ansi is forced by the ansi option, + %% we format using the local + %% ansi definition and send as characters + F(format([Ansi], [], Options), ok); + Ref -> + %% We drop the ansi codes + ok + end; + Else -> Else + end; + F(_Data, Error) -> + throw({Ref, Error}) + end, ok, format_internal(Format, Data, [{format_only, true} | Options])) + catch {Ref, Error} -> + erlang:error(Error, [Device, Format, Data, Options]) + end. + +group(Fmt) -> + group(Fmt, []). +group([Ansi | T], []) when is_atom(Ansi); is_tuple(Ansi) -> + [Ansi | group(T, [])]; +group([Ansi | T], Acc) when is_atom(Ansi); is_tuple(Ansi) -> + [lists:reverse(Acc), Ansi | group(T, [])]; +group([C | T], Acc) -> + group(T, [C | Acc]); +group([], []) -> + []; +group([], Acc) -> + [lists:reverse(Acc)]. + +sgr(Args) -> ["\e[",lists:join($;,Args),"m"]. + +lookup(Key, Arity) -> + lookup(get_mappings(), Key, Arity). +lookup(Mappings, Key, Arity) -> + case maps:get(Arity, maps:get(Key, Mappings)) of + #{ terminfo := TermInfoFun } -> TermInfoFun; + #{ ansi := AnsiFun } -> AnsiFun + end. + +get_mappings() -> + case persistent_term:get(?MODULE, undefined) of + undefined -> + persistent_term:put(?MODULE, init_mappings()), + get_mappings(); + Value -> Value + end. + +init_mappings() -> + maps:map( + fun + Map(Key, Mapping) when not is_list(Mapping) -> + Map(Key, [Mapping]); + Map(_Key, Mappings) when is_list(Mappings) -> + maps:from_list( + lists:map( + fun + Fun({undefined, AnsiFun}) when is_function(AnsiFun) -> + {arity, Arity} = erlang:fun_info(AnsiFun, arity), + {Arity, #{ ansi => AnsiFun }}; + Fun({undefined, AnsiString}) when not is_function(AnsiString) -> + Fun({undefined, fun() -> AnsiString end}); + Fun({TermInfoCap, AnsiFun}) when is_function(AnsiFun) -> + try prim_tty:tigetstr(TermInfoCap) of + {ok, TermInfoStr} -> + {arity, Arity} = erlang:fun_info(AnsiFun, arity), + TermInfoFun = + case Arity of + 0 -> fun() -> {ok, S} = prim_tty:tputs(TermInfoStr, []), S end; + 1 -> fun(A) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A]), S end; + 2 -> fun(A, B) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B]), S end; + 3 -> fun(A, B, C) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B, C]), S end; + 4 -> fun(A, B, C, D) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B, C, D]), S end; + 5 -> fun(A, B, C, D, E) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B , C, D, E]), S end; + 6 -> fun(A, B, C, D, E, F) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F]), S end; + 7 -> fun(A, B, C, D, E, F, G) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F, G]), S end; + 8 -> fun(A, B, C, D, E, F, G, H) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F, G, H]), S end; + 9 -> fun(A, B, C, D, E, F, G, H, I) -> {ok, S} =prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F, G, H, I]), S end + end, + { Arity, #{ terminfo => TermInfoFun, ansi => AnsiFun }}; + false -> Fun({undefined, AnsiFun}) + catch error:badarg -> + Fun({undefined, AnsiFun}) + end; + Fun({{TermInfoCap, Args}, AnsiString}) when not is_function(AnsiString) -> + try prim_tty:tigetstr(TermInfoCap) of + {ok, S} -> + {ok, TermInfoStr} = prim_tty:tputs(S, Args), + {0, #{ terminfo => fun() -> TermInfoStr end, + ansi => fun() -> AnsiString end } }; + false -> + Fun({undefined, AnsiString}) + catch error:badarg -> + Fun({undefined, AnsiString}) + end; + Fun({TermInfoCap, AnsiString}) when not is_function(AnsiString) -> + Fun({{TermInfoCap, []}, AnsiString}) + end, Mappings)) + end, default_mappings()). + +get_vts_mappings() -> + case persistent_term:get(io_ansi_vts_mappings, undefined) of + undefined -> + persistent_term:put(io_ansi_vts_mappings, init_vts_mappings()), + get_vts_mappings(); + Value -> Value + end. + +init_vts_mappings() -> + maps:map(fun Map(Key, Value) when not is_list(Value) -> + Map(Key, [Value]); + Map(_Key, Values) when is_list(Values) -> + lists:flatmap(fun F({_TermInfoCap, Fun}) when is_function(Fun) -> + []; + F({undefined, Ansi}) -> + [unicode:characters_to_binary(Ansi)]; + F({{TermInfoCap, Args}, Ansi}) -> + try prim_tty:tigetstr(TermInfoCap) of + {ok, S} -> + {ok, TermInfoStr} = prim_tty:tputs(S, Args), + [unicode:characters_to_binary(TermInfoStr), + unicode:characters_to_binary(Ansi)]; + undefined -> + F({undefined, Ansi}) + catch error:badarg -> + F({undefined, Ansi}) + end; + F({TermInfoCap, Ansi}) -> + F({{TermInfoCap, []}, Ansi}) + end, Values) + end, default_mappings()). + +default_mappings() -> + #{ cursor => + [{"cup", fun(Line, Column) -> ["\e[",Line+1,";",Column+1,"H"] end}], + cursor_home => {"home", "\eH"}, + cursor_up => + [{"cuu1", "\e[A"}, + {"cuu", fun(Steps) -> ["\e[", Steps ,"A"] end}], + + cursor_down => + [{"cud1", "\n"}, + {"cud", fun(Steps) -> ["\e[", Steps ,"B"] end}], + + cursor_backward => + [{"cub1", "\b"}, + {"cub", fun(Steps) -> ["\e[", Steps ,"D"] end}], + + cursor_forward => + [{"cuf1", "\e[C"}, + {"cuf", fun(Steps) -> ["\e[", Steps ,"C"] end}], + + reverse_index => { "ri", "\eM" }, + cursor_save => { "sc", "\e7" }, + cursor_restore => { "rc", "\e8" }, + cursor_show => { "cvvis", "\e[?25h"}, + cursor_hide => { "civis", "\e[?25l"}, + + cursor_next_line => { "nel", "\e[E" }, + cursor_previous_line => { undefined, "\e[F" }, + cursor_horizontal_absolute => { "hpa", fun(X) -> ["\e[", X, "G"] end}, + cursor_vertical_absolute => { "vpa", fun(Y) -> ["\e[", Y, "d"] end}, + cursor_horizontal_vertical => { undefined, fun(X, Y) -> ["\e[", X, ";", Y, "f"] end}, + + alternate_screen => { "smcup", "\e[?1049h" }, + alternate_screen_off => { "rmcup", "\e[?1049l" }, + + scroll_forward => [{{"indn", [1]}, "\eS"}, + { "indn", fun(Steps) -> ["\e", Steps, "S"] end}], + scroll_backward => [{{"rin", [1]}, "\eT"}, + { "rin", fun(Steps) -> ["\e", Steps, "T"] end}], + scroll_change_region => {"csr", fun(Line1, Line2) -> ["\e[",Line1,";",Line2,"r"] end}, + + insert_character => { "ich", fun(Chars) -> ["\e[", Chars, "@"] end}, + delete_character => [{"dch1", "\e[P"}, + { "dch", fun(Chars) -> ["\e[", Chars, "P"] end}], + erase_character => { "ech", fun(Chars) -> ["\e[", Chars, "X"] end}, + insert_line => [{"il1", "\e[L"}, + { "il", fun(Chars) -> ["\e[", Chars, "L"] end}], + delete_line => [{"dl1", "\e[M"}, + { "dl", fun(Chars) -> ["\e[", Chars, "M"] end}], + + erase_display => { "ed", "\e[J"}, + erase_line => {"el", "\e[K"}, + clear => { "clear", "\e[H\e[2J"}, + + modify_color => { "initc", fun(Index, R, G, B) -> + io_lib:format("\e]4;~.16b;rgb:~.16b/~.16b/~.16b\e\\",[Index, R, G, B]) + end}, + + keypad_transmit_mode => { "smkx", "\e=" }, + keypad_transmit_mode_off => { "rmkx", "\e>" }, + + kcursor_home => {"khome", "\eH"}, + kcursor_end => {"kend", "\eF"}, + kcursor_up => {"kcuu1", "\e[A"}, + kcursor_down => {"kcud1", "\e[B"}, + kcursor_backward => {"kcub1", "\e[D"}, + kcursor_forward => {"kcuf1", "\e[C"}, + + cursor_report_position => { "u7", "\e[6n" }, + device_report_attributes => { undefined, "\e[0c"}, + + tab_set => { "hts", "\eH" }, + tab => { "ht", "\e[I" }, + tab_backward => { "cbt", "\eI" }, + tab_clear => { undefined, "\e[0g" }, + tab_clear_all => { "tbc", "\e[3g" }, + + reset => { "sgr0", sgr(["0"])}, + + black => { {"setaf", [0]}, sgr(["30"]) }, + red => { {"setaf", [1]}, sgr(["31"]) }, + green => { {"setaf", [2]}, sgr(["32"]) }, + yellow => { {"setaf", [3]}, sgr(["33"]) }, + blue => { {"setaf", [4]}, sgr(["34"]) }, + magenta => { {"setaf", [5]}, sgr(["35"]) }, + cyan => { {"setaf", [6]}, sgr(["36"]) }, + white => { {"setaf", [7]}, sgr(["37"]) }, + default_color => { undefined, sgr(["39"]) }, + color => [{"setaf", fun(Index) -> sgr(["38","5",Index]) end}, + {undefined, fun(R, G, B) -> sgr(["38","2",R,G,B]) end}], + + black_background => { {"setab", [0]}, sgr(["40"]) }, + red_background => { {"setab", [1]}, sgr(["41"]) }, + green_background => { {"setab", [2]}, sgr(["42"]) }, + yellow_background => { {"setab", [3]}, sgr(["43"]) }, + blue_background => { {"setab", [4]}, sgr(["44"]) }, + magenta_background => { {"setab", [5]}, sgr(["45"]) }, + cyan_background => { {"setab", [6]}, sgr(["46"]) }, + white_background => { {"setab", [7]}, sgr(["47"]) }, + default_background => { undefined, sgr(["49"]) }, + background => [{"setab", fun(Index) -> sgr(["48","5",Index]) end}, + {undefined, fun(R, G, B) -> sgr(["48","2",R,G,B]) end}], + + light_black => { {"setaf", [8]}, sgr(["90"]) }, + light_red => { {"setaf", [9]}, sgr(["91"]) }, + light_green => { {"setaf", [10]}, sgr(["92"]) }, + light_yellow => { {"setaf", [11]}, sgr(["93"]) }, + light_blue => { {"setaf", [12]}, sgr(["94"]) }, + light_magenta => { {"setaf", [13]}, sgr(["95"]) }, + light_cyan => { {"setaf", [14]}, sgr(["96"]) }, + light_white => { {"setaf", [15]}, sgr(["97"]) }, + + light_black_background => { {"setab", [8]}, sgr(["100"]) }, + light_red_background => { {"setab", [9]}, sgr(["101"]) }, + light_green_background => { {"setab", [10]}, sgr(["102"]) }, + light_yellow_background => { {"setab", [11]}, sgr(["103"]) }, + light_light_blue_background => { {"setab", [12]}, sgr(["104"]) }, + light_magenta_background => { {"setab", [13]}, sgr(["105"]) }, + light_cyan_background => { {"setab", [14]}, sgr(["106"]) }, + light_white_background => { {"setab", [15]}, sgr(["107"]) }, + + bold => { "bold", "\e[1m" }, + bold_off => { undefined, "\e[22m" }, + underline => { "smul", "\e[4m" }, + underline_off => { "rmul", "\e[24m" }, + negative => { "smso", "\e[7m"}, + negative_off => { "rmso", "\e[27m"}, + + alternate_character_set_mode => {"smacs", "\e(0" }, + alternate_character_set_mode_off => {"rmacs", "\e(B" }, + + hyperlink => + [{undefined, hyperlink("", [])}, + {undefined, fun(Url) -> hyperlink(Url, []) end}, + {undefined, fun(Url, Params) -> hyperlink(Url, Params) end}] + }. + +hyperlink(URL, Params) -> + StringParams = lists:join($:, [[K, "=", V] || {K, V} <- Params]), + io_lib:format("\e]8;~s;~s\e\\",[URL, StringParams]). \ No newline at end of file diff --git a/lib/stdlib/src/io_lib.erl b/lib/stdlib/src/io_lib.erl index e43541da93cb..b07ddca62b16 100644 --- a/lib/stdlib/src/io_lib.erl +++ b/lib/stdlib/src/io_lib.erl @@ -109,7 +109,8 @@ used for flattening deep lists. -export([write_bin/5, write_string_bin/3, write_binary_bin/4]). -export_type([chars/0, latin1_string/0, continuation/0, - fread_error/0, fread_item/0, format_spec/0, chars_limit/0]). + fread_error/0, fread_item/0, format_spec/0, + chars_limit/0, format_options/0]). -dialyzer([{nowarn_function, [string_bin_escape_unicode/6, @@ -195,8 +196,20 @@ Unicode data is allowed. fwrite(Format, Args) -> format(Format, Args). +-doc """ +A soft limit on the number of characters returned. + +When the number of characters is reached, remaining structures are +replaced by "`...`". `CharsLimit` defaults to -1, which means no limit on the +number of characters returned. +""". -type chars_limit() :: integer(). +-doc """ +Options that can be passed to `format/3` and `fwrite/3`. +""". +-type format_options() :: [{'chars_limit', chars_limit()}]. + -doc """ Returns a character list that represents `Data` formatted in accordance with `Format` in the same way as `fwrite/2` and `format/2`, but takes an extra @@ -213,9 +226,7 @@ Valid option: -spec fwrite(Format, Data, Options) -> chars() when Format :: io:format(), Data :: [term()], - Options :: [Option], - Option :: {'chars_limit', CharsLimit}, - CharsLimit :: chars_limit(). + Options :: format_options(). fwrite(Format, Args, Options) -> format(Format, Args, Options). @@ -239,9 +250,7 @@ bfwrite(F, A) -> -spec bfwrite(Format, Data, Options) -> unicode:unicode_binary() when Format :: io:format(), Data :: [term()], - Options :: [Option], - Option :: {'chars_limit', CharsLimit}, - CharsLimit :: chars_limit(). + Options :: format_options(). bfwrite(F, A, Opts) -> bformat(F, A, Opts). @@ -344,9 +353,7 @@ format(Format, Args) -> -spec format(Format, Data, Options) -> chars() when Format :: io:format(), Data :: [term()], - Options :: [Option], - Option :: {'chars_limit', CharsLimit}, - CharsLimit :: chars_limit(). + Options :: format_options(). format(Format, Args, Options) -> try io_lib_format:fwrite(Format, Args, Options) @@ -376,9 +383,7 @@ bformat(Format, Args) -> -spec bformat(Format, Data, Options) -> unicode:unicode_binary() when Format :: io:format(), Data :: [term()], - Options :: [Option], - Option :: {'chars_limit', CharsLimit}, - CharsLimit :: chars_limit(). + Options :: format_options(). bformat(Format, Args, Options) -> try io_lib_format:fwrite_bin(Format, Args, Options) @@ -408,7 +413,9 @@ formatting to text in, for example, a logger. FormatList :: [char() | format_spec()]. scan_format(Format, Args) -> - try io_lib_format:scan(Format, Args) + try + {Scanned, []} = io_lib_format:scan(Format, Args), + Scanned catch C:R:S -> test_modules_loaded(C, R, S), @@ -441,9 +448,7 @@ build_text(FormatList) -> -doc false. -spec build_text(FormatList, Options) -> chars() when FormatList :: [char() | format_spec()], - Options :: [Option], - Option :: {'chars_limit', CharsLimit}, - CharsLimit :: chars_limit(). + Options :: format_options(). build_text(FormatList, Options) -> try io_lib_format:build(FormatList, Options) diff --git a/lib/stdlib/src/io_lib_format.erl b/lib/stdlib/src/io_lib_format.erl index 4e7614a7e84b..2d4783ca7e2d 100644 --- a/lib/stdlib/src/io_lib_format.erl +++ b/lib/stdlib/src/io_lib_format.erl @@ -55,7 +55,8 @@ Data :: [term()]. fwrite(Format, Args) -> - build(scan(Format, Args)). + {Scanned, []} = scan(Format, Args), + build(Scanned). -spec fwrite(Format, Data, Options) -> io_lib:chars() when Format :: io:format(), @@ -65,7 +66,8 @@ fwrite(Format, Args) -> CharsLimit :: io_lib:chars_limit(). fwrite(Format, Args, Options) -> - build(scan(Format, Args), Options). + {Scanned, []} = scan(Format, Args), + build(Scanned, Options). %% Binary variants -spec fwrite_bin(Format, Data) -> unicode:unicode_binary() when @@ -73,7 +75,8 @@ fwrite(Format, Args, Options) -> Data :: [term()]. fwrite_bin(Format, Args) -> - build_bin(scan(Format, Args)). + {Scanned, []} = scan(Format, Args), + build_bin(Scanned). -spec fwrite_bin(Format, Data, Options) -> unicode:unicode_binary() when Format :: io:format(), @@ -83,7 +86,8 @@ fwrite_bin(Format, Args) -> CharsLimit :: io_lib:chars_limit(). fwrite_bin(Format, Args, Options) -> - build_bin(scan(Format, Args), Options). + {Scanned, []} = scan(Format, Args), + build_bin(Scanned, Options). %% Build the output text for a pre-parsed format list. @@ -139,10 +143,11 @@ build_bin(Cs, Options) -> %% Parse all control sequences in the format string. --spec scan(Format, Data) -> FormatList when +-spec scan(Format, Data) -> {FormatList, Rest} when Format :: io:format(), Data :: [term()], - FormatList :: [char() | io_lib:format_spec()]. + FormatList :: [char() | io_lib:format_spec()], + Rest :: [term()]. scan(Format, Args) when is_atom(Format) -> scan(atom_to_list(Format), Args); @@ -209,12 +214,15 @@ print_maps_order(ordered) -> "k"; print_maps_order(reversed) -> "K"; print_maps_order(CmpFun) when is_function(CmpFun, 2) -> "K". -collect([$~|Fmt0], Args0) -> +collect(Fmt, Args) -> + collect(Fmt, Args, []). +collect([$~|Fmt0], Args0, Acc) -> {C,Fmt1,Args1} = collect_cseq(Fmt0, Args0), - [C|collect(Fmt1, Args1)]; -collect([C|Fmt], Args) -> - [C|collect(Fmt, Args)]; -collect([], []) -> []. + collect(Fmt1, Args1, [C | Acc]); +collect([C|Fmt], Args, Acc) -> + collect(Fmt, Args, [C | Acc]); +collect([], Remain, Acc) -> + {lists:reverse(Acc), Remain}. collect_cseq(Fmt0, Args0) -> {F,Ad,Fmt1,Args1} = field_width(Fmt0, Args0), diff --git a/lib/stdlib/test/Makefile b/lib/stdlib/test/Makefile index 1ce324b46c87..214626e7b246 100644 --- a/lib/stdlib/test/Makefile +++ b/lib/stdlib/test/Makefile @@ -73,6 +73,7 @@ MODULES= \ gen_statem_SUITE \ id_transform_SUITE \ io_SUITE \ + io_ansi_SUITE \ io_proto_SUITE \ json_SUITE \ lists_SUITE \ diff --git a/lib/stdlib/test/io_ansi_SUITE.erl b/lib/stdlib/test/io_ansi_SUITE.erl new file mode 100644 index 000000000000..3f95e54d5deb --- /dev/null +++ b/lib/stdlib/test/io_ansi_SUITE.erl @@ -0,0 +1,71 @@ +%% +%% %CopyrightBegin% +%% +%% SPDX-License-Identifier: Apache-2.0 +%% +%% Copyright Ericsson AB 2020-2025. 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% +%% + +-module(io_ansi_SUITE). +-moduledoc false. + +-behaviour(ct_suite). + +-export([all/0, suite/0, groups/0, init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2]). + +-export([format_3/1, doctests/1]). + +-include_lib("stdlib/include/assert.hrl"). + +suite() -> + []. + +all() -> + [ doctests, format_3 ]. + + +groups() -> + [ ]. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, _Config) -> + ok. + +init_per_testcase(_TestCase, Config) -> + Config. + +end_per_testcase(_TestCase, _Config) -> + ok. + +format_3(_Config) -> + true = io_ansi:enabled(), + ok. + +doctests(_Config) -> + shell_docs:test( + io_ansi, + []), + ok. \ No newline at end of file From fdf0fa067e57a60adb13bf6ee689845afef3f8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 14 May 2025 12:52:04 +0200 Subject: [PATCH 04/15] Migrate prim_tty to use io_ansi --- bootstrap/lib/kernel/ebin/prim_tty.beam | Bin 28328 -> 26808 bytes lib/kernel/src/prim_tty.erl | 160 ++++-------------------- 2 files changed, 21 insertions(+), 139 deletions(-) diff --git a/bootstrap/lib/kernel/ebin/prim_tty.beam b/bootstrap/lib/kernel/ebin/prim_tty.beam index 6e7e7a8152f62554d1ba6e2e53534fa8ca41cc53..3dd9fbaa6fe0d4174c30b917f1b522e7c491ec75 100644 GIT binary patch literal 26808 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 literal 28328 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 diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl index 430740d6ab32..bbafa2634b21 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -111,7 +111,7 @@ handle_signal/2, window_size/1, update_geometry/3, handle_request/2, write/2, write/3, npwcwidth/1, npwcwidth/2, - ansi_regexp/0, ansi_color/2]). + ansi_regexp/0]). -export([reader_stop/1, disable_reader/1, enable_reader/1, read/1, read/2, is_reader/2, is_writer/2, output_mode/1]). @@ -146,6 +146,7 @@ reader :: {pid(), reference()} | undefined, writer :: {pid(), reference()} | undefined, options = #{ input => cooked, output => cooked } :: options(), + have_termcap = false :: boolean(), unicode = true :: boolean(), lines_before = [], %% All lines before the current line in reverse order lines_after = [], %% All lines after the current line. @@ -155,22 +156,9 @@ buffer_expand_row = 1, buffer_expand_limit = 0 :: non_neg_integer(), putc_buffer = <<>>, %% Buffer for putc containing the last row of characters - have_termcap = false :: boolean(), cols = 80, rows = 24, xn = false, - clear = <<"\e[H\e[2J">>, - up = <<"\e[A">>, - down = <<"\n">>, - left = <<"\b">>, - right = <<"\e[C">>, - %% Tab to next 8 column windows is "\e[1I", for unix "ta" termcap - tab = <<"\e[1I">>, - delete_after_cursor = <<"\e[J">>, - insert = false, %% Not used - delete = false, %% Not used - position = <<"\e[6n">>, %% "u7" on my Linux, Not used - position_reply = <<"\e\\[([0-9]+);([0-9]+)R">>, %% Not used ansi_regexp }). @@ -204,32 +192,6 @@ ansi_regexp() -> ?ANSI_REGEXP. -ansi_fg_color(Color) -> - case Color of - black -> 30; - red -> 31; - green -> 32; - yellow -> 33; - blue -> 34; - magenta -> 35; - cyan -> 36; - white -> 37; - bright_black -> 90; - bright_red -> 91; - bright_green -> 92; - bright_yellow -> 93; - bright_blue -> 94; - bright_magenta -> 95; - bright_cyan -> 96; - bright_white -> 97 - end. -ansi_bg_color(Color) -> - ansi_fg_color(Color) + 10. - --spec ansi_color(BgColor :: atom(), FgColor :: atom()) -> iolist(). -ansi_color(BgColor, FgColor) -> - io_lib:format("\e[~w;~wm", [ansi_bg_color(BgColor), ansi_fg_color(FgColor)]). - -spec load() -> ok. load() -> case erlang:load_nif(atom_to_list(?MODULE), #{}) of @@ -370,85 +332,14 @@ init(State, {unix,_}) -> State#state.have_termcap orelse error(enotsup), - - %% See https://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html#SEC23 - %% for a list of all possible termcap capabilities - Clear = case tigetstr("clear") of - {ok, C} -> C; - false -> (#state{})#state.clear - end, - Cols = case tigetnum("co") of {ok, Cs} -> Cs; _ -> (#state{})#state.cols end, - Up = case tigetstr("cuu1") of - {ok, U} -> U; - false -> error(enotsup) - end, - Down = case tigetstr("cud1") of - false -> (#state{})#state.down; - {ok, D} -> D - end, - Left = case {tigetflag("OTbs"),tigetstr("OTbc")} of - {true,_} -> (#state{})#state.left; - {_,false} -> (#state{})#state.left; - {_,{ok, L}} -> L - end, - - Right = case tigetstr("cuf1") of - {ok, R} -> R; - false -> error(enotsup) - end, - Insert = - case tigetstr("ich") of - {ok, IC} -> IC; - false -> (#state{})#state.insert - end, - - Tab = case tigetstr("ht") of - {ok, TA} -> TA; - false -> (#state{})#state.tab - end, - - Delete = case tigetstr("dch") of - {ok, DC} -> DC; - false -> (#state{})#state.delete - end, - - Position = case tigetstr("u7") of - {ok, <<"\e[6n">> = U7} -> - %% User 7 should contain the codes for getting - %% cursor position. - %% User 6 should contain how to parse the reply - {ok, <<"\e[%i%d;%dR">>} = tigetstr("u6"), - <<"\e[6n">> = U7; - false -> (#state{})#state.position - end, - - %% According to the manual this should only be issued when the cursor - %% is at position 0, but until we encounter such a console we keep things - %% simple and issue this with the cursor anywhere - DeleteAfter = case tigetstr("ed") of - {ok, DA} -> - DA; - false -> - (#state{})#state.delete_after_cursor - end, State#state{ cols = Cols, - clear = Clear, - xn = tigetflag("xn"), - up = Up, - down = Down, - left = Left, - right = Right, - insert = Insert, - delete = Delete, - tab = Tab, - position = Position, - delete_after_cursor = DeleteAfter + xn = tigetflag("xn") }; init(State, {win32, _}) -> State#state{ @@ -736,9 +627,8 @@ handle_request(State = #state{unicode = U, cols = W, rows = R}, redraw_prompt_pr ExpandRowsLimit1 = min(ExpandRowsLimit, R-1-InputRows), BufferExpand1 = case ExpandRows > ExpandRowsLimit1 of true -> - Color = lists:flatten(ansi_color(cyan, bright_white)), - StatusLine = io_lib:format(Color ++"\e[1m" ++ "rows ~w to ~w of ~w" ++ "\e[0m", - [ERow, (ERow-1) + ExpandRowsLimit1, ExpandRows]), + StatusLine = io_ansi:format([cyan_background, light_white, bold, "rows ~w to ~w of ~w"], + [ERow, (ERow-1) + ExpandRowsLimit1, ExpandRows], [{enabled, true}]), Cols1 = max(0,W*ExpandRowsLimit1), Cols0 = max(0,W*(ERow-1)), {_, _, BufferExpandLinesInViewStart, {_, BEStartIVHalf}} = split_cols_multiline(Cols0, BufferExpandLines, U, W), @@ -780,7 +670,7 @@ handle_request(State = #state{ buffer_expand = Expand, buffer_expand_row = ERow, handle_request(State = #state{unicode = U, cols = W, buffer_before = Bef, lines_before = LinesBefore}, delete_line) -> MoveToBeg = move_cursor(State, cols_multiline(Bef, LinesBefore, W, U), 0), - {[MoveToBeg, State#state.delete_after_cursor], + {[MoveToBeg, io_ansi:erase_display()], State#state{buffer_before = [], buffer_after = [], lines_before = [], @@ -847,7 +737,7 @@ handle_request(State = #state{ unicode = U }, {putc, Binary}) -> {[Delete, Moves, encode(PutBuffer, U), Redraw], PutcBufferState} end; handle_request(State = #state{}, delete_after_cursor) -> - {[State#state.delete_after_cursor], + {[io_ansi:erase_display()], State#state{buffer_after = [], lines_after = []}}; handle_request(State = #state{ unicode = U, cols = W }, {delete, N}) when N > 0 -> @@ -1010,10 +900,10 @@ handle_request(State = #state{cols = W, xn = OrigXn, unicode = U,lines_after = L handle_request(State, beep) -> {<<7>>, State}; handle_request(State, clear) -> - {State#state.clear, State#state{buffer_before = [], - buffer_after = [], - lines_before = [], - lines_after = []}}; + {io_ansi:clear(), State#state{buffer_before = [], + buffer_after = [], + lines_before = [], + lines_after = []}}; handle_request(State, Req) -> erlang:display({unhandled_request, Req}), {"", State}. @@ -1101,14 +991,14 @@ move_cursor(#state{ cols = W } = State, FromCol, ToCol) -> move(right, State, N) end]. -move(up, #state{ up = Up }, N) -> - lists:duplicate(N, Up); -move(down, #state{ down = Down }, N) -> - lists:duplicate(N, Down); -move(left, #state{ left = Left }, N) -> - lists:duplicate(N, Left); -move(right, #state{ right = Right }, N) -> - lists:duplicate(N, Right). +move(up, _, N) -> + lists:duplicate(N, io_ansi:cursor_up()); +move(down, _, N) -> + lists:duplicate(N, io_ansi:cursor_down()); +move(left, _, N) -> + lists:duplicate(N, io_ansi:cursor_backward()); +move(right, _, N) -> + lists:duplicate(N, io_ansi:cursor_forward()). in_view(#state{lines_after = LinesAfter, buffer_before = Bef, buffer_after = Aft, lines_before = LinesBefore, rows=R, cols=W, unicode=U, buffer_expand = BufferExpand, buffer_expand_limit = BufferExpandLimit} = State) -> @@ -1253,16 +1143,8 @@ npwcwidth(Char, Encoding) -> %% Return the xn fix for the current cursor position. -%% We use get_position to figure out if we need to calculate the current columns -%% or not. -%% -%% We need to know the actual column because get_position will return the last -%% column number when the cursor is: -%% * in the last column -%% * off screen %% -%% and it is when the cursor is off screen that we should do the xnfix. -xnfix(#state{ position = _, unicode = U } = State) -> +xnfix(#state{ unicode = U } = State) -> xnfix(State, cols(State#state.buffer_before, U)). %% Return the xn fix for CurrCols location. xnfix(#state{ xn = true, cols = Cols } = State, CurrCols) @@ -1308,7 +1190,7 @@ insert_buf(State, Bin, LineAcc, Acc) -> {[Acc, characters_to_output(lists:reverse(LineAcc)), xnfix(NewState)], NewState}; [$\t | Rest] -> - insert_buf(State, Rest, [State#state.tab | LineAcc], Acc); + insert_buf(State, Rest, [io_ansi:tab() | LineAcc], Acc); [CSI | Rest] when CSI =:= $\e; CSI =:= 155 -> case ansi_sgr(Bin) of none -> From 5a3bdcbb6dcaf244803352500bdc2401b4712f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 14 May 2025 12:51:39 +0200 Subject: [PATCH 05/15] Migrate shell_docs to use io_ansi --- lib/stdlib/src/shell_docs.erl | 130 ++++++++++++++------------- lib/stdlib/test/shell_docs_SUITE.erl | 23 +++-- 2 files changed, 83 insertions(+), 70 deletions(-) diff --git a/lib/stdlib/src/shell_docs.erl b/lib/stdlib/src/shell_docs.erl index 9eda7d7b9ea4..f74908d6f147 100644 --- a/lib/stdlib/src/shell_docs.erl +++ b/lib/stdlib/src/shell_docs.erl @@ -928,13 +928,14 @@ render_meta_(_) -> render_headers_and_docs(Headers, DocContents, D, Config) -> render_headers_and_docs(Headers, DocContents, init_config(D, Config)). render_headers_and_docs(Headers, DocContents, #config{} = Config) -> - ["\n",render_docs( + unicode:characters_to_list( + ["\n",render_docs( lists:flatmap( fun(Header) -> [{br,[],[]},Header] end,Headers), Config), "\n", - render_docs(DocContents, 2, Config)]. + render_docs(DocContents, 2, Config)]). %%% Functions for rendering type/callback documentation render_signature_listing(Module, Type, D, Config) when is_map(Config) -> @@ -981,7 +982,7 @@ render_docs(DocContents, Ind, D = #config{}) when is_integer(Ind) -> init_ansi(D), try {Doc,_} = trimnl(render_docs(DocContents, [], 0, Ind, D)), - Doc + io_ansi:format(Doc, [], [{reset, false} | D#config.ansi]) after clean_ansi() end. @@ -996,9 +997,20 @@ init_config(D, Config) when is_map(Config) -> {ok, C} -> C end, + Ansi = + case maps:get(ansi, Config, undefined) of + undefined -> + case application:get_env(kernel, shell_docs_ansi) of + {ok, Enabled} when is_boolean(Enabled) -> + [{enabled, Enabled}]; + _ -> + [] + end; + Enabled when is_boolean(Enabled) -> [{enabled, Enabled}] + end, #config{ docs = D, encoding = maps:get(encoding, Config, DefaultEncoding), - ansi = maps:get(ansi, Config, undefined), + ansi = Ansi, columns = Columns, module = maps:get(module, Config, undefined) }; @@ -1208,7 +1220,7 @@ render_words([],_State,Pos,_Ind,Acc,_D) -> Line = lists:reverse(RevLine), lists:join($ ,Line) end,lists:reverse(Acc)), - {iolist_to_binary(Lines), Pos}. + {Lines, Pos}. %% If the encoding is not unicode, we translate all nbsp to sp translate(UnicodeWord, #config{ encoding = unicode }) -> @@ -1235,10 +1247,10 @@ nlpad(N) -> pad(N, Extra) -> Pad = lists:duplicate(N," "), case ansi() of - undefined -> + reset -> [Extra, Pad]; Ansi -> - ["\033[0m",Extra,Pad,Ansi] + [reset,Extra,Pad,Ansi] end. get_bullet(_State,latin1) -> @@ -1256,13 +1268,18 @@ get_bullet(State,unicode) -> %% Look for the length of the last line of a string lastline(Str) -> - LastStr = case string:find(Str,"\n",trailing) of - nomatch -> - Str; - Match -> - tl(string:next_codepoint(Match)) - end, - string:length(LastStr). + lastline(lists:reverse(characters_to_binary(Str)), 0). +lastline([Str | T], Sz) when is_atom(Str); is_tuple(Str) -> + lastline(T, Sz); +lastline([Str | T], Sz) when is_binary(Str) -> + case string:find(Str,"\n",trailing) of + nomatch -> + lastline(T, string:length(Str) + Sz); + Match -> + string:length(tl(string:next_codepoint(Match))) + Sz + end; +lastline([], Sz) -> + Sz. split_to_words(B) -> binary:split(B,[<<" ">>],[global]). @@ -1272,82 +1289,71 @@ split_to_words(B) -> %% that would add 4 \n at after the last . This is trimmed %% here to only be 2 \n trimnlnl({Chars, _Pos}) -> - nl(nl(string:trim(Chars, trailing, "\n"))); + nl(nl(trim(Chars, trailing, "\n"))); trimnlnl(Chars) -> - nl(nl(string:trim(Chars, trailing, "\n"))). + nl(nl(trim(Chars, trailing, "\n"))). trimnl({Chars, _Pos}) -> - nl(string:trim(Chars, trailing, "\n")). + nl(trim(Chars, trailing, "\n")). nl({Chars, _Pos}) -> nl(Chars); nl(Chars) -> {[Chars,"\n"],0}. -%% We keep the current ansi state in the pdict so that we know -%% what to disable and enable when doing padding -init_ansi(#config{ ansi = undefined, io_opts = Opts }) -> - %% We use this as our heuristic to see if we should print ansi or not - case {application:get_env(kernel, shell_docs_ansi), - proplists:get_value(terminal, Opts, false), - proplists:is_defined(echo, Opts) andalso - proplists:is_defined(expand_fun, Opts)} of - {{ok,false}, _, _} -> - put(ansi, noansi); - {{ok,true}, _, _} -> - put(ansi, []); - {_, true, _} -> - put(ansi, []); - {_, _, true} -> - put(ansi, []); - {_, _, false} -> - put(ansi, noansi) - end; -init_ansi(#config{ ansi = true }) -> - put(ansi, []); -init_ansi(#config{ ansi = false }) -> - put(ansi, noansi). +trim(Chars, Dir, What) -> + trim_flat(characters_to_binary(Chars), Dir, What). +trim_flat([H], Dir, What) when not is_atom(H), not is_tuple(H) -> + [string:trim([H], Dir, What)]; +trim_flat([H], _Dir, _What) when is_atom(H); is_tuple(H) -> + [H]; +trim_flat([H|T], Dir, What) when is_binary(H); is_atom(H); is_tuple(H) -> + [H | trim_flat(T, Dir, What)]; +trim_flat([], _, _) -> []. + +characters_to_binary(L) -> + characters_to_binary(lists:flatten(L), []). +characters_to_binary([], Acc) -> + lists:reverse(Acc); +characters_to_binary(L, Acc) -> + case lists:splitwith(fun is_not_ansi/1, L) of + {Str, []} -> + lists:reverse([unicode:characters_to_binary(Str) | Acc]); + {[], [Ansi | T]} -> + characters_to_binary(T, [Ansi | Acc]); + {Str, [Ansi | T]} -> + characters_to_binary(T, [Ansi, unicode:characters_to_binary(Str) | Acc]) + end. +is_not_ansi(C) -> + not (is_atom(C) orelse is_tuple(C)). +%% We keep the current ansi state in the pdict so that we know +%% what to disable and enable when doing padding +init_ansi(_) -> + put(ansi, []). clean_ansi() -> - case get(ansi) of - [] -> erase(ansi); - noansi -> erase(ansi) - end, + erase(ansi), ok. %% Set ansi sansi(Type) -> sansi(Type, get(ansi)). -sansi(_Type, noansi) -> - []; sansi(Type, Curr) -> put(ansi,[Type | Curr]), ansi(get(ansi)). %% Clear ansi ransi(Type) -> ransi(Type, get(ansi)). -ransi(_Type, noansi) -> - []; ransi(Type, Curr) -> put(ansi,proplists:delete(Type,Curr)), - case ansi(get(ansi)) of - undefined -> - "\033[0m"; - Ansi -> - Ansi - end. + ansi(get(ansi)). ansi() -> ansi(get(ansi)). -ansi(noansi) -> undefined; ansi(Curr) -> case lists:usort(Curr) of [] -> - undefined; - [bold] -> - "\033[;1m"; - [underline] -> - "\033[;;4m"; - [bold,underline] -> - "\033[;1;4m" + reset; + Else -> + Else end. filtermap_mfa({MetaKind, Function, none}, Map, Docs) -> diff --git a/lib/stdlib/test/shell_docs_SUITE.erl b/lib/stdlib/test/shell_docs_SUITE.erl index 4e6f17e1eb46..c073a3df1093 100644 --- a/lib/stdlib/test/shell_docs_SUITE.erl +++ b/lib/stdlib/test/shell_docs_SUITE.erl @@ -138,7 +138,7 @@ read_file(Filename) -> strip_comment(Data) -> case re:replace(Data, "^%.*\n", "", [{return, binary}]) of - Data -> {ok, Data}; + Data -> {ok, fixup(Data)}; NewData -> strip_comment(NewData) end. @@ -521,11 +521,11 @@ render_module(Mod, #docs_v1{ docs = Docs } = D) -> Files = #{ SMod ++ ".txt" => - unicode:characters_to_binary(shell_docs:render(Mod, D, Opts)), + fixup(unicode:characters_to_binary(shell_docs:render(Mod, D, Opts))), SMod ++ "_type.txt" => - unicode:characters_to_binary(shell_docs:render_type(Mod, D, Opts)), + fixup(unicode:characters_to_binary(shell_docs:render_type(Mod, D, Opts))), SMod ++ "_cb.txt" => - unicode:characters_to_binary(shell_docs:render_callback(Mod, D, Opts)) + fixup(unicode:characters_to_binary(shell_docs:render_callback(Mod, D, Opts))) }, lists:foldl( fun({_Type,_Anno,_Sig,none,_Meta}, Acc) -> @@ -533,8 +533,8 @@ render_module(Mod, #docs_v1{ docs = Docs } = D) -> ({{function,Name,Arity},_Anno,_Sig,_Doc,_Meta}, Acc) -> FAName = SMod ++ "_"++atom_to_list(Name)++"_"++integer_to_list(Arity)++"_func.txt", FName = SMod ++ "_"++atom_to_list(Name)++"_func.txt", - FADocs = unicode:characters_to_binary(shell_docs:render(Mod, Name, Arity, D, Opts)), - FDocs = unicode:characters_to_binary(shell_docs:render(Mod, Name, D, Opts)), + FADocs = fixup(unicode:characters_to_binary(shell_docs:render(Mod, Name, Arity, D, Opts))), + FDocs = fixup(unicode:characters_to_binary(shell_docs:render(Mod, Name, D, Opts))), case string:equal(FADocs,FDocs) of true -> Acc#{ sanitize(FAName) => FADocs }; @@ -545,11 +545,11 @@ render_module(Mod, #docs_v1{ docs = Docs } = D) -> ({{type,Name,Arity},_Anno,_Sig,_Doc,_Meta}, Acc) -> FName = SMod ++ "_"++atom_to_list(Name)++"_"++integer_to_list(Arity)++"_type.txt", Acc#{ sanitize(FName) => - unicode:characters_to_binary(shell_docs:render_type(Mod, Name, Arity, D, Opts))}; + fixup(unicode:characters_to_binary(shell_docs:render_type(Mod, Name, Arity, D, Opts)))}; ({{callback,Name,Arity},_Anno,_Sig,_Doc,_Meta}, Acc) -> FName = SMod ++ "_"++atom_to_list(Name)++"_"++integer_to_list(Arity)++"_cb.txt", Acc#{ sanitize(FName) => - unicode:characters_to_binary(shell_docs:render_callback(Mod, Name, Arity, D, Opts))} + fixup(unicode:characters_to_binary(shell_docs:render_callback(Mod, Name, Arity, D, Opts)))} end, Files, Docs); render_module(Mod, Datadir) -> {ok, [Docs]} = file:consult(filename:join(Datadir, atom_to_list(Mod) ++ ".docs_v1")), @@ -562,6 +562,13 @@ sanitize(FName) -> end, FName, [{"/","slash"},{":","colon"}, {"\\*","star"},{"<","lt"},{">","gt"},{"=","eq"}]). +fixup(Data) -> + Replacements = [{"\\Q\e(B\e[m\\E", "\e[0m"}, + {"\e\\[;+", "\e["}], + lists:foldl(fun({Replace, With}, D) -> + re:replace(D, Replace, With, [{return, binary}, unicode, global]) + end, Data, Replacements). + ansi(_Config) -> {ok, Docs} = code:get_doc(?MODULE), From 6a1403ac3640e4cedbc6b8ca69933a64f1c06bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 16:43:25 +0200 Subject: [PATCH 06/15] kernel: Introduce diagnostic codes --- lib/kernel/src/application.erl | 168 ++++++++++++++++++++++ lib/kernel/src/application_controller.erl | 75 +++++++--- lib/kernel/src/code.erl | 4 + lib/kernel/src/code_server.erl | 2 + make/doc.mk | 7 +- make/ex_doc.exs | 5 +- 6 files changed, 238 insertions(+), 23 deletions(-) diff --git a/lib/kernel/src/application.erl b/lib/kernel/src/application.erl index 02be8ae1d1ed..6d0c7989e59f 100644 --- a/lib/kernel/src/application.erl +++ b/lib/kernel/src/application.erl @@ -60,6 +60,8 @@ For details about applications and behaviours, see -export([get_env/1, get_env/2, get_env/3, get_all_env/0, get_all_env/1]). -export([get_key/1, get_key/2, get_all_key/0, get_all_key/1]). -export([get_application/0, get_application/1, get_supervisor/1, info/0]). +-export([get_diagnostic/1, get_diagnostic/2, explain/1, explain/2, + print_explain/1, print_explain/2]). -export([start_type/0]). -export_type([start_type/0, restart_type/0]). @@ -1074,6 +1076,172 @@ function returns `undefined`. start_type() -> application_controller:start_type(group_leader()). +-doc """ +Fetches the data associated with the `DiagnosticCode`. + +This function will search the `doc/diagnostics/` folders of all applications +in the code path looking for files with the [`rootname`](`filename:rootname/1`) of +the `DiagnosticCode` in ether short or long form. + +It will return all occurrences together with which application defined it, +the absolute filename of the file, the long and short diagnostic code +and the diagnostic file's contents. + +The `url` returned is composed by adding the [`documentation_url`](app.md#documentation_url) +application key with the `DiagnosticCode` in short form suffixed by `.html`. + +Example: + +``` +> application:get_diagnostic("ERL-0001"). +{ok, [#{application => compiler, + filename => "/home/erlang/lib/compiler/doc/diagnostics/ERL-0001-update-literal.md", + url => "https://erlang.org/doc/ERL-0001.html", + short => "ERL-0001", + long => "ERL-0001-update-literal", + doc => ~"# ERL-0001\n..."}]} +``` + +If no application defines the diagnostic code, then `{ok,[]}` is returned. + +""". +-spec get_diagnostic(DiagnosticCode :: unicode:chardata()) -> + {ok, [#{application := atom(), filename := file:name(), + url => uri_string:uri_string(), + short := string(), long := string(), + diagnostic => unicode:unicode_binary()}]}. +get_diagnostic(Code) -> + Apps = application_controller:available_applications(), + Diagnostics = + lists:flatmap(fun({App, Keys}) -> + %% TODO: Should this work for archives? + DiagnosticsDir = filename:join([code:lib_dir(App), "doc", "diagnostics"]), + case file:list_dir(DiagnosticsDir) of + {ok, Codes} -> + [read_diagnostic(filename:join(DiagnosticsDir, C), App, Keys) || C <- Codes, is_code_prefix(C, Code)]; + _ -> [] + end + end, Apps), + {ok, Diagnostics}. + +-doc """ +Equivalent to `get_diagnostic/1`, but only searches a specific application for +the `DiagnosticCode`. + +If the diagnostic is not found, but the application has a [`documentation_url`](app.md#documentation_url) +application key, then only the url will be returned. + +Example: + +``` +> application:get_diagnostic(compiler, "ERL-0001"). +{ok, #{application => compiler, + filename => "/home/erlang/lib/compiler/doc/diagnostics/ERL-0001-update-literal.md", + url => "https://erlang.org/doc/ERL-0001.html", + short => "ERL-0001", + long => "ERL-0001-update-literal", + doc => ~"# ERL-0001\n..."}} +> application:get_diagnostic(compiler, "ERL-7654-slogan"). +{ok, #{application => compiler, + url => "https://erlang.org/doc/ERL-7654.html", + short => "ERL-7654", + long => "ERL-7654-slogan" }} +``` + +""". +-spec get_diagnostic(Application :: atom(), DiagnosticCode :: unicode:chardata()) -> + {ok, #{application := atom(), filename := file:name(), + url => uri_string:uri_string(), + short := string(), long := string(), + diagnostic => unicode:unicode_binary()} | + #{application := atom(), + url := uri_string:uri_string(), + short => string(), + long => string() } + } | error. +get_diagnostic(App, Code) -> + ListCode = unicode:characters_to_list(Code), + DiagnosticsDir = filename:join([code:lib_dir(App), "doc", "diagnostics", unicode:characters_to_list(Code) ++ "*"]), + Keys = proplists:get_value(App, application_controller:available_applications()), + case filelib:wildcard(DiagnosticsDir) of + [File] -> + read_diagnostic(File, App, Keys); + [] -> + read_diagnostic(ListCode ++ ".md", App, Keys) + end. + +-doc false. +explain([DiagnosticCode]) -> + case get_diagnostic(DiagnosticCode) of + {ok, [#{ diagnostic := D }]} -> + io_lib:format("~n~ts~n",[shell_docs:render_markdown(D)]) + end. + +-doc false. +explain(App, [DiagnosticCode]) -> + case get_diagnostic(App, DiagnosticCode) of + {ok, #{ diagnostic := D }} -> + io_lib:format("~n~ts~n",[shell_docs:render_markdown(D)]) + end. + +-doc false. +print_explain(Arguments) -> + io:format("~ts",[explain(Arguments)]). + +-doc false. +print_explain(App, Arguments) -> + io:format("~ts",[explain(App, Arguments)]). + +is_code_prefix(Filename, Code) -> + {FNSpace, FNNumber, FNSlug, _} = split_code(Filename), + {CSpace, CNumber, CSlug, _} = split_code(Code), + + string:equal(FNSpace, CSpace) + andalso + (string:equal(FNNumber, CNumber) orelse + string:equal(FNSlug, CNumber) orelse + string:equal(FNNumber, CSlug)). + +split_code(Code) -> + [Namespace, NamespaceT] = string:split(Code, "-"), + [Number, NumberT] = + case string:split(NamespaceT, "-") of + [N, T] -> [N, T]; + [NamespaceT] -> + case string:split(NamespaceT, ".") of + [N, T] -> [N, "." ++ T]; + [NamespaceT] -> [NamespaceT, ""] + end + end, + case string:split(NumberT, ".") of + [Extension] -> {Namespace, Number, undefined, Extension}; + ["", Extension] -> {Namespace, Number, undefined, Extension}; + [Slug, Extension] -> {Namespace, Number, Slug, Extension} + end. + +read_diagnostic(Filename, App, AppKeys) -> + {Namespace, Number, Slug, _Ext} = split_code(filename:basename(Filename)), + Long = if Slug =:= undefined -> + #{}; + true -> + #{ long => Namespace ++ "-" ++ Number ++ "-" ++ Slug } + end, + Common = Long#{ short => Namespace ++ "-" ++ Number, application => App }, + case file:read_file(Filename) of + {ok, Diagnostic} -> + UrlMap = diagnostic_url(Namespace, Number, AppKeys), + maps:merge(Common#{ filename => Filename, diagnostic => Diagnostic}, UrlMap); + _ -> + UrlMap = diagnostic_url(Namespace, Number, AppKeys), + maps:merge(Common, UrlMap) + end. + +diagnostic_url(Namespace, Number, AppKeys) -> + case proplists:get_value(documentation_url, AppKeys) of + undefined -> #{}; + DocUrl -> #{ url => DocUrl ++ Namespace ++ "-" ++ Number ++ ".html" } + end. + %% Internal get_appl_name(Name) when is_atom(Name) -> Name; get_appl_name({application, Name, _}) when is_atom(Name) -> Name. diff --git a/lib/kernel/src/application_controller.erl b/lib/kernel/src/application_controller.erl index 5b94880eecb2..749ee5844e08 100644 --- a/lib/kernel/src/application_controller.erl +++ b/lib/kernel/src/application_controller.erl @@ -32,6 +32,7 @@ control_application/1, is_running/1, change_application_data/2, prep_config_change/0, config_change/1, which_applications/0, which_applications/1, + available_applications/0, available_applications/1, loaded_applications/0, info/0, set_env/2, get_pid_env/2, get_env/2, get_env/3, get_pid_all_env/1, get_all_env/1, get_pid_key/2, get_key/2, get_pid_all_key/1, get_all_key/1, @@ -164,7 +165,8 @@ %% Env = [{Key, Value}] %%----------------------------------------------------------------- --record(appl, {name, appl_data, descr, id, vsn, inc_apps, opt_apps, apps}). +-record(appl, {name, appl_data, descr, id, vsn, inc_apps, opt_apps, apps, + documentation_url}). %%----------------------------------------------------------------- %% Func: start/1 @@ -298,6 +300,12 @@ loaded_applications() -> [{{'$1', '$2', '$3'}}] }]). +available_applications() -> + call(available_applications). + +available_applications(Timeout) -> + call(available_applications, Timeout). + %% Returns some debug info info() -> call(info). @@ -390,6 +398,8 @@ get_key(AppName, optional_applications) -> get_key_direct(AppName, #appl{opt_apps = '$1', _ = '_'}); get_key(AppName, applications) -> get_key_direct(AppName, #appl{apps = '$1', _ = '_'}); +get_key(AppName, documentation_url) -> + get_key_direct(AppName, #appl{documentation_url = '$1', _ = '_'}); get_key(AppName, env) -> case ets:member(ac_tab, {loaded, AppName}) of true -> {ok, get_all_env(AppName)}; @@ -420,24 +430,27 @@ get_pid_all_key(Master) -> get_all_key(AppName) -> case ets:lookup(ac_tab, {loaded, AppName}) of [{_, Appl}] -> - {ok, [{description, Appl#appl.descr}, - {id, Appl#appl.id}, - {vsn, Appl#appl.vsn}, - {modules, (Appl#appl.appl_data)#appl_data.mods}, - {maxP, (Appl#appl.appl_data)#appl_data.maxP}, - {maxT, (Appl#appl.appl_data)#appl_data.maxT}, - {registered, (Appl#appl.appl_data)#appl_data.regs}, - {included_applications, Appl#appl.inc_apps}, - {optional_applications, Appl#appl.opt_apps}, - {applications, Appl#appl.apps}, - {env, get_all_env(AppName)}, - {mod, (Appl#appl.appl_data)#appl_data.mod}, - {start_phases, (Appl#appl.appl_data)#appl_data.phases} - ]}; + {ok, format_appl(Appl, get_all_env(AppName))}; _ -> undefined end. +format_appl(Appl, Env) -> + [{description, Appl#appl.descr}, + {id, Appl#appl.id}, + {vsn, Appl#appl.vsn}, + {modules, (Appl#appl.appl_data)#appl_data.mods}, + {documentation_url, Appl#appl.documentation_url}, + {maxP, (Appl#appl.appl_data)#appl_data.maxP}, + {maxT, (Appl#appl.appl_data)#appl_data.maxT}, + {registered, (Appl#appl.appl_data)#appl_data.regs}, + {included_applications, Appl#appl.inc_apps}, + {optional_applications, Appl#appl.opt_apps}, + {applications, Appl#appl.apps}, + {env, Env}, + {mod, (Appl#appl.appl_data)#appl_data.mod}, + {start_phases, (Appl#appl.appl_data)#appl_data.phases} + ]. start_type(Master) -> case ets:match(ac_tab, {{application_master, '$1'}, Master}) of @@ -687,6 +700,25 @@ handle_call({unload_application, AppName}, _From, S) -> end end; +handle_call(available_applications, _From, S) -> + Apps = + lists:foldl(fun(Name, Acc) -> + case make_appl(Name) of + {error, _} -> Acc; + {ok, {ApplData, ApplEnv, IncApps, OptApps, Descr, Id, Vsn, Apps, DocUrl}} -> + Name = ApplData#appl_data.name, + ConfEnv = get_env_i(Name, S), + NewEnv = merge_app_env(ApplEnv, ConfEnv), + CmdLineEnv = get_cmd_env(Name), + NewEnv2 = merge_app_env(NewEnv, CmdLineEnv), + Appl = #appl{name = Name, descr = Descr, id = Id, vsn = Vsn, apps = Apps, + appl_data = ApplData, inc_apps = IncApps, opt_apps = OptApps, + documentation_url = DocUrl}, + [{Name, format_appl(Appl, NewEnv2)} | Acc] + end + end, [], code:libs()), + {reply, Apps, S}; + handle_call({start_application, AppName, RestartType}, From, S) -> #state{running = Running, starting = Starting, start_p_false = SPF, started = Started, start_req = Start_req} = S, @@ -1324,7 +1356,7 @@ do_load_application(Application, S) -> %% Recursively load the application and its included apps. %load(S, {ApplData, ApplEnv, IncApps, Descr, Vsn, Apps}) -> -load(S, {ApplData, ApplEnv, IncApps, OptApps, Descr, Id, Vsn, Apps}) -> +load(S, {ApplData, ApplEnv, IncApps, OptApps, Descr, Id, Vsn, Apps, DocUrl}) -> Name = ApplData#appl_data.name, ConfEnv = get_env_i(Name, S), NewEnv = merge_app_env(ApplEnv, ConfEnv), @@ -1332,7 +1364,8 @@ load(S, {ApplData, ApplEnv, IncApps, OptApps, Descr, Id, Vsn, Apps}) -> NewEnv2 = merge_app_env(NewEnv, CmdLineEnv), add_env(Name, NewEnv2), Appl = #appl{name = Name, descr = Descr, id = Id, vsn = Vsn, apps = Apps, - appl_data = ApplData, inc_apps = IncApps, opt_apps = OptApps}, + appl_data = ApplData, inc_apps = IncApps, opt_apps = OptApps, + documentation_url = DocUrl}, ets:insert(ac_tab, {{loaded, Name}, Appl}), NewS = foldl(fun(App, S1) -> @@ -1555,9 +1588,10 @@ make_appl_i({application, Name, Opts}) when is_atom(Name), is_list(Opts) -> MaxT = get_opt(maxT, Opts, infinity), IncApps = get_opt(included_applications, Opts, []), OptApps = get_opt(optional_applications, Opts, []), + DocUrl = get_opt(documentation_url, Opts, undefined), {#appl_data{name = Name, regs = Regs, mod = Mod, phases = Phases, mods = Mods, maxP = MaxP, maxT = MaxT}, - Env, IncApps, OptApps, Descr, Id, Vsn, Apps}; + Env, IncApps, OptApps, Descr, Id, Vsn, Apps, DocUrl}; make_appl_i({application, Name, Opts}) when is_list(Opts) -> throw({error,{invalid_name,Name}}); make_appl_i({application, _Name, Opts}) -> @@ -1609,7 +1643,7 @@ is_loaded_app(AppName, [{application, AppName, App} | _]) -> is_loaded_app(AppName, [_ | T]) -> is_loaded_app(AppName, T); is_loaded_app(_AppName, []) -> false. -do_change_appl({ok, {ApplData, Env, IncApps, OptApps, Descr, Id, Vsn, Apps}}, +do_change_appl({ok, {ApplData, Env, IncApps, OptApps, Descr, Id, Vsn, Apps, DocUrl}}, OldAppl, Config) -> AppName = OldAppl#appl.name, @@ -1631,7 +1665,8 @@ do_change_appl({ok, {ApplData, Env, IncApps, OptApps, Descr, Id, Vsn, Apps}}, vsn=Vsn, inc_apps=IncApps, opt_apps=OptApps, - apps=Apps}; + apps=Apps, + documentation_url = DocUrl}; do_change_appl({error, _R} = Error, _Appl, _ConfData) -> throw(Error). diff --git a/lib/kernel/src/code.erl b/lib/kernel/src/code.erl index cd06a6da1196..06b18dbdf94f 100644 --- a/lib/kernel/src/code.erl +++ b/lib/kernel/src/code.erl @@ -374,6 +374,7 @@ common reasons. all_available/0, stop/0, root_dir/0, + libs/0, lib_dir/0, lib_dir/1, lib_dir/2, @@ -861,6 +862,9 @@ _Example:_ -spec root_dir() -> file:filename(). root_dir() -> call({dir,root_dir}). +-doc false. +libs() -> call({dir,libs}). + -doc """ Returns the library directory, `$OTPROOT/lib`, where `$OTPROOT` is the root directory of Erlang/OTP. diff --git a/lib/kernel/src/code_server.erl b/lib/kernel/src/code_server.erl index ebd78dc9708c..ff0e35847233 100644 --- a/lib/kernel/src/code_server.erl +++ b/lib/kernel/src/code_server.erl @@ -1022,6 +1022,8 @@ lookup_name(Name, Db) -> %% do_dir(Root,lib_dir,_) -> filename:append(Root, "lib"); +do_dir(_Root,libs,NameDb) -> + [list_to_atom(A) || {A, _D, _Base, _} <:- ets:tab2list(NameDb)]; do_dir(Root,root_dir,_) -> Root; do_dir(_Root,compiler_dir,NameDb) -> diff --git a/make/doc.mk b/make/doc.mk index 73f57fdc814f..9718ecd0e9c6 100644 --- a/make/doc.mk +++ b/make/doc.mk @@ -47,6 +47,11 @@ else EX_DOC_FORMATS= endif +# ---------------------------------------------------- +# Error-index dependencies +# ---------------------------------------------------- +ERROR_INDEX_FILES?=$(wildcard diagnostic-index/*.diagnosticmd) + # ---------------------------------------------------- # Man dependencies # ---------------------------------------------------- @@ -72,7 +77,7 @@ docs: $(DOC_TARGETS) chunks: -HTML_DEPS?=$(wildcard $(APP_EBIN_DIR)/*.beam) $(wildcard *.md) $(wildcard */*.md) $(wildcard assets/*) +HTML_DEPS?=$(ERROR_INDEX) $(wildcard $(APP_EBIN_DIR)/*.beam) $(wildcard *.md) $(wildcard */*.md) $(ERROR_INDEX_FILES) $(wildcard assets/*) $(HTMLDIR)/index.html: $(HTML_DEPS) docs.exs $(ERL_TOP)/make/ex_doc.exs $(gen_verbose)EX_DOC_WARNINGS_AS_ERRORS=$(EX_DOC_WARNINGS_AS_ERRORS) ERL_FLAGS="-pz $(ERL_TOP)/erts/ebin" \ diff --git a/make/ex_doc.exs b/make/ex_doc.exs index ca49433d1f61..cd391ac550a7 100644 --- a/make/ex_doc.exs +++ b/make/ex_doc.exs @@ -172,7 +172,7 @@ source_url_pattern = ## Merge the local config with the default extras = (Access.get(local_config, :extras, []) ++ - Path.wildcard("*.md") ++ Path.wildcard("{guides,references,internal_docs}/*.md")) + Path.wildcard("*.md") ++ Path.wildcard("{guides,references,internal_docs,diagnostics}/*.md")) |> Enum.uniq() annotations = Access.get(local_config, :annotations_for_docs, fn _ -> [] end) @@ -199,7 +199,8 @@ config = [ (Access.get(local_config, :groups_for_extras, []) ++ [ "User's Guides": ~r/guides/, - "Command Line Tools": ~r|references/.*_cmd.md$|, + "Command Line Tools": ~r|references/.*_cmd\.md$|, + "Diagnostic Index": ~r|diagnostics/.*\.md$|, References: ~r|references|, "Internal Docs": ~r/internal_doc/ ]) From 4939412d6eaf775123207aaff70b052a18153d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 16:51:13 +0200 Subject: [PATCH 07/15] stdlib: Add erlc -explain --- lib/stdlib/src/erl_compile.erl | 17 +++++++++++++++-- lib/stdlib/src/shell_docs.erl | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/stdlib/src/erl_compile.erl b/lib/stdlib/src/erl_compile.erl index a11e9a3fee09..6861e0eac6f3 100644 --- a/lib/stdlib/src/erl_compile.erl +++ b/lib/stdlib/src/erl_compile.erl @@ -76,6 +76,9 @@ compile_cmdline1(Args) -> case Result of ok -> halt(0); + {info, Output} -> + io:put_chars(standard_io, Output), + halt(0); {error, Output} -> io:put_chars(standard_error, Output), halt(1); @@ -100,6 +103,8 @@ do_compile(Args, Cwd) -> catch throw:{error, Output} -> {error, unicode:characters_to_binary(Output)}; + throw:{info, Output} -> + {info, unicode:characters_to_binary(Output)}; C:E:Stk -> {crash, {C,E,Stk}} end. @@ -203,6 +208,10 @@ parse_generic_option("describe-feature" ++ Str, T0, parse_generic_option("list-features", T, #options{specific = Spec} = Opts) -> compile1(T, Opts#options{specific =[{list_features, true}| Spec]}); +parse_generic_option("explain" ++ Str, T0, + #options{specific = Spec} = Opts) -> + {ExplainStr, T} = get_option("explain", Str, T0), + compile1(T, Opts#options{specific =[{explain, ExplainStr}| Spec]}); parse_generic_option(Option, _T, _Opts) -> usage(io_lib:format("Unknown option: -~ts\n", [Option])). @@ -270,6 +279,8 @@ usage(Error) -> "list short descriptions of available features (Erlang compiler)"}, {"-describe-feature ", "show long description of "}, + {"-explain ", + "print long explanation for "}, {"+term","pass the Erlang term unchanged to the compiler"}], Fmt = fun(K, D) when length(K) < 15 -> io_lib:format("~-14s ~s\n", [K, D]); @@ -299,7 +310,7 @@ split_at_equals([], Acc) -> compile2(Files, #options{cwd=Cwd,includes=Incl,outfile=Outfile}=Opts0) -> case show_info(Opts0) of {ok, Msg} -> - throw({error, Msg}); + throw({info, Msg}); false -> Opts = Opts0#options{includes=lists:reverse(Incl)}, case {Outfile,length(Files)} of @@ -351,7 +362,9 @@ show_info(#options{specific = Spec}) -> end end, - case G([list_features, describe_feature]) of + case G([list_features, describe_feature, explain]) of + {explain, DiagnosticCode} -> + {ok, application:explain(compiler, [DiagnosticCode])}; {list_features, true} -> Features = erl_features:configurable(), Msg = ["Available features:\n", diff --git a/lib/stdlib/src/shell_docs.erl b/lib/stdlib/src/shell_docs.erl index f74908d6f147..d88e07f224c0 100644 --- a/lib/stdlib/src/shell_docs.erl +++ b/lib/stdlib/src/shell_docs.erl @@ -68,6 +68,8 @@ be rendered as is. -export([render_type/2, render_type/3, render_type/4, render_type/5]). -export([render_callback/2, render_callback/3, render_callback/4, render_callback/5]). +-export([render_markdown/1]). + -export([test/2]). %% Used by chunks.escript in erl_docgen @@ -818,6 +820,10 @@ render_callback(_Module, Callback, Arity, #docs_v1{ } = D, Config) -> render_cb(_Module, Type, Arity, #docs_v1{ } = D, Config) -> renderer(Config, {_Module, callback, Type, Arity}, D). +-doc false. +render_markdown(MD) -> + HTML = shell_docs_markdown:parse_md(MD), + render_docs(HTML, init_config(#{}, #{ columns => 80, ansi => true })). %% Get the docs in the correct locale if it exists. -spec get_local_doc(atom() | tuple() | binary(), Docs, D) -> term() when From ab3fe7c14be02dc61745638ca46331877c8c4b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 16:52:38 +0200 Subject: [PATCH 08/15] erts: Add erl -explain --- erts/preloaded/ebin/init.beam | Bin 27912 -> 27900 bytes erts/preloaded/src/init.erl | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/erts/preloaded/ebin/init.beam b/erts/preloaded/ebin/init.beam index a64afe58c65423743dce5e8689950d54182ef369..7327c25548c4358e99ebc1ea238c8c5ca9b49f34 100644 GIT binary patch delta 6238 zcmZu#4SWwhGXIp;5)E=e0dOSd%g1zp{pk|fL0$)Ar&68YjtWDLzdTo@?q zDC{ikD*Uu?Q(<@Emcng?{e=e$cNFd_>?zz=*ju>2aJX=^aJ=wL;f2D>g*OXt72YYl zSNNdt^TMwRzb^c?@JZpT!Z(iW_?>bm=!|eqb5tkfjCRI36CK@|IcuDCPN%bep3~*@IDO7m=W6E~XPfgA=LToHv%}fx>~d~&b~`saw>UZH zPRDlca`rj*Is2W5oQIu9oX4Ceoadbr&K~D(C*xf1^g5U36nT^1{O-GNcjpJ>ii+05 zom(>-JG#2pcJyA7>D}zMyEpZ9^!5qPrykY0wXe6sljyvnvv*VH;f_pZW7owUeO*1> z?`L|uy8G4^aq=%JANs4Cde*_2)?4tUQuOCBN%DP=e{R%AGO5W8ju}fj^RJD0+DBUR z{o|jONjg7Ue^VxB<^1efGCBVu+fK;L{CP%-kgEK3(W_M zV1Dd5)kLXNOr?eTXp2m#UqCgVyPS}Q+#WNAcfpJgZ#Jolxqgk`o0A(CfV-%1rhkuE zP;O`AEdQN?%D>)t0!HSx&uM_Eug)=iq$dBVRZqy*xvBFsAD40q=ZzsFa~tMOo%?M# zpcs|0uM9Qzr4fq#$*78bVT_9X(Fn&rH>zWQFlu6-5%!t6ls*>LB1;F9R-4@@w3xKn zOoFK-UG2Y^R|~|`=P$wg;`wv31M#@cZiQ1uD!Y|5mDCp1r|wt7X0YBEV~j<2Zop8@ zh;Gt|sZvu{ZFbu*El{awO3U0pG^D2^3T#abQq^z9ejg|73)82XA)zudpqVziU7!o- zG*x6^*z68j8F9YSs_dt^cjwnM{i!q|E{7sEyK{&<1+2|%p%8PH+3YSfi_=uYjz&_k zZ`4MWs>#?lIb*@-k$Yrq#5&>m5Oo#bom;kGO7`OD5Fq?IAselm{wG;N0qDd{0AM(FW#(qaDG}BDTrXF%lyH`LChN#=fXtF=h z@C~f6B7}Wwvz_xOVc*$oudmV=rx8nfh!3Y4J%)EUV~p=tf-zrzAoeiTwPQR;!@C8~ zm=O2%D~bBH#D_c>-d7&u<>gEGu<}k$%X_)6d=n?cBCfn8hUN(u7At_m#-TxPGhMPvEm{ei22jF~N zA=Zc#Kb%HwHZ;sa&Z5UrllaOZVLJ z@X&Fe>w}QZ4nY5zabi`aco=6Oq3-3Q5?U~#Wa2Hlj|z_kBeVs^Oa$Pl3Pr@I%?9N- zuM#I0nafobPV!K$wRKds+GdXca}qFXDzvz%%O*uc>*h$Dzek6~1H6upg(z| ze1+n!ps4M4Gv z>#8~)NzBHhr4v$9lt-G{sF_tXGo4TpEeX9iGvWc9ndE1vFbP4>K&(kNJM60rDhjbC zr#KVQqSx_B>TEok>$Xr@mE^RlAOenH=L|0uOcu#b4Qq0!MTO^uA6Sx462UaZW=DOM zS;GLPk>XQb%s7v#B_B}SMpc6%m2hL|) za~6WjW=|qQrziQeHhS312B&zZYFhEUrM)gLhhy+x6qBZ>cmn>@)jB>ypN&UL>k_to zMgl1?ZmJ?+(z-DGsiNUH3ImEFv;WTu)jPZYC-w*Op@YX~g!-p^9cDd6Et}5Gu}tt1o4F!yI zpQ|_OR8R7Abj8%fJu1#0q|Z^zc(E%&$!5Yagt6J030Elv~ z2e(D}JZj7Ut+vU#8~A);%`0}Id;tZISo06bRVIQ)g!Tf^HhVds1ZE&^cr`zdSPN^! z`+S?df`uor@YWI1thgu^HAQ;#{VWYjHhUGJK3_Q8UFT~7Z8roL+3a=9NrGH#&JZDk z09s0{B{q8l{FzX(v?8tvCzxsoWdX64h50H3&`LTR&&h06ukw}Q_TXW@BG}5?jOlzi zv6jO}Rc1(Cg>-Dns8^7Y3y9d&$SO8_Q<#7~ZbMjAwNQwJRMd1!z5CTez~Zcw-f8)8u4Cdv%kUO&z6YQSs_WdJXM5iT*qrVH2y7ecEise;Mwe7WQkuUz8#Eo?nGPls9wNqu-UJG*o3&a z6!nG8ex3WcttR^+TpBJe+}dcfe}zm7#4p3TZT4@&q`(rSlN<2tK)H*J@4;ZV&Hi1; zX~gPD@eD;AqmppT2*igm7f!L@l>f*mZoGs<$*S>dy{$wU@Sjjx>1L>`h@~FjWV|GH zxy}9y$mfC6o8nuBA{+C(F#HWBFWt+xQq7nruGBc+MC)-E^1iS+gKrM@@Xf^9G?j0r zK4SHm#EoB)DAv(7`)}FgSGlEtDybb&enp!aP*8eqHcN$0FAmBTA|=zIoGnRyc`?;) zwb^e_0$W$w%)x3Gn8dm&#ea-zrfEqXzp`mI9y8sfpa%J$T<1zNdlj*Moa9%kO1mj? zT0$3et9qtc|8L8sZ7Wa#YhN0Kh_9Pz=Km(@${j~?Q1VB3$r1(x+p(u^i ztp|1toxc-vciHSiC|wHK8|(NkMrAFMoh)!6}t-AUt5R&)}yu z^^`v(j?sg`DBn-4{Ypj>UztXEBqTx>>11_T_Q4W&o$PU`i}8>+5)a!f2GyOg@<56| z;>J%MAH*&WqSP!!&;eK>BGipaQ3Vg8P(47bN0R(eVbG(e81$IU5_R0s7Gt zf1(JU2YCK_@H%t^BR&-VC<^3sNOMa&*|SDe5_#x5N~|Z6{7IoN`{XGKkJ)S*6t0KD zr&9dsqQc|AKQ0ssOOC^mbtnDxl_=D2&k?8iX&IK}G{g1`;nP`72^Mubi@+S8etOU$)Y?w3S$|RVc0QXPQ2kKy8eZ z`?+k!ZgGol+&F)fwb(wJeSy_yL|JR>II-Tb10oajK=R!|Rj(4KJs#u4de07YVIZRr>;3%8Yd@md z(fIGrhi5JRNS%z9;_-t{p(RDS4y}+8{06ionRAM?nA;3m$bXDx4qEXO^7GM_=JFx* zLXXb6Lg%AXQYL6jAj|M9FVZWDbV>25A@Ub_GA_==g5R<=vlIs3 z@jHtA`_TsQloqrG6b$g)j<(#TU3uXn*Dw(XK|DoyN%SF~mBxV>F8IY2tp+84a%fR1 z;pZt%y0qk5jCO=ed;AMP3(Iasdzwpo{FR^u&P`}fcWIB0&2#xTqE)0Y-a^5Gu09O- z?-5=6X5}|tazdUq?t0-jvR>Ze7k_tOxkc{k?&{kjZ|dvGY}kOI5!bKxZ5h_x)772X R+_xdeHXa~S?!Aq5{|{~jNC5x< delta 6334 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 diff --git a/erts/preloaded/src/init.erl b/erts/preloaded/src/init.erl index bed1570f00f9..8b5088aa8a8b 100644 --- a/erts/preloaded/src/init.erl +++ b/erts/preloaded/src/init.erl @@ -1674,10 +1674,15 @@ parse_boot_args([B|Bs], Ss, Fs, As) -> {S,Rest} = get_args(Bs, []), Instructions = run_args_to_start_instructions(S, fun bs2as/1), parse_boot_args(Rest, Instructions ++ Ss, Fs, As); - start_arg2 -> + run_arg -> {S,Rest} = get_args(Bs, []), Instructions = run_args_to_start_instructions(S, fun bs2ss/1), parse_boot_args(Rest, Instructions ++ Ss, Fs, As); + explain_arg -> + {S,Rest} = get_args(Bs, []), + Instructions = [{apply, application, print_explain, [S]}, + {apply, init, stop, []}], + parse_boot_args(Rest, Instructions ++ Ss, [{noinput, []} | Fs], As); ending_start_arg -> {S,Rest} = get_args(Bs, []), %% Forward any additional arguments to the function we are calling, @@ -1709,7 +1714,8 @@ parse_boot_args([], Start, Flags, Args) -> check(<<"-extra">>, _Bs) -> start_extra_arg; check(<<"-s">>, _Bs) -> start_arg; -check(<<"-run">>, _Bs) -> start_arg2; +check(<<"-run">>, _Bs) -> run_arg; +check(<<"-explain">>, _Bs) -> explain_arg; check(<<"-S">>, Bs) -> case has_end_args(Bs) of true -> @@ -1731,15 +1737,8 @@ has_end_args([]) -> get_args([B|Bs], As) -> case check(B, Bs) of - start_extra_arg -> {reverse(As), [B|Bs]}; - start_arg -> {reverse(As), [B|Bs]}; - start_arg2 -> {reverse(As), [B|Bs]}; - eval_arg -> {reverse(As), [B|Bs]}; - end_args -> {reverse(As), Bs}; - ending_start_arg -> {reverse(As), [B|Bs]}; - {flag,_} -> {reverse(As), [B|Bs]}; - arg -> - get_args(Bs, [B|As]) + arg -> get_args(Bs, [B|As]); + _ -> {reverse(As), [B|Bs]} end; get_args([], As) -> {reverse(As),[]}. From d26df52dad48eebfc0dfdc72ad4960a67cc56b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 16:53:17 +0200 Subject: [PATCH 09/15] stdlib: Add erl_diagnostic --- lib/stdlib/src/Makefile | 1 + lib/stdlib/src/erl_diagnostic.erl | 653 ++++++++++++++++++++++++++++++ lib/stdlib/src/uri_string.erl | 2 +- 3 files changed, 655 insertions(+), 1 deletion(-) create mode 100644 lib/stdlib/src/erl_diagnostic.erl diff --git a/lib/stdlib/src/Makefile b/lib/stdlib/src/Makefile index a9aa44dc265c..c17494fdadb5 100644 --- a/lib/stdlib/src/Makefile +++ b/lib/stdlib/src/Makefile @@ -69,6 +69,7 @@ MODULES= \ erl_bits \ erl_compile \ erl_error \ + erl_diagnostic \ erl_eval \ erl_expand_records \ erl_features \ diff --git a/lib/stdlib/src/erl_diagnostic.erl b/lib/stdlib/src/erl_diagnostic.erl new file mode 100644 index 000000000000..7d88fee4f7a8 --- /dev/null +++ b/lib/stdlib/src/erl_diagnostic.erl @@ -0,0 +1,653 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2025. All Rights Reserved. +%% Copyright 2020-2024 Facebook, Inc. and its affiliates. +%% +%% 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% +%% + +%% Notes: +%% * Sorting of warnings/errors in the compiler is a bit weird +%% - First they are partitioned into their types (i.e. error/warning) +%% - Then the `Type` in erl_scan, epp and erl_parse are printed sorted +%% by their filename + anno. +%% - Then any other `Type` is printed, sorted by their filename + +%% the order returned from the pass. +%% - The order returned from each pass varies depending on pass, but +%% erl_lint for example sorts errors by reversed insertion order and +%% warnings by location (aka line number). +%% +%% How do we want sorting? Ideally we would like it sorted on order of +%% relevance, I assume that is why erl_scan, epp, erl_parse are printed +%% first as any error there is probably what you want to fix first. +%% +%% Many other compilers seem to mix errors and warnings and they are emitted +%% in pass order + insertion order (which is usually encounter order). +-module(erl_diagnostic). +-moduledoc """ +This module provides infrastructure for collecting and reporting diagnostics +such as errors, warnings, informational messages, and hints during compilation +or code analysis. + +It supports: +- Declarative definition of diagnostics with custom formatting functions. +- Tracking diagnostic states across different types (error, warning, info, hint). +- Emission and management of diagnostics including enabling/disabling, + overrides, and legacy formats. +- Rich formatting output including plain text, ANSI-colored terminal output, + and JSON (with optional links to documentation). +- Utilities for attaching related source locations, underlining source ranges, + and emitting helpful suggestions or notes. + +Diagnostics can be reported through various APIs, including a modern map-based +system (`emit/4`, `emit/5`, etc.) and a legacy format (`emit_legacy/3`). + +This system is designed to be extensible, efficient, and suitable for both +machine-readable outputs (e.g., for editors or LSPs) and human-friendly +terminal displays. +""". +-moduledoc(#{since => "OTP 29.0"}). + +-export([init/1, emit/3, emit/4, emit/5, emit/6, emit_legacy/3, emit_legacy/5, + get_warnings/1, get_errors/1, has_errors/1, has_warnings/1, report/4]). +-export([add_diagnostics/2, get_diagnostic/2, push_diagnostic/4, pop_diagnostic/2, get_value/2, + dump/1]). + +-export([format_error/1]). + +-export_type([error_info/0, error_listing/0, error_description/0]). + +-type error_description() :: term(). +-type error_info() :: {erl_anno:location(), module(), error_description()}. +-type error_listing() :: [{file:filename(), [error_info()]}]. + +-type code() :: unicode:unicode_binary(). + +-type key() :: term(). +-type types() :: error | warning | info | hint. + +-type diagnostic() :: + #{ key := key(), + type := types(), + code => code(), + off => unicode:chardata(), + default => term(), + type => boolean | value | set, + format => function() + }. + +-type diagnostic_int() :: { + #{ diagnostic => diagnostic(), + value => [dynamic()] } +}. + +-type entry() :: {diagnostic_int(), erl_anno:anno(), Arg :: term()}. + +-type options() :: #{ }. + +-export_type([state/0]). +-opaque state() :: + #{ diagnostics := #{ term() => diagnostic_int() }, + warnings_as_errors := boolean(), + warning := [entry()], + error := [entry()], + info := [entry()], + hint := [entry()] + }. +% -nominal state() :: {?MODULE, state()}. + +-doc """ +Initialize a new diagnostic context +""". +-spec init(Opts :: options()) -> state(). +init(Opts) -> + #{ warnings_as_errors => maps:get(warnings_as_errors, Opts, false), + diagnostics => #{}, + warning => [], + error => [], + info => [], + hint => [] }. + +-doc """ +Add new diagnostics into the context. +""". +-spec add_diagnostics(state(), [diagnostic()]) -> state(). +add_diagnostics(S = #{ diagnostics := Ds }, Diagnostics) -> + NewDs = lists:foldl( + fun F(#{ type := warning } = D, Acc) when map_get(warnings_as_errors, S) -> + F(D#{ type := error }, Acc); + F(#{ key := Key } = D, Acc) -> + maps:is_key(Key, Acc) andalso error({duplicate_key, D}), + Acc#{ Key => #{ diagnostic => D, + value => [{maps:get(default, D, true), default}] } } + end, Ds, Diagnostics), + S#{ diagnostics := NewDs }. + +-doc """ +Push a new value for a diagnostic. +""". +-spec push_diagnostic(state(), Key :: key(), Value :: term(), Anno :: erl_anno:anno()) -> + state(). +push_diagnostic(#{ diagnostics := Ds } = S, Key, Value, Anno) -> + D = maps:get(Key, Ds), + S#{ diagnostics := Ds#{ Key := D#{ value := [{Value, Anno} | maps:get(value, D)]}}}. + +-doc """ +Pop a previous value for a diagnostic. +""". +-spec pop_diagnostic(state(), key()) -> + {ok, Value :: term(), state()}. +pop_diagnostic(S, _Key) -> + {ok, undefined, S}. + +-doc """ +Get a diagnostic. +""". +-spec get_diagnostic(state(), key()) -> {ok, diagnostic()} | undefined. +get_diagnostic(#{ diagnostics := Ds }, Key) -> + case maps:find(Key, Ds) of + error -> undefined; + {ok, _} = Ok -> Ok + end. + +-doc """ +Get the value of a diagnostic. +""". +-spec get_value(state(), Key :: key()) -> term(). +get_value(S, Key) -> + {ok, D} = get_diagnostic(S, Key), + element(1,hd(maps:get(value, D))). + +-doc #{ equiv => emit(S, Key, Anno, [], #{}) }. +-spec emit(state(), key(), erl_anno:anno()) -> + state(). +emit(S, Key, Anno) -> + emit(S, Key, Anno, []). + +-doc #{ equiv => emit(S, Key, Anno, Args, #{}) }. +-spec emit(state(), key(), erl_anno:anno(), [term()]) -> + state(). +emit(S, Key, Anno, Args) -> + emit(S, Key, Anno, Args, #{}). + +-doc """ +Emit a diagnostic entry into the collection. +""". +-spec emit(state(), key(), erl_anno:anno(), [term()], #{ related => [{erl_anno:anno(), unicode:chardata()}]}) -> state(). +emit(S, Key, Anno, Args, Opts) -> + try get_diagnostic(S, Key) of + {ok, #{ diagnostic := #{ type := Type, format := Fmt } } = D} + when is_function(Fmt, length(Args)) -> + case get_value(S, Key) of + true -> + S#{ Type := [{D, Anno, Args, Opts} | maps:get(Type, S)]}; + false -> S + end; + undefined -> + erlang:error({badarg,[S, Key, Anno, Args]}) + catch error:{bad_key, _} -> + erlang:error({badarg,[S, Key, Anno, Args]}) + end. + +-doc """ +Emit a diagnostic entry into the collection and create the diagnostic `D` +if it does not exist. +""". +-spec emit(state(), key(), erl_anno:anno(), [term()], #{}, diagnostic()) -> + state(). +emit(S, Key, Anno, Args, Opts, D) -> + DiagState = + case get_diagnostic(S, Key) of + undefined -> erl_diagnostic:add_diagnostics(S, [D]); + _ -> S + end, + erl_diagnostic:emit( + DiagState, + Key, Anno, Args, Opts). + +%% Emit a legacy diagnostic entry into the collection +%% This is a private API only used by the compiler +-doc """ +Emits a diagnostic using the legacy + +This function takes +""". +-spec emit_legacy(state(), types(), error_listing()) -> state(). +emit_legacy(S, Type, [{File, [{Location, Mod, Reason}|T]}|R]) -> + AnnoNoFile = case Location of + none -> erl_anno:new(0); + Location -> erl_anno:new(Location) + end, + + Anno = erl_anno:set_file(File, AnnoNoFile), + + emit_legacy(emit_legacy(S, Type, Anno, Mod, Reason), Type, [{File, T} | R]); +emit_legacy(S, Type, [{_File, []}|R]) -> + emit_legacy(S, Type, R); +emit_legacy(S, _Type, []) -> + S. + +-spec emit_legacy(state(), types(), erl_anno:anno(), module(), term()) -> state(). +emit_legacy(S, Type, Anno, Mod, Reason) -> + Code = case erlang:function_exported(Mod, error_code, 1) of + true -> case Mod:error_code(Reason) of undefined -> #{}; C -> #{ code => C } end; + false -> #{} + end, + + erl_anno:file(Anno) =/= undefined orelse error({badarg, "anno has no file set"}), + + D = #{ diagnostic => Code#{ key => {Mod, Reason}, type => Type, + format => fun(A) -> Mod:format_error(A) end} }, + S#{ Type := [{D, Anno, [Reason], #{ legacy => Mod }} | maps:get(Type, S)]}. + +-doc """ +Get all emitted warnings in legacy format. +""". +-spec get_warnings(state()) -> [term()]. +get_warnings(#{ warning := Ws }) -> + pack_diagnostics(Ws). + +-doc """ +Get all emitted warnings in legacy format. +""". +-spec get_errors(state()) -> [term()]. +get_errors(#{ error := Es }) -> + pack_diagnostics(Es). + +pack_diagnostics(Ds) -> + [{File, lists:map(fun({_D, Anno, [Tag], #{ legacy := Mod }}) -> + Location = case erl_anno:location(Anno) of + 0 -> none; + L -> L + end, + {Location, Mod, Tag}; + ({D, Anno, Arg, Opts}) -> + {erl_anno:location(Anno), erl_diagnostic, {D, Arg, get_help(D, Opts)}} + end, FileDs)} || {File, FileDs} <- group_diagnostics(Ds)]. + +group_diagnostics(Ds) -> + Files = lists:usort([erl_anno:file(Anno) || {_, Anno,_, _} <- Ds]), + [{File, lists:sort(fun({_DA, AnnoA, [TagA], #{ legacy := ModA }}, + {_DB, AnnoB, [TagB], #{ legacy := ModB }}) -> + case erl_anno:location(AnnoA) == erl_anno:location(AnnoB) of + true -> {ModA, TagA} =< {ModB, TagB}; + false -> erl_anno:location(AnnoA) < erl_anno:location(AnnoB) + end; + ({_, AnnoA, _, _}, {_, AnnoB, _, _}) -> + erl_anno:location(AnnoA) =< erl_anno:location(AnnoB) + end, + [{D, Anno, Arg, Opts} + || {D, Anno, Arg, Opts} <- lists:reverse(Ds), + erl_anno:file(Anno) =:= File])} + || File <- Files]. + +-doc """ +Return if any errors have been emitted +""". +-spec has_errors(state()) -> boolean(). +has_errors(#{ error := Es }) -> Es =/= []. + +-doc """ +Return if any warnings have been emitted +""". +-spec has_warnings(state()) -> boolean(). +has_warnings(#{ warning := Ws }) -> Ws =/= []. + +-doc """ +Dump the diagnostic state to stdout +""". +-spec dump(state()) -> ok. +dump(#{ diagnostics := D }) -> + io:format("Diagnostics:\n"), + [io:format(" ~p: ~p ~p~n",[Key, Type, Values]) + || Key := #{ diagnostic := #{ type := Type }, value := Values} <:- maps:iterator(D, ordered)], + ok. + +-doc """ +Format all entries into the given format. +""". +-spec report(io:device(), state(), json | ansi | txt, #{ types => [types()], + filename => base | relative | full }) -> ok. +report(Dev, S, Format, Opts) -> + [report_type(Dev, group_diagnostics(maps:get(T, S, [])), Format, Opts) + || T <- maps:get(types, Opts, [error, warning, info, hint])], + ok. + +report_type(Dev, Files, ansi, Opts) -> + [format_messages(Dev, File, Ds, Opts) || {File, Ds} <- Files]; +report_type(Dev, Files, json, _Opts) -> + io:format(Dev, "~ts", [json:format(format_json(Files))]). + +%% TODO: gcc-13 added support for outputting in sarif format. Should we use that instead of LSP? +format_json([{File, [{#{ diagnostic := D } = DI, Anno, Arg, _Opts} | T]} | Files]) -> + Code = case application:get_diagnostic(maps:get(code, D, "")) of + {ok, [#{ url := Url }]} -> + #{ ~"doc_uri" => unicode:characters_to_binary(Url), ~"code" => maps:get(code, D)}; + _ -> #{} + end, + [Code#{ ~"uri" => unicode:characters_to_binary("file://" ++ filename:absname(File)), + ~"range" => #{ ~"start" => #{ ~"line" => erl_anno:line(Anno), + ~"character" => erl_anno:column(Anno) } }, + ~"severity" => atom_to_binary(maps:get(type, D)), + ~"message" => unicode:characters_to_binary(format_error({DI, Arg, []})) + } | + format_json([{File, T} | Files])]; +format_json([{_File, []} | Files]) -> + format_json(Files); +format_json([]) -> + []. + +-doc false. +format_error({#{ diagnostic := #{ format := Fmt } }, FmtArgs, _Help}) -> + string:trim(case apply(Fmt, FmtArgs) of + {Format, Args} when is_list(Args) -> + io_lib:format(Format, Args); + Bin -> + unicode:characters_to_list(Bin) + end). + +get_help(D, Opts) -> + lists:flatten([get_code_note(D, Opts), + get_warning_help(D), + get_suggestion_help(D, Opts)]). + +get_code_note(#{ diagnostic := #{ code := Code }}, Opts) when is_binary(Code) -> + case lists:member(Code, maps:get(ignore_code, Opts, [])) of + false -> + {"help", io_lib:format("call `erlc -explain ~ts` to see a detailed explanation", [Code])}; + true -> + [] + end; +get_code_note(_, _) -> + []. + +get_warning_help(#{ diagnostic := #{ type := warning, off := Off, on := On } } = D) -> + {"note", io_lib:format("`~ts` was enabled ~ts; \n\tuse `~ts` to turn off this warning", [On, get_enabler(D), Off])}; +get_warning_help(#{ diagnostic := #{ type := warning, off := Off } } = D) -> + {"note", io_lib:format("was enabled ~ts; use `~ts` to turn off this warning", [get_enabler(D), Off])}; +get_warning_help(_) -> + []. + +get_enabler(#{ value := [{_, Tool}|_]}) when is_atom(Tool) -> + io_lib:format("by ~p", [Tool]); +get_enabler(#{ value := [{_, Anno}|_]}) -> + io_lib:format("at ~s:~p:~p",[erl_anno:file(Anno), + erl_anno:line(Anno), erl_anno:column(Anno)]). + +get_suggestion_help(_D, #{ possible := {Name, PossibleNames} } = _O) -> + case PossibleNames of + [] -> []; + _ -> + %% kk and kl has a similarity of 0.66. Short names are common in + %% Erlang programs, therefore we choose a relatively low threshold + %% here. + SufficientlySimilar = 0.66, + NameString = to_string(Name), + Similarities = [{string:jaro_similarity(NameString, to_string(F)), to_string(F)} || + F <- PossibleNames], + {MaxSim, GuessName} = lists:last(lists:sort(Similarities)), + case MaxSim > SufficientlySimilar of + true -> {"help", io_lib:format("did you mean ~ts?",[GuessName])}; + false -> [] + end + end; +get_suggestion_help(_D, _O) -> + []. + +to_string(N) -> + io_lib:format("~p",[N]). + +% -spec format_messages(File::string(), [erl_lint:error_info()], Opts::[term()]) -> [message()]. +format_messages(Dev, F, [{D, none, Args, DOpts} | Es], Opts) -> + Brief = maps:get(brief, Opts, false), + Type = maps:get(type, maps:get(diagnostic, D)), + io:format(Dev, "~ts: ~s~ts\n~ts~s", + [format_filename(F, Opts), io_lib:format("~p: ", [Type]), + format_error({D, Args, []}), + [io_lib:format("\n% ~ts: ~ts",[Cat, Msg]) || + {Cat, Msg} <- get_help(D, maps:merge(DOpts, Opts)), + not Brief], + ["\n\n" || not Brief] + ]), + format_messages(Dev, F, Es, ignore_code(D, Opts)); +format_messages(Dev, F, [{D, Anno, Args, DOpts} | Es], Opts) -> + Brief = maps:get(brief, Opts, false), + StartLoc = erl_anno:location(Anno), + EndLoc = StartLoc, + Src = [string:trim(quote_source(F, StartLoc, EndLoc)) || not Brief], + io:format(Dev, "~ts:~ts: ~s~ts~ts\n~ts~ts~ts~s", + [ + format_filename(F, Opts), + fmt_pos(StartLoc), + fmt_type(D, Opts), + format_error({D, Args, []}), + [get_code(D) || not Brief], + Src, + [format_related(maps:get(related, DOpts, []), F, Opts) || not Brief], + [io_ansi:format(["\n% ",help_color(Cat),"~ts: ~ts", io_ansi:reset()],[Cat, Msg]) || + {Cat, Msg} <- get_help(D, maps:merge(DOpts, Opts)), not Brief], + ["\n\n" || not Brief] + ]), + format_messages(Dev, F, Es, ignore_code(D, Opts)); +format_messages(_, _, [], _Opts) -> + []. + +format_related([], _, _) -> + []; +format_related([{Anno, Slogan}|R], Filename, Opts) -> + case erl_anno:file(Anno) =/= undefined andalso string:equal(erl_anno:file(Anno), Filename) of + true -> + StartLoc = erl_anno:location(Anno), + EndLoc = erl_anno:location(Anno), + ["\n\n",indent(2, [format_filename(Filename, Opts),":", + fmt_pos(StartLoc),": ", Slogan,"\n", + string:trim(quote_source(Filename, StartLoc, EndLoc))]) + | format_related(R, Filename, Opts)]; + false -> + format_related(R, Filename, Opts) + end. + +indent(Num, String) -> + Lines = string:split(String, "\n", all), + lists:join($\n, [[lists:duplicate(Num, $ ), Line] || Line <- Lines]). + +format_filename(Filename, #{ filename := base}) -> + filename:basename(Filename); +format_filename(Filename, #{ filename := full}) -> + filename:absname(Filename); +format_filename(Filename, _) -> + {ok, Cwd} = file:get_cwd(), + case string:prefix(filename:absname(Filename), Cwd ++ "/") of + nomatch -> Filename; + RelativeFilename -> RelativeFilename + end. + +ignore_code(#{ diagnostic := #{ code := Code }}, Opts) -> + IgnoreCore = maps:get(ignore_code, Opts, []), + Opts#{ ignore_code => [Code | IgnoreCore]}; +ignore_code(_, Opts) -> + Opts. + +help_color("note") -> io_ansi:cyan(); +help_color(_) -> io_ansi:light_black(). + +get_code(#{ diagnostic := #{ code := Code } }) -> + case application:get_diagnostic(Code) of + {ok, [#{url := Url}]} -> + io_ansi:format([" [",io_ansi:hyperlink_start(Url),"~ts",io_ansi:hyperlink_reset(),"]"], [Code]); + _ -> + io_lib:format(" [~ts]", [Code]) + end; +get_code(_) -> "". + +fmt_type(#{ diagnostic := #{ type := T }} = _D, _Opts) -> + Color = case T of + error -> red; + warning -> magenta; + info -> light_blue; + hint -> light_black + end, + io_ansi:format([Color, "~p: ", io_ansi:reset()], [T]). + +fmt_pos({Line, Col}) -> + io_lib:format("~w:~w", [Line, Col]); +fmt_pos(Line) -> + io_lib:format("~w", [Line]). + +quote_source(File, Line, Loc2) when is_integer(Line) -> + quote_source(File, {Line, 1}, Loc2); +quote_source(File, Loc1, Line) when is_integer(Line) -> + quote_source(File, Loc1, {Line, -1}); +quote_source(File, {StartLine, StartCol}, {EndLine, EndCol}) -> + case file:read_file(File) of + {ok, Bin} -> + Enc = case epp:read_encoding_from_binary(Bin) of + none -> epp:default_encoding(); + Enc0 -> Enc0 + end, + Ctx = + if + StartLine =:= EndLine -> 0; + true -> 1 + end, + case seek_line(Bin, 1, StartLine - Ctx) of + {ok, Bin1} -> + quote_source_1(Bin1, Enc, StartLine, StartCol, EndLine, EndCol, Ctx); + error -> + "" + end; + {error, _} -> + "" + end. + +quote_source_1(Bin, Enc, StartLine, StartCol, EndLine, EndCol, Ctx) -> + case take_lines(Bin, Enc, StartLine - Ctx, EndLine + Ctx) of + [] -> + ""; + Lines -> + Lines1 = + case length(Lines) =< (4 + Ctx) of + true -> + Lines; + false -> + %% before = context + start line + following line + %% after = end line + context + %% (total lines: 3 + 1 + context) + Before = lists:sublist(Lines, 2 + Ctx), + After = lists:reverse( + lists:sublist(lists:reverse(Lines), 1 + Ctx) + ), + Before ++ [{0, "..."}] ++ After + end, + Lines2 = decorate(Lines1, StartLine, StartCol, EndLine, EndCol), + [[fmt_line(L, Text) || {L, Text} <:- Lines2], $\n] + end. + +line_prefix() -> + "% ". + +fmt_line(L, Text) -> + {LineText, LineTextLength} = line_to_txt(L), + [line_prefix(), + io_lib:format("~*.ts| ", [LineTextLength, LineText]), + Text, "\n"]. + +line_to_txt(L) -> + LineText = integer_to_list(abs(L)), + Length = max(4, length(LineText)), + if + L < 0 -> + {"", Length}; + true -> + {LineText, Length} + end. + +decorate([{Line, Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) when + Line =:= StartLine, EndLine =:= StartLine -> + %% start and end on same line + S = underline(Text, StartCol, EndCol), + decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol); +decorate([{Line, Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) when Line =:= StartLine -> + %% start with end on separate line + S = underline(Text, StartCol, string:length(Text) + 1), + decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol); +decorate([{_Line, _Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) -> + [L | decorate(Ls, StartLine, StartCol, EndLine, EndCol)]; +decorate([], _StartLine, _StartCol, _EndLine, _EndCol) -> + []. + +%% don't produce empty decoration lines +decorate("", L, Ls, StartLine, StartCol, EndLine, EndCol) -> + [L | decorate(Ls, StartLine, StartCol, EndLine, EndCol)]; +decorate(Text, {Line, _} = L, Ls, StartLine, StartCol, EndLine, EndCol) -> + [L, {-Line, Text} | decorate(Ls, StartLine, StartCol, EndLine, EndCol)]. + +%% End typically points to the first position after the actual region. +%% If End = Start, we adjust it to Start+1 to mark at least one character +%% TODO: colorization option +underline(_Text, Start, End) when End < Start -> + % no underlining at all if end column is unknown + ""; +underline(Text, Start, Start) -> + underline(Text, Start, Start + 1); +underline(Text, Start, End) -> + underline(Text, Start, End, 1). + +underline([$\t | Text], Start, End, N) when N < Start -> + [$\t | underline(Text, Start, End, N + 1)]; +underline([_ | Text], Start, End, N) when N < Start -> + [$\s | underline(Text, Start, End, N + 1)]; +underline(_Text, _Start, End, N) -> + underline_1(N, End). + +underline_1(N, End) when N < End -> + [$^ | underline_1(N + 1, End)]; +underline_1(_N, _End) -> + "". + +seek_line(Bin, L, L) -> {ok, Bin}; +seek_line(<<$\n, Rest/binary>>, N, L) -> seek_line(Rest, N + 1, L); +seek_line(<<$\r, $\n, Rest/binary>>, N, L) -> seek_line(Rest, N + 1, L); +seek_line(<<_, Rest/binary>>, N, L) -> seek_line(Rest, N, L); +seek_line(<<>>, _, _) -> error. + +take_lines(<<>>, _Enc, _Here, _To) -> + []; +take_lines(Bin, Enc, Here, To) when Here =< To -> + {Text, Rest} = take_line(Bin, <<>>), + [{Here, text_to_string(Text, Enc)} + | take_lines(Rest, Enc, Here + 1, To)]; +take_lines(_Bin, _Enc, _Here, _To) -> + []. + +text_to_string(Text, Enc) -> + case unicode:characters_to_list(Text, Enc) of + String when is_list(String) -> String; + {error, String, _Rest} -> String; + {incomplete, String, _Rest} -> String + end. + +take_line(<<$\n, Rest/binary>>, Ack) -> + {Ack, Rest}; +take_line(<<$\r, $\n, Rest/binary>>, Ack) -> + {Ack, Rest}; +take_line(<>, Ack) -> + take_line(Rest, <>); +take_line(<<>>, Ack) -> + {Ack, <<>>}. + + +% dbg(T) -> io:format("~p~n",[T]), T. \ No newline at end of file diff --git a/lib/stdlib/src/uri_string.erl b/lib/stdlib/src/uri_string.erl index 780ae4942f02..588b8b188bdf 100644 --- a/lib/stdlib/src/uri_string.erl +++ b/lib/stdlib/src/uri_string.erl @@ -388,7 +388,7 @@ representing an [RFC 3986](https://www.ietf.org/rfc/rfc3986.txt) compliant URI set: the letters of the basic Latin alphabet, digits, and a few special characters. """. --type uri_string() :: iodata(). +-type uri_string() :: unicode:chardata(). -doc """ Error tuple indicating the type of error. Possible values of the second component: From 93027bec4ed6390ac11bd4d27331b930878bf288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 17:02:37 +0200 Subject: [PATCH 10/15] compiler: Start using erl_diagnostic to emit warnings --- lib/compiler/src/Makefile | 1 - lib/compiler/src/compile.erl | 228 ++++++++++++++---------------- lib/compiler/src/compiler.app.src | 3 +- lib/compiler/src/sys_messages.erl | 218 ---------------------------- lib/compiler/test/error_SUITE.erl | 8 +- 5 files changed, 108 insertions(+), 350 deletions(-) delete mode 100644 lib/compiler/src/sys_messages.erl diff --git a/lib/compiler/src/Makefile b/lib/compiler/src/Makefile index 00d179222735..1f711e38bff7 100644 --- a/lib/compiler/src/Makefile +++ b/lib/compiler/src/Makefile @@ -105,7 +105,6 @@ MODULES = \ sys_core_inline \ sys_core_prepare \ sys_coverage \ - sys_messages \ sys_pre_attributes \ v3_core diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index eedd993f84c1..65d890bfcce4 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -180,7 +180,7 @@ See `m:erl_id_trans` for an example and an explanation of the function -include("core_parse.hrl"). -import(lists, [member/2,reverse/1,reverse/2,keyfind/3,last/1, - map/2,flatmap/2,flatten/1,foreach/2,foldr/3,any/2]). + map/2,flatten/1,foreach/2,foldr/3,any/2]). -define(PASS_TIMES, compile__pass_times). -define(SUB_PASS_TIMES, compile__sub_pass_times). @@ -198,10 +198,8 @@ List of Erlang abstract or Core Erlang format representations, as used by -doc "See `file/2` for detailed description.". -type option() :: atom() | {atom(), term()} | {'d', atom(), term()}. --type error_description() :: erl_lint:error_description(). --type error_info() :: erl_lint:error_info(). --type errors() :: [{file:filename(), [error_info()]}]. --type warnings() :: [{file:filename(), [error_info()]}]. +-type errors() :: erl_diagnostic:error_listing(). +-type warnings() :: erl_diagnostic:error_listing(). -type mod_ret() :: {'ok', module()} | {'ok', module(), cerl:c_module()} %% with option 'to_core' | {'ok', %% with option 'to_pp' @@ -1109,7 +1107,7 @@ the error. This function is usually called implicitly when an `ErrorInfo` structure is processed. """. --spec format_error(ErrorDescription :: error_description()) -> string(). +-spec format_error(ErrorDescription :: erl_diagnostic:error_description()) -> string(). format_error({obsolete_option,Ver}) -> io_lib:fwrite("the ~p option is no longer supported", [Ver]); @@ -1172,8 +1170,7 @@ format_error_reason(Class, Reason, Stack) -> options=[] :: [option()], %Options for compilation mod_options=[] :: [option()], %Options for module_info encoding=none :: none | epp:source_encoding(), - errors=[] :: errors(), - warnings=[] :: warnings(), + diagnostics :: erl_diagnostic:state() | undefined, extra_chunks=[] :: [{binary(), binary()}]}). internal({forms,Forms}, Opts0) -> @@ -1196,7 +1193,8 @@ internal({file,File}, Opts) -> build_compile(Opts0) -> ExtraChunks = proplists:get_value(extra_chunks, Opts0, []), Opts1 = proplists:delete(extra_chunks, Opts0), - #compile{options=Opts1,mod_options=Opts1,extra_chunks=ExtraChunks}. + #compile{options=Opts1,mod_options=Opts1,extra_chunks=ExtraChunks, + diagnostics=erl_diagnostic:init(#{ warnings_as_errors => member(warnings_as_errors, Opts1) })}. internal_comp(Passes, Code0, File, Suffix, St0) -> Dir = filename:dirname(File), @@ -1243,8 +1241,7 @@ fold_comp([{Name,Pass}|Ps], Run, Code0, St0) -> Error catch error:Reason:Stk -> - Es = [{St0#compile.ifile,[{none,?MODULE,{crash,Name,Reason,Stk}}]}], - {error,St0#compile{errors=St0#compile.errors ++ Es}} + {error,emit_error(St0, none, ?MODULE, {crash,Name,Reason,Stk})} end; fold_comp([], _Run, Code, St) -> {ok,Code,St}. @@ -1365,8 +1362,24 @@ run_tprof({Name,Fun}, Code, Name, Measurement, St) -> run_tprof({_,Fun}, Code, _, _, St) -> Fun(Code, St). -comp_ret_ok(Code, #compile{warnings=Warn0,module=Mod,options=Opts}=St) -> - Warn1 = filter_warnings(Warn0, Opts), +emit_errors(St, Errors) -> + St#compile{ diagnostics = erl_diagnostic:emit_legacy( + St#compile.diagnostics, error, Errors)}. +emit_error(St, Location, Mod, Reason) -> + emit_error(St, St#compile.ifile, Location, Mod, Reason). +emit_error(St, File, none, Mod, Reason) -> + emit_error(St, File, 0, Mod, Reason); +emit_error(St, File, Location, Mod, Reason) -> + Anno = erl_anno:set_file(File, erl_anno:new(Location)), + St#compile{ diagnostics = erl_diagnostic:emit_legacy( + St#compile.diagnostics, error, Anno, Mod, Reason)}. + +emit_warnings(St, Warnings) -> + Filtered = filter_warnings(Warnings, St#compile.options), + St#compile{ diagnostics = erl_diagnostic:emit_legacy( + St#compile.diagnostics, warning, Filtered)}. + +comp_ret_ok(Code, #compile{module=Mod,diagnostics=Ds,options=Opts}=St) when is_map(Ds) -> case werror(St) of true -> case member(report_warnings, Opts) of @@ -1378,52 +1391,44 @@ comp_ret_ok(Code, #compile{warnings=Warn0,module=Mod,options=Opts}=St) -> end, comp_ret_err(St); false -> - Warn = messages_per_file(Warn1), - report_warnings(St#compile{warnings = Warn}), + [erl_diagnostic:report( + standard_io, Ds, + proplists:get_value(report_format, Opts, ansi), + #{ brief => proplists:get_bool(brief, Opts) }) + || member(report_warnings, Opts)], Ret1 = case member(binary, Opts) andalso - not member(no_code_generation, Opts) of + not member(no_code_generation, Opts) of true -> [Code]; false -> [] end, Ret2 = case member(return_warnings, Opts) of - true -> Ret1 ++ [Warn]; + true -> Ret1 ++ [erl_diagnostic:get_warnings(Ds)]; false -> Ret1 end, list_to_tuple([ok,Mod|Ret2]) end. -comp_ret_err(#compile{warnings=Warn0,errors=Err0,options=Opts}=St) -> - Warn = messages_per_file(Warn0), - Err = messages_per_file(Err0), - report_errors(St#compile{errors=Err}), - report_warnings(St#compile{warnings=Warn}), +comp_ret_err(#compile{options=Opts,diagnostics = Ds}) -> + erl_diagnostic:report( + standard_io, Ds, + proplists:get_value(report_format, Opts, ansi), + #{ brief => proplists:get_bool(brief, Opts), + types => [error || member(report_errors, Opts)] ++ + [warning || member(report_warnings, Opts)] ++ + [info || member(report_warnings, Opts)] ++ + [hint || member(report_warnings, Opts)] + }), + case member(return_errors, Opts) of - true -> {error,Err,Warn}; + true -> {error,erl_diagnostic:get_errors(Ds), + erl_diagnostic:get_warnings(Ds)}; false -> error end. not_werror(St) -> not werror(St). -werror(#compile{options=Opts,warnings=Ws}) -> - Ws =/= [] andalso member(warnings_as_errors, Opts). - -%% messages_per_file([{File,[Message]}]) -> [{File,[Message]}] -messages_per_file(Ms) -> - T = lists:sort([{File,M} || {File,Messages} <:- Ms, M <- Messages]), - PrioMs = [erl_scan, epp, erl_parse], - {Prio0, Rest} = - lists:mapfoldl(fun(M, A) -> - lists:partition(fun({_,{_,Mod,_}}) -> Mod =:= M; - (_) -> false - end, A) - end, T, PrioMs), - Prio = lists:sort(fun({_,{L1,_,_}}, {_,{L2,_,_}}) -> L1 =< L2 end, - lists:append(Prio0)), - flatmap(fun mpf/1, [Prio, Rest]). - -mpf(Ms) -> - [{File,[M || {F,M} <- Ms, F =:= File]} || - File <- lists:usort([F || {F,_} <:- Ms])]. +werror(#compile{options=Opts,diagnostics = D}) -> + erl_diagnostic:has_warnings(D) andalso member(warnings_as_errors, Opts). %% passes(forms|file, [Option]) -> {Extension,[{Name,PassFun}]} %% Figure out the extension of the input file and which passes @@ -1538,17 +1543,28 @@ fix_first_pass([_|Passes]) -> select_passes([{pass,Mod}|Ps], Opts) -> F = fun(Code0, St) -> - case Mod:module(Code0, St#compile.options) of - {ok,Code} -> - {ok,Code,St}; - {ok,Code,Ws} -> - {ok,Code,St#compile{warnings=St#compile.warnings++Ws}}; - {error,Es} -> - {error,St#compile{errors=St#compile.errors ++ Es}}; - Other -> - Es = [{St#compile.ifile,[{none,?MODULE,{bad_return,Mod,Other}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} - end + case erlang:function_exported(Mod, module, 3) of + true -> + case Mod:module(Code0, St#compile.options, St#compile.diagnostics) of + {ok, Code, Ds} -> + {ok, Code, St#compile{ diagnostics = Ds }}; + {error, Ds} -> + {error, St#compile{ diagnostics = Ds }}; + Other -> + {error,emit_error(St, none, ?MODULE, {bad_return,Mod,Other})} + end; + false -> + case Mod:module(Code0, St#compile.options) of + {ok,Code} -> + {ok,Code,St}; + {ok,Code,Ws} -> + {ok,Code,emit_warnings(St, Ws)}; + {error,Es} -> + {error,emit_errors(St, Es)}; + Other -> + {error,emit_error(St, none, ?MODULE, {bad_return,Mod,Other})} + end + end end, [{Mod,F}|select_passes(Ps, Opts)]; select_passes([{_,Fun}=P|Ps], Opts) when is_function(Fun) -> @@ -1640,7 +1656,7 @@ make_ssa_check_pass(PassFlag) -> case beam_ssa_check:module(Code, PassFlag) of ok -> {ok, Code, St}; {error, Errors} -> - {error, St#compile{errors=St#compile.errors++Errors}} + {error, emit_errors(St, Errors)} end end, {iff, PassFlag, {PassFlag, F}}. @@ -1863,8 +1879,7 @@ beam_consult_asm(_Code, St) -> {Module,Forms} = preprocess_asm_forms(Forms0), {ok,Forms,St#compile{module=Module,encoding=Encoding}}; {error,E} -> - Es = [{St#compile.ifile,[{none,?MODULE,{open,E}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_error(St, none, ?MODULE, {open, E})} end. get_module_name_from_asm({Mod,_,_,_,_}=Asm, St) -> @@ -1936,12 +1951,10 @@ do_parse_module(DefEncoding, #compile{ifile=File,options=Opts,dir=Dir}=St) -> end, {ok,Forms,St1#compile{encoding=Encoding}}; {error,E} -> - Es = [{St#compile.ifile,[{none,?MODULE,{epp,E}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error, emit_error(St, St#compile.ifile, none, ?MODULE, {epp, E})} end; {error, {Mod, Reason}} -> - Es = [{St#compile.ifile,[{none, Mod, Reason}]}], - {error, St#compile{errors = St#compile.errors ++ Es}} + {error, emit_error(St, none, Mod, Reason)} end. %% The atom to be used in the proplist of the meta chunk indicating @@ -1992,8 +2005,7 @@ consult_abstr(_Code, St) -> Encoding = epp:read_encoding(St#compile.ifile), {ok,Forms,St#compile{encoding=Encoding}}; {error,E} -> - Es = [{St#compile.ifile,[{none,?MODULE,{open,E}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_error(St, none, ?MODULE, {open, E})} end. parse_core(_Code, St) -> @@ -2007,15 +2019,15 @@ parse_core(_Code, St) -> {ok,Mod,St#compile{module=Name}}; {error,E} -> Es = [{St#compile.ifile,[E]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end; {error,E,_} -> Es = [{St#compile.ifile,[E]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end; {error,E} -> Es = [{St#compile.ifile,[{none,compile,{open,E}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end. get_module_name_from_core(Core, St) -> @@ -2076,23 +2088,22 @@ foldl_transform([T|Ts], Code0, St) -> StrippedCode = maybe_strip_columns(Code0, T, St), try Run({Name, Fun}, StrippedCode, St) of {error,Es,Ws} -> - {error,St#compile{warnings=St#compile.warnings ++ Ws, - errors=St#compile.errors ++ Es}}; + {error,emit_warnings(emit_errors(St, Es), Ws)}; {warning, Forms, Ws} -> foldl_transform(Ts, Forms, - St#compile{warnings=St#compile.warnings ++ Ws}); + emit_warnings(St, Ws)); Forms -> foldl_transform(Ts, Forms, St) catch Class:Reason:Stk -> Es = [{St#compile.ifile,[{none,compile, {parse_transform,T,{Class,Reason,Stk}}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end; false -> Es = [{St#compile.ifile,[{none,compile, {undef_parse_transform,T}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end; foldl_transform([], Code, St) -> %% We may need to strip columns added by parse transforms before returning @@ -2150,7 +2161,7 @@ foldl_core_transforms([T|Ts], Code0, St) -> Class:Reason:Stk -> Es = [{St#compile.ifile,[{none,compile, {core_transform,T,{Class,Reason,Stk}}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end; foldl_core_transforms([], Code, St) -> {ok,Code,St}. @@ -2179,19 +2190,17 @@ lint_module(Code, St) -> %% Insert name of module as base name, if needed. This is %% for compile:forms to work with listing files. St1 = add_default_base(St, Code), - {ok,Code,St1#compile{warnings=St1#compile.warnings ++ Ws}}; + {ok,Code,emit_warnings(St1, Ws)}; {error,Es,Ws} -> - {error,St#compile{warnings=St#compile.warnings ++ Ws, - errors=St#compile.errors ++ Es}} + {error,emit_warnings(emit_errors(St, Es), Ws)} end. core_lint_module(Code, St) -> case core_lint:module(Code, St#compile.options) of {ok,Ws} -> - {ok,Code,St#compile{warnings=St#compile.warnings ++ Ws}}; + {ok,Code,emit_warnings(St, Ws)}; {error,Es,Ws} -> - {error,St#compile{warnings=St#compile.warnings ++ Ws, - errors=St#compile.errors ++ Es}} + {error,emit_warnings(emit_errors(St, Es), Ws)} end. %% makedep + output and continue @@ -2342,7 +2351,7 @@ makedep_output(Code, #compile{options=Opts,ofile=Ofile}=St) -> {ok,Code,St}; {error,Reason} -> Err = {St#compile.ifile,[{none,?MODULE,{write_error,Reason}}]}, - {error,St#compile{errors=St#compile.errors++[Err]}} + {error,emit_errors(St, [Err])} end; true -> %% Write the dependencies to a device. @@ -2352,7 +2361,7 @@ makedep_output(Code, #compile{options=Opts,ofile=Ofile}=St) -> catch error:_ -> Err = {St#compile.ifile,[{none,?MODULE,write_error}]}, - {error,St#compile{errors=St#compile.errors++[Err]}} + {error,emit_errors(St, [Err])} end end. @@ -2384,8 +2393,7 @@ compile_directives_1(Opts1, Forms, #compile{options=Opts0}=St0) -> case any_obsolete_option(Opts) of {yes,Opt} -> Error = {St1#compile.ifile,[{none,?MODULE,{obsolete_option,Opt}}]}, - St = St1#compile{errors=[Error|St1#compile.errors]}, - {error,St}; + {error,emit_errors(St1, [Error])}; no -> {ok,Forms,St1} end. @@ -2419,8 +2427,7 @@ is_obsolete(_) -> false. core(Forms, #compile{options=Opts}=St) -> {ok,Core,Ws} = v3_core:module(Forms, Opts), Mod = cerl:concrete(cerl:module_name(Core)), - {ok,Core,St#compile{module=Mod,options=Opts, - warnings=St#compile.warnings++Ws}}. + {ok,Core,emit_warnings(St#compile{module=Mod,options=Opts}, Ws)}. core_fold_module_after_inlining(Code0, #compile{options=Opts}=St) -> %% Inlining may produce code that generates spurious warnings. @@ -2428,11 +2435,11 @@ core_fold_module_after_inlining(Code0, #compile{options=Opts}=St) -> {ok,Code,_Ws} = sys_core_fold:module(Code0, Opts), {ok,Code,St}. -core_to_ssa(Code0, #compile{options=Opts,warnings=Ws0}=St) -> +core_to_ssa(Code0, #compile{options=Opts}=St) -> {ok,Code,Ws} = beam_core_to_ssa:module(Code0, Opts), case Ws =:= [] orelse test_core_inliner(St) of false -> - {ok,Code,St#compile{warnings=Ws0++Ws}}; + {ok,Code,emit_warnings(St, Ws)}; true -> %% cerl_inline may produce code that generates spurious %% warnings. Ignore any such warnings. @@ -2482,8 +2489,7 @@ beam_docs(Code, #compile{dir = Dir, options = Options, {ok, Docs, Ws} -> Binary = term_to_binary(Docs, [deterministic, compressed]), MetaDocs = [{?META_DOC_CHUNK, Binary} | ExtraChunks], - {ok, Code, St#compile{extra_chunks = MetaDocs, - warnings = St#compile.warnings ++ Ws}}; + {ok, Code, emit_warnings(St#compile{extra_chunks = MetaDocs}, Ws)}; {error, no_docs} -> {ok, Code, St} end. @@ -2613,12 +2619,12 @@ beam_validator_strong(Code, St) -> beam_validator_weak(Code, St) -> beam_validator_1(Code, St, weak). -beam_validator_1(Code, #compile{errors=Errors0}=St, Level) -> +beam_validator_1(Code, St, Level) -> case beam_validator:validate(Code, Level) of ok -> {ok, Code, St}; {error, Es} -> - {error, St#compile{errors=Errors0 ++ Es}} + {error, emit_errors(St, Es)} end. beam_asm(Code0, #compile{ifile=File,extra_chunks=ExtraChunks,options=CompilerOpts}=St) -> @@ -2630,7 +2636,7 @@ beam_asm(Code0, #compile{ifile=File,extra_chunks=ExtraChunks,options=CompilerOpt {ok,Code} = beam_asm:module(Code0, Chunks, CompileInfo, CompilerOpts), {ok,Code,St#compile{abstract_code=[]}}; {error,Es} -> - {error,St#compile{errors=St#compile.errors ++ [{File,Es}]}} + {error,emit_errors(St, [{File,Es}])} end. beam_strip_types(Beam0, #compile{}=St) -> @@ -2701,7 +2707,7 @@ save_binary(Code, #compile{module=Mod,ofile=Outfile,options=Opts}=St) -> _ -> Es = [{St#compile.ofile, [{none,?MODULE,{module_name,Mod,Base}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end end. @@ -2717,11 +2723,11 @@ save_binary_1(Code, St) -> Es = [{Ofile,[{none,?MODULE,{rename,Tfile,Ofile, RenameError}}]}], _ = file:delete(Tfile), - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end; {error,Error} -> Es = [{Tfile,[{none,compile,{write_error,Error}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end. write_binary(Name, Bin, St) -> @@ -2734,35 +2740,6 @@ write_binary(Name, Bin, St) -> {error,_}=Error -> Error end. -%% report_errors(State) -> ok -%% report_warnings(State) -> ok - -report_errors(#compile{options=Opts,errors=Errors}) -> - case member(report_errors, Opts) of - true -> - foreach(fun ({{F,_L},Eds}) -> sys_messages:list_errors(F, Eds, Opts); - ({F,Eds}) -> sys_messages:list_errors(F, Eds, Opts) end, - Errors); - false -> ok - end. - -report_warnings(#compile{options=Opts,warnings=Ws0}) -> - Werror = member(warnings_as_errors, Opts), - P = case Werror of - true -> ""; - false -> "Warning: " - end, - ReportWerror = Werror andalso member(report_errors, Opts), - case member(report_warnings, Opts) orelse ReportWerror of - true -> - Ws1 = flatmap(fun({{F,_L},Eds}) -> sys_messages:format_messages(F, P, Eds, Opts); - ({F,Eds}) -> sys_messages:format_messages(F, P, Eds, Opts) end, - Ws0), - Ws = lists:sort(Ws1), - foreach(fun({_,Str}) -> io:put_chars(Str) end, Ws); - false -> ok - end. - %%% %%% Filter warnings. %%% @@ -2798,6 +2775,7 @@ ignore_tags([_|Opts], Ignore) -> ignore_tags(Opts, Ignore); ignore_tags([], Ignore) -> Ignore. + %% erlfile(Dir, Base) -> ErlFile %% outfile(Base, Extension, Options) -> OutputFile %% objfile(Base, Target, Options) -> ObjFile @@ -2888,7 +2866,7 @@ listing(LFun, Ext, Code, St) -> {ok,Code,St}; {error,Error} -> Es = [{Lfile,[{none,compile,{write_error,Error}}]}], - {error,St#compile{errors=St#compile.errors ++ Es}} + {error,emit_errors(St, Es)} end. to_dis(Code, #compile{module=Module,ofile=Outfile}=St) -> @@ -3058,7 +3036,7 @@ make_erl_options(Opts) -> (Name) -> {d,Name} end, Defines), - Options ++ [report_errors, {cwd, Cwd}, {outdir, Outdir} | + Options ++ [{options_source, erlc}, report_errors, {cwd, Cwd}, {outdir, Outdir} | [{i, Dir} || Dir <- Includes]] ++ Specific. pre_load() -> diff --git a/lib/compiler/src/compiler.app.src b/lib/compiler/src/compiler.app.src index 7b1f0be40fa1..0691003cd9d1 100644 --- a/lib/compiler/src/compiler.app.src +++ b/lib/compiler/src/compiler.app.src @@ -33,7 +33,7 @@ beam_dict, beam_digraph, beam_disasm, - beam_doc, + beam_doc, beam_flatten, beam_jump, beam_listing, @@ -80,7 +80,6 @@ sys_core_inline, sys_core_prepare, sys_coverage, - sys_messages, sys_pre_attributes, v3_core ]}, diff --git a/lib/compiler/src/sys_messages.erl b/lib/compiler/src/sys_messages.erl deleted file mode 100644 index 48c0b257c7e3..000000000000 --- a/lib/compiler/src/sys_messages.erl +++ /dev/null @@ -1,218 +0,0 @@ -%% -%% %CopyrightBegin% -%% -%% SPDX-License-Identifier: Apache-2.0 -%% -%% Copyright 2020-2024 Facebook, Inc. and its affiliates. -%% Copyright Ericsson AB 1996-2025. 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% - --module(sys_messages). --moduledoc false. - --export([format_messages/4, list_errors/3]). - --type loc() :: erl_anno:location(). --type message() :: {Where :: none | {File::string(), loc()}, Text :: iolist()}. - --spec format_messages(File::string(), Prefix::string(), [erl_lint:error_info()], - Opts::[term()]) -> [message()]. - -format_messages(F, P, [{none, Mod, E} | Es], Opts) -> - M = {none, io_lib:format("~ts: ~s~ts\n", [F, P, Mod:format_error(E)])}, - [M | format_messages(F, P, Es, Opts)]; -format_messages(F, P, [{Loc, Mod, E} | Es], Opts) -> - StartLoc = Loc, - EndLoc = StartLoc, - Src = quote_source(F, StartLoc, EndLoc, Opts), - Msg = io_lib:format("~ts:~ts: ~s~ts\n~ts", [ - F, - fmt_pos(StartLoc), - P, - Mod:format_error(E), - Src - ]), - Pos = StartLoc, - [{{F, Pos}, Msg} | format_messages(F, P, Es, Opts)]; -format_messages(_, _, [], _Opts) -> - []. - --spec list_errors(File::string(), [erl_lint:error_info()], Opts::[term()]) -> ok. - -list_errors(F, [{none, Mod, E} | Es], Opts) -> - io:fwrite("~ts: ~ts\n", [F, Mod:format_error(E)]), - list_errors(F, Es, Opts); -list_errors(F, [{Loc, Mod, E} | Es], Opts) -> - StartLoc = Loc, - EndLoc = StartLoc, - Src = quote_source(F, StartLoc, EndLoc, Opts), - io:fwrite("~ts:~ts: ~ts\n~ts", [F, fmt_pos(StartLoc), Mod:format_error(E), Src]), - list_errors(F, Es, Opts); -list_errors(_F, [], _Opts) -> - ok. - -fmt_pos({Line, Col}) -> - io_lib:format("~w:~w", [Line, Col]); -fmt_pos(Line) -> - io_lib:format("~w", [Line]). - -quote_source(File, StartLoc, EndLoc, Opts) -> - case proplists:get_bool(brief, Opts) of - true -> ""; - false -> quote_source_1(File, StartLoc, EndLoc) - end. - -quote_source_1(File, Line, Loc2) when is_integer(Line) -> - quote_source_1(File, {Line, 1}, Loc2); -quote_source_1(File, Loc1, Line) when is_integer(Line) -> - quote_source_1(File, Loc1, {Line, -1}); -quote_source_1(File, {StartLine, StartCol}, {EndLine, EndCol}) -> - case file:read_file(File) of - {ok, Bin} -> - Enc = case epp:read_encoding_from_binary(Bin) of - none -> epp:default_encoding(); - Enc0 -> Enc0 - end, - Ctx = - if - StartLine =:= EndLine -> 0; - true -> 1 - end, - case seek_line(Bin, 1, StartLine - Ctx) of - {ok, Bin1} -> - quote_source_2(Bin1, Enc, StartLine, StartCol, EndLine, EndCol, Ctx); - error -> - "" - end; - {error, _} -> - "" - end. - -quote_source_2(Bin, Enc, StartLine, StartCol, EndLine, EndCol, Ctx) -> - case take_lines(Bin, Enc, StartLine - Ctx, EndLine + Ctx) of - [] -> - ""; - Lines -> - Lines1 = - case length(Lines) =< (4 + Ctx) of - true -> - Lines; - false -> - %% before = context + start line + following line - %% after = end line + context - %% (total lines: 3 + 1 + context) - Before = lists:sublist(Lines, 2 + Ctx), - After = lists:reverse( - lists:sublist(lists:reverse(Lines), 1 + Ctx) - ), - Before ++ [{0, "..."}] ++ After - end, - Lines2 = decorate(Lines1, StartLine, StartCol, EndLine, EndCol), - [[fmt_line(L, Text) || {L, Text} <:- Lines2], $\n] - end. - -line_prefix() -> - "% ". - -fmt_line(L, Text) -> - {LineText, LineTextLength} = line_to_txt(L), - [line_prefix(), - io_lib:format("~*.ts| ", [LineTextLength, LineText]), - Text, "\n"]. - -line_to_txt(L) -> - LineText = integer_to_list(abs(L)), - Length = max(4, length(LineText)), - if - L < 0 -> - {"", Length}; - true -> - {LineText, Length} - end. - -decorate([{Line, Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) when - Line =:= StartLine, EndLine =:= StartLine -> - %% start and end on same line - S = underline(Text, StartCol, EndCol), - decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol); -decorate([{Line, Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) when Line =:= StartLine -> - %% start with end on separate line - S = underline(Text, StartCol, string:length(Text) + 1), - decorate(S, L, Ls, StartLine, StartCol, EndLine, EndCol); -decorate([{_Line, _Text} = L | Ls], StartLine, StartCol, EndLine, EndCol) -> - [L | decorate(Ls, StartLine, StartCol, EndLine, EndCol)]; -decorate([], _StartLine, _StartCol, _EndLine, _EndCol) -> - []. - -%% don't produce empty decoration lines -decorate("", L, Ls, StartLine, StartCol, EndLine, EndCol) -> - [L | decorate(Ls, StartLine, StartCol, EndLine, EndCol)]; -decorate(Text, {Line, _} = L, Ls, StartLine, StartCol, EndLine, EndCol) -> - [L, {-Line, Text} | decorate(Ls, StartLine, StartCol, EndLine, EndCol)]. - -%% End typically points to the first position after the actual region. -%% If End = Start, we adjust it to Start+1 to mark at least one character -%% TODO: colorization option -underline(_Text, Start, End) when End < Start -> - % no underlining at all if end column is unknown - ""; -underline(Text, Start, Start) -> - underline(Text, Start, Start + 1); -underline(Text, Start, End) -> - underline(Text, Start, End, 1). - -underline([$\t | Text], Start, End, N) when N < Start -> - [$\t | underline(Text, Start, End, N + 1)]; -underline([_ | Text], Start, End, N) when N < Start -> - [$\s | underline(Text, Start, End, N + 1)]; -underline(_Text, _Start, End, N) -> - underline_1(N, End). - -underline_1(N, End) when N < End -> - [$^ | underline_1(N + 1, End)]; -underline_1(_N, _End) -> - "". - -seek_line(Bin, L, L) -> {ok, Bin}; -seek_line(<<$\n, Rest/binary>>, N, L) -> seek_line(Rest, N + 1, L); -seek_line(<<$\r, $\n, Rest/binary>>, N, L) -> seek_line(Rest, N + 1, L); -seek_line(<<_, Rest/binary>>, N, L) -> seek_line(Rest, N, L); -seek_line(<<>>, _, _) -> error. - -take_lines(<<>>, _Enc, _Here, _To) -> - []; -take_lines(Bin, Enc, Here, To) when Here =< To -> - {Text, Rest} = take_line(Bin, <<>>), - [{Here, text_to_string(Text, Enc)} - | take_lines(Rest, Enc, Here + 1, To)]; -take_lines(_Bin, _Enc, _Here, _To) -> - []. - -text_to_string(Text, Enc) -> - case unicode:characters_to_list(Text, Enc) of - String when is_list(String) -> String; - {error, String, _Rest} -> String; - {incomplete, String, _Rest} -> String - end. - -take_line(<<$\n, Rest/binary>>, Ack) -> - {Ack, Rest}; -take_line(<<$\r, $\n, Rest/binary>>, Ack) -> - {Ack, Rest}; -take_line(<>, Ack) -> - take_line(Rest, <>); -take_line(<<>>, Ack) -> - {Ack, <<>>}. diff --git a/lib/compiler/test/error_SUITE.erl b/lib/compiler/test/error_SUITE.erl index ff91a3e65339..445b85a0faef 100644 --- a/lib/compiler/test/error_SUITE.erl +++ b/lib/compiler/test/error_SUITE.erl @@ -178,8 +178,8 @@ bif_clashes(Config) when is_list(Config) -> %% Tests that a head mismatch is reported on the correct line (OTP-2125). head_mismatch_line(Config) when is_list(Config) -> - [E|_] = get_compilation_errors(Config, "head_mismatch_line"), - {{28,1}, Mod, Reason} = E, + {{28,1}, Mod, Reason} = + lists:keyfind({28,1}, 1, get_compilation_errors(Config, "head_mismatch_line")), ("head mismatch: previous function foo/1 is distinct from bar/1. " "Is the semicolon in foo/1 unwanted?") = lists:flatten(Reason), Mod:format_error(Reason), @@ -188,8 +188,8 @@ head_mismatch_line(Config) when is_list(Config) -> %% Tests that a head mismatch with the same function name reports a different error from above. %% https://github.com/erlang/otp/pull/7383#issuecomment-1586564294 head_mismatch_same_function_name(Config) when is_list(Config) -> - [E|_] = get_compilation_errors(Config, "head_mismatch_same_function_name"), - {{27,1}, Mod, Reason} = E, + {{27,1}, Mod, Reason} = + lists:keyfind({27,1}, 1, get_compilation_errors(Config, "head_mismatch_same_function_name")), ("head mismatch: function foo with arities 1 and 2 is regarded as " "two distinct functions. Is the number of arguments incorrect " "or is the semicolon in foo/1 unwanted?") = lists:flatten(Reason), From 79c35bf55e6220f867cf4d168b81465992dc2d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 17:05:13 +0200 Subject: [PATCH 11/15] compile: Migrate recv opt info to use erl_diagnostic --- .../doc/diagnostics/ERL-2001-recv-opt-info.md | 55 +++++++++ lib/compiler/src/beam_ssa_recv.erl | 108 ++++++++++-------- lib/compiler/src/compiler.app.src | 1 + 3 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 lib/compiler/doc/diagnostics/ERL-2001-recv-opt-info.md diff --git a/lib/compiler/doc/diagnostics/ERL-2001-recv-opt-info.md b/lib/compiler/doc/diagnostics/ERL-2001-recv-opt-info.md new file mode 100644 index 000000000000..6dce10063356 --- /dev/null +++ b/lib/compiler/doc/diagnostics/ERL-2001-recv-opt-info.md @@ -0,0 +1,55 @@ +# ERL-0001 - Function head mismatch + +## Example + +```erlang +%% foo.erl +-module(foo). +foo(0) -> 1; +boo(1) -> 2. +``` + +``` +$ erlc foo.erl +foo.erl:4:1: error: head mismatch: previous function foo/1 is distinct from bar/1. [ERL-0001] +% 4| bar(1) -> 2. +% | ^ +% help: call `erlc -explain ERL-0001` to see a detailed explanation +% help: Is the semicolon in foo/1 unwanted? +``` + +## Explanation + +The error message indicates that two function clauses belonging the same function +differ in their name or in the number of arguments. + +In Erlang functions are uniquely identified by the module they belong to, the +function name and the number of argument they take (known as *arity*). +Each function can be composed by multiple *clauses*, separated by a semicolon (`;`). +Therefore, all clauses belonging to the same function have to share the same name. + +To fix the error you need to ensure that every function clause has the same name +and that it takes the same number of arguments. + +In the above example, `boo/1` could be a second clause for the `foo/1` function, +containing a typo. In that case, the corrective action would be to fix the typo: + +```erlang +foo(0) -> 1; +foo(1) -> 2. +``` + +It could also be that `boo/1` is intended to be a completely different function. +In that case the error can be fixed by replacing the semicolon on the previous +line with a fullstop. Leaving an empty line between the two functions would also +be a good idea, to help the reader understanding `foo/1` and `boo/1` are two +distinct functions: + +```erlang +foo(0) -> 1. + +boo(1) -> 2. +``` + +For more information about Erlang functions please refer to the +[Reference Manual](`e:system:ref_man_functions`). diff --git a/lib/compiler/src/beam_ssa_recv.erl b/lib/compiler/src/beam_ssa_recv.erl index 77559a80ffd6..c85ee7ec2116 100644 --- a/lib/compiler/src/beam_ssa_recv.erl +++ b/lib/compiler/src/beam_ssa_recv.erl @@ -22,7 +22,7 @@ -module(beam_ssa_recv). -moduledoc false. --export([format_error/1, module/2]). +-export([module/3]). %%% %%% In code such as: @@ -114,11 +114,6 @@ -define(RETURN_BLOCK, -1). -define(ENTRY_BLOCK, 0). --spec format_error(term()) -> nonempty_string(). - -format_error(OptInfo) -> - format_opt_info(OptInfo). - -record(scan, { graph=beam_digraph:new(), module :: #{ beam_ssa:b_local() => {beam_ssa:block_map(), [beam_ssa:b_var()], @@ -126,12 +121,13 @@ format_error(OptInfo) -> recv_candidates=#{}, ref_candidates=#{} }). --spec module(Module, Options) -> Result when +-spec module(Module, Options, Diagnostics) -> Result when Module :: beam_ssa:b_module(), Options :: [compile:option()], + Diagnostics :: erl_diagnostic:state(), Result :: {ok, beam_ssa:b_module(), list()}. -module(#b_module{}=Mod0, Opts) -> +module(#b_module{}=Mod0, Opts, Ds) -> case scan(Mod0) of #scan{}=Scan -> %% Figure out where to place marker creation, usage, and clearing @@ -145,15 +141,12 @@ module(#b_module{}=Mod0, Opts) -> Mod = optimize(Mod0, Markers, Uses, Clears), - Ws = case proplists:get_bool(recv_opt_info, Opts) of - true -> collect_opt_info(Mod); - false -> [] - end, + RecvOptDs = collect_opt_info(Mod, Ds, Opts), - {ok, Mod, Ws}; + {ok, Mod, RecvOptDs}; none -> %% No peek_message instructions; just skip it all. - {ok, Mod0, []} + {ok, Mod0, Ds} end. scan(#b_module{body=Fs}) -> @@ -868,10 +861,21 @@ insert_clears_1([], Count, Acc) -> %%% +recv_opt_info %%% -collect_opt_info(#b_module{body=Fs}) -> - coi_1(Fs, []). +collect_opt_info(#b_module{body=Fs}, Ds0, Opts) -> + RecvOptInfo = #{ key => recv_opt_info, + format => fun format_opt_info/1, + type => info, + default => false, + on => ~"recv_opt_info", + code => ~"ERL-2001" }, + Ds1 = erl_diagnostic:add_diagnostics(Ds0, [RecvOptInfo]), + case proplists:get_bool(recv_opt_info, Opts) of + false -> Ds1; + true -> + coi_1(Fs, erl_diagnostic:push_diagnostic(Ds1, recv_opt_info, true, erl_anno:new(0))) + end. -coi_1([#b_function{args=Args,bs=Blocks}=F | Fs], Acc0) -> +coi_1([#b_function{args=Args,bs=Blocks}=F | Fs], Ds0) -> Lbls = beam_ssa:rpo(Blocks), Where = beam_ssa:get_anno(location, F, []), {Defs, _} = foldl(fun(Var, {Defs0, Index0}) -> @@ -879,25 +883,26 @@ coi_1([#b_function{args=Args,bs=Blocks}=F | Fs], Acc0) -> Index = Index0 + 1, {Defs, Index} end, {#{}, 1}, Args), - Acc = coi_bs(Lbls, Blocks, Where, Defs, Acc0), - coi_1(Fs, Acc); -coi_1([], Acc) -> - Acc. + Ds = coi_bs(Lbls, Blocks, Where, Defs, Ds0), + coi_1(Fs, Ds); +coi_1([], Ds) -> + Ds. -coi_bs([Lbl | Lbls], Blocks, Where, Defs0, Ws0) -> +coi_bs([Lbl | Lbls], Blocks, Where, Defs0, Ds0) -> #{ Lbl := #b_blk{is=Is,last=Last} } = Blocks, - {Defs, Ws} = coi_is(Is, Last, Blocks, Where, Defs0, Ws0), - coi_bs(Lbls, Blocks, Where, Defs, Ws); -coi_bs([], _Blocks, _Where, _Defs, Ws) -> - Ws. + {Defs, Ds} = coi_is(Is, Last, Blocks, Where, Defs0, Ds0), + coi_bs(Lbls, Blocks, Where, Defs, Ds); +coi_bs([], _Blocks, _Where, _Defs, Ds) -> + Ds. coi_is([#b_set{anno=Anno,op=peek_message,args=[#b_var{}]=Args } | Is], - Last, Blocks, Where, Defs, Ws) -> + Last, Blocks, Where, Defs, Ds) -> [Creation] = coi_creations(Args, Blocks, Defs), %Assertion. - Warning = make_warning({used_receive_marker, Creation}, Anno, Where), - coi_is(Is, Last, Blocks, Where, Defs, [Warning | Ws]); + Ds1 = erl_diagnostic:emit(Ds, recv_opt_info, loc(Anno, Where), + [{used_receive_marker, Creation}]), + coi_is(Is, Last, Blocks, Where, Defs, Ds1); coi_is([#b_set{anno=Anno,op=peek_message,args=[#b_literal{}] } | Is], - Last, Blocks, Where, Defs, Ws) -> + Last, Blocks, Where, Defs, Ds) -> %% Is this a selective receive? #b_br{succ=NextMsg} = Last, @@ -907,23 +912,28 @@ coi_is([#b_set{anno=Anno,op=peek_message,args=[#b_literal{}] } | Is], _ -> unoptimized_selective_receive end, - Warning = make_warning(Info, Anno, Where), - coi_is(Is, Last, Blocks, Where, Defs, [Warning | Ws]); + Ds1 = erl_diagnostic:emit(Ds, recv_opt_info, loc(Anno, Where), + [Info]), + coi_is(Is, Last, Blocks, Where, Defs, Ds1); coi_is([#b_set{anno=Anno,op=recv_marker_reserve} | Is], - Last, Blocks, Where, Defs, Ws) -> - Warning = make_warning(reserved_receive_marker, Anno, Where), - coi_is(Is, Last, Blocks, Where, Defs, [Warning | Ws]); + Last, Blocks, Where, Defs, Ds) -> + Ds1 = erl_diagnostic:emit(Ds, recv_opt_info, loc(Anno, Where), + [reserved_receive_marker]), + coi_is(Is, Last, Blocks, Where, Defs, Ds1); coi_is([#b_set{anno=Anno,op=call,dst=Dst,args=[#b_local{} | Args] }=I | Is], - Last, Blocks, Where, Defs0, Ws0) -> + Last, Blocks, Where, Defs0, Ds0) -> Defs = Defs0#{ Dst => I }, - Ws = [make_warning({passed_marker, Creation}, Anno, Where) - || #b_set{}=Creation <- coi_creations(Args, Blocks, Defs)] ++ Ws0, - coi_is(Is, Last, Blocks, Where, Defs, Ws); -coi_is([#b_set{dst=Dst}=I | Is], Last, Blocks, Where, Defs0, Ws) -> + Ds1 = foldl(fun(#b_set{}=Creation, Ds) -> + erl_diagnostic:emit(Ds, recv_opt_info, loc(Anno, Where), + [{passed_marker, Creation}]); + (_, Ds) -> Ds + end, Ds0, coi_creations(Args, Blocks, Defs)), + coi_is(Is, Last, Blocks, Where, Defs, Ds1); +coi_is([#b_set{dst=Dst}=I | Is], Last, Blocks, Where, Defs0, Ds) -> Defs = Defs0#{ Dst => I }, - coi_is(Is, Last, Blocks, Where, Defs, Ws); -coi_is([], _Last, _Blocks, _Where, Defs, Ws) -> - {Defs, Ws}. + coi_is(Is, Last, Blocks, Where, Defs, Ds); +coi_is([], _Last, _Blocks, _Where, Defs, Ds) -> + {Defs, Ds}. coi_creations([Var | Vars], Blocks, Defs) -> case Defs of @@ -944,13 +954,11 @@ coi_creations([Var | Vars], Blocks, Defs) -> coi_creations([], _Blocks, _Defs) -> []. -make_warning(Term, Anno, Where) -> - {File, Line} = - case maps:get(location, Anno, Where) of - {_, _} = Location -> Location; - _ -> {"no_file", none} - end, - {File,[{Line,?MODULE,Term}]}. +loc(Anno, Where) -> + case maps:get(location, Anno, Where) of + {File, Line} -> erl_anno:set_file(File, erl_anno:new(Line)); + _ -> erl_anno:new(0) + end. format_opt_info(matches_any_message) -> "INFO: receive matches any message, this is always fast"; diff --git a/lib/compiler/src/compiler.app.src b/lib/compiler/src/compiler.app.src index 0691003cd9d1..7bcb1bd3aa59 100644 --- a/lib/compiler/src/compiler.app.src +++ b/lib/compiler/src/compiler.app.src @@ -86,5 +86,6 @@ {registered, []}, {applications, [kernel, stdlib]}, {env, []}, + {documentation_url, "https://erlang.org/doc/apps/compiler/"}, {runtime_dependencies, ["stdlib-6.0","kernel-8.4","erts-13.0", "crypto-5.1"]}]}. From 1d2ad43e6f58629d727136a3b98e079566766fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 17:06:50 +0200 Subject: [PATCH 12/15] Start adding diagnostic codes to erl_parse --- .../doc/diagnostics/ERL-0001-head_mismatch.md | 55 +++++++++++++++++++ .../doc/diagnostics/ERL-0007-syntax-error.md | 55 +++++++++++++++++++ lib/stdlib/src/erl_parse.yrl | 8 +++ 3 files changed, 118 insertions(+) create mode 100644 lib/compiler/doc/diagnostics/ERL-0001-head_mismatch.md create mode 100644 lib/compiler/doc/diagnostics/ERL-0007-syntax-error.md diff --git a/lib/compiler/doc/diagnostics/ERL-0001-head_mismatch.md b/lib/compiler/doc/diagnostics/ERL-0001-head_mismatch.md new file mode 100644 index 000000000000..6dce10063356 --- /dev/null +++ b/lib/compiler/doc/diagnostics/ERL-0001-head_mismatch.md @@ -0,0 +1,55 @@ +# ERL-0001 - Function head mismatch + +## Example + +```erlang +%% foo.erl +-module(foo). +foo(0) -> 1; +boo(1) -> 2. +``` + +``` +$ erlc foo.erl +foo.erl:4:1: error: head mismatch: previous function foo/1 is distinct from bar/1. [ERL-0001] +% 4| bar(1) -> 2. +% | ^ +% help: call `erlc -explain ERL-0001` to see a detailed explanation +% help: Is the semicolon in foo/1 unwanted? +``` + +## Explanation + +The error message indicates that two function clauses belonging the same function +differ in their name or in the number of arguments. + +In Erlang functions are uniquely identified by the module they belong to, the +function name and the number of argument they take (known as *arity*). +Each function can be composed by multiple *clauses*, separated by a semicolon (`;`). +Therefore, all clauses belonging to the same function have to share the same name. + +To fix the error you need to ensure that every function clause has the same name +and that it takes the same number of arguments. + +In the above example, `boo/1` could be a second clause for the `foo/1` function, +containing a typo. In that case, the corrective action would be to fix the typo: + +```erlang +foo(0) -> 1; +foo(1) -> 2. +``` + +It could also be that `boo/1` is intended to be a completely different function. +In that case the error can be fixed by replacing the semicolon on the previous +line with a fullstop. Leaving an empty line between the two functions would also +be a good idea, to help the reader understanding `foo/1` and `boo/1` are two +distinct functions: + +```erlang +foo(0) -> 1. + +boo(1) -> 2. +``` + +For more information about Erlang functions please refer to the +[Reference Manual](`e:system:ref_man_functions`). diff --git a/lib/compiler/doc/diagnostics/ERL-0007-syntax-error.md b/lib/compiler/doc/diagnostics/ERL-0007-syntax-error.md new file mode 100644 index 000000000000..6dce10063356 --- /dev/null +++ b/lib/compiler/doc/diagnostics/ERL-0007-syntax-error.md @@ -0,0 +1,55 @@ +# ERL-0001 - Function head mismatch + +## Example + +```erlang +%% foo.erl +-module(foo). +foo(0) -> 1; +boo(1) -> 2. +``` + +``` +$ erlc foo.erl +foo.erl:4:1: error: head mismatch: previous function foo/1 is distinct from bar/1. [ERL-0001] +% 4| bar(1) -> 2. +% | ^ +% help: call `erlc -explain ERL-0001` to see a detailed explanation +% help: Is the semicolon in foo/1 unwanted? +``` + +## Explanation + +The error message indicates that two function clauses belonging the same function +differ in their name or in the number of arguments. + +In Erlang functions are uniquely identified by the module they belong to, the +function name and the number of argument they take (known as *arity*). +Each function can be composed by multiple *clauses*, separated by a semicolon (`;`). +Therefore, all clauses belonging to the same function have to share the same name. + +To fix the error you need to ensure that every function clause has the same name +and that it takes the same number of arguments. + +In the above example, `boo/1` could be a second clause for the `foo/1` function, +containing a typo. In that case, the corrective action would be to fix the typo: + +```erlang +foo(0) -> 1; +foo(1) -> 2. +``` + +It could also be that `boo/1` is intended to be a completely different function. +In that case the error can be fixed by replacing the semicolon on the previous +line with a fullstop. Leaving an empty line between the two functions would also +be a good idea, to help the reader understanding `foo/1` and `boo/1` are two +distinct functions: + +```erlang +foo(0) -> 1. + +boo(1) -> 2. +``` + +For more information about Erlang functions please refer to the +[Reference Manual](`e:system:ref_man_functions`). diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index 4e966b659aba..3840f1f52b34 100644 --- a/lib/stdlib/src/erl_parse.yrl +++ b/lib/stdlib/src/erl_parse.yrl @@ -791,6 +791,7 @@ processed (see section [Error Information](#module-error-information)). new_anno/1, anno_to_term/1, anno_from_term/1]). -export([first_anno/1]). % Internal export. +-export([error_code/1]). -export_type([abstract_clause/0, abstract_expr/0, abstract_form/0, abstract_type/0, form_info/0, error_info/0]). @@ -2318,4 +2319,11 @@ build_ssa_check_label({atom,L,_}, _) -> add_anno_check({check_expr,Loc,Args}, AnnoCheck) -> {check_expr,Loc,Args,AnnoCheck}. +%% Errors in erl_parse are strings, so producing an error code is a bit difficult. +%% Should do something about that so that we get an ID even for erl_parse errors. +-doc false. +error_code("bad attribute") -> ~"ERL-0006"; +error_code(["syntax error before:" ++ _ | _]) -> ~"ERL-0007"; +error_code(_) -> undefined. + %% vim: ft=erlang From 890347b2de327ebb2ffd928fd8018f372dc2fe0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 17:07:44 +0200 Subject: [PATCH 13/15] Start adding diagnostic codes to erl_scan --- lib/stdlib/src/erl_scan.erl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/stdlib/src/erl_scan.erl b/lib/stdlib/src/erl_scan.erl index 567344abed88..88aae2943ce8 100644 --- a/lib/stdlib/src/erl_scan.erl +++ b/lib/stdlib/src/erl_scan.erl @@ -92,7 +92,7 @@ Armstrong, Virding and Williams: 'Concurrent Programming in Erlang', Chapter 13. category/1,symbol/1]). %%% Private --export([continuation_location/1]). +-export([continuation_location/1, error_code/1]). -export_type([error_info/0, options/0, @@ -176,6 +176,11 @@ format_error(string_concat) -> format_error(Other) -> lists:flatten(io_lib:write(Other)). +-doc false. +error_code({unterminated, _}) -> ~"ERL-0003"; +error_code({illegal, _}) -> ~"ERL-0004"; +error_code(_) -> undefined. + -doc(#{equiv => string(String, 1)}). -spec string(String) -> Return when String :: string(), @@ -600,7 +605,7 @@ scan1([$\%=C|Cs], St, Line, Col, Toks) -> scan_comment(Cs, St, Line, Col, Toks, [C]); %% More punctuation characters below. scan1([C|_], _St, _Line, _Col0, _Toks) when not ?CHAR(C) -> - error({not_character,C}); + erlang:error({not_character,C}); scan1([C|Cs], St, Line, Col, Toks) when C >= $A, C =< $Z -> scan_variable(Cs, St, Line, Col, Toks, [C]); scan1([C|Cs], St, Line, Col, Toks) when C >= $a, C =< $z -> From 81222547eaf73149c3aa6dea4032cc4a5c475d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 17:10:36 +0200 Subject: [PATCH 14/15] stdlib: Migrate erl_lint to use erl_diagnostic --- .../diagnostics/ERL-1001-unused-function.md | 26 + .../doc/diagnostics/ERL-1002-missing-spec.md | 27 + lib/compiler/src/compile.erl | 12 +- lib/stdlib/src/erl_lint.erl | 941 ++++++++++++++---- lib/stdlib/test/erl_lint_SUITE.erl | 12 +- 5 files changed, 812 insertions(+), 206 deletions(-) create mode 100644 lib/compiler/doc/diagnostics/ERL-1001-unused-function.md create mode 100644 lib/compiler/doc/diagnostics/ERL-1002-missing-spec.md diff --git a/lib/compiler/doc/diagnostics/ERL-1001-unused-function.md b/lib/compiler/doc/diagnostics/ERL-1001-unused-function.md new file mode 100644 index 000000000000..8a7064a50192 --- /dev/null +++ b/lib/compiler/doc/diagnostics/ERL-1001-unused-function.md @@ -0,0 +1,26 @@ +# ERL-1001 - Unused functions + +## Example + +```erlang +%% foo.erl +-module(foo). +-export([foo/1]). +foo(0) -> 1. +boo(1) -> 2. +``` + +```bash +$ erlc foo.erl +foo.erl:4:1: warning: function bar/1 is unused [ERL-1001] +% 4| bar(1) -> 2. +% | ^ +% help: call `erlc -explain ERL-1001` to see a detailed explanation +% note: `warn_unused_function` was enabled by default; + use `nowarn_unused_function` to turn off this warning +``` + +## Explanation + +This warning is emitted when when a function in a module is not exported and +not used by any other function within the same module. \ No newline at end of file diff --git a/lib/compiler/doc/diagnostics/ERL-1002-missing-spec.md b/lib/compiler/doc/diagnostics/ERL-1002-missing-spec.md new file mode 100644 index 000000000000..b2bd047089b0 --- /dev/null +++ b/lib/compiler/doc/diagnostics/ERL-1002-missing-spec.md @@ -0,0 +1,27 @@ +# ERL-1002 - Function is missing specification + +## Example + +```erlang +%% foo.erl +-module(foo). +-export([foo/1]). +foo(0) -> 1. +``` + +```bash +$ erlc +warn_missing_spec foo.erl +foo.erl:3:1: warning: missing specification for function foo/1 [ERL-1002] +% 3| foo(0) -> 1. +% | ^ +% help: call `erlc -explain ERL-1002` to see a detailed explanation +% note: `warn_missing_spec` was enabled by erlc; + use `nowarn_missing_spec` to turn off this warning +``` + +## Explanation + +This warning is disabled by default. It can be enabled by the options +`warn_missing_spec`, `warn_missing_spec_all` and `warn_missing_spec_documented`. +When enabled it triggers a warning for each function that does not have +a function specification. \ No newline at end of file diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index 65d890bfcce4..c59584d449ff 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -2185,14 +2185,16 @@ add_default_base(St, Forms) -> end. lint_module(Code, St) -> - case erl_lint:module(Code, St#compile.ifile, St#compile.options) of - {ok,Ws} -> + % io:format("~p~n",[St#compile.options]), + LintDs = erl_lint:module(Code, St#compile.ifile, St#compile.options, St#compile.diagnostics), + case erl_diagnostic:has_errors(LintDs) of + false -> %% Insert name of module as base name, if needed. This is %% for compile:forms to work with listing files. St1 = add_default_base(St, Code), - {ok,Code,emit_warnings(St1, Ws)}; - {error,Es,Ws} -> - {error,emit_warnings(emit_errors(St, Es), Ws)} + {ok, Code, St1#compile{ diagnostics = LintDs }}; + true -> + {error,St#compile{ diagnostics = LintDs }} end. core_lint_module(Code, St) -> diff --git a/lib/stdlib/src/erl_lint.erl b/lib/stdlib/src/erl_lint.erl index 314ac7d1a24a..ae4e853e5a8a 100644 --- a/lib/stdlib/src/erl_lint.erl +++ b/lib/stdlib/src/erl_lint.erl @@ -70,7 +70,7 @@ Module:format_error(ErrorDescriptor) `m:epp`, `m:erl_parse` """. --export([module/1,module/2,module/3,format_error/1]). +-export([module/1,module/2,module/3,module/4,format_error/1]). -export([exprs/2,exprs_opt/3,used_vars/2]). % Used from erl_eval.erl. -export([is_pattern_expr/1,is_guard_test/1,is_guard_test/2,is_guard_test/3]). -export([is_guard_expr/1]). @@ -190,17 +190,14 @@ value_option(Flag, Default, On, OnVal, Off, OffVal, Opts) -> on_load=[] :: [fa()], %On-load function on_load_anno=erl_anno:new(0) %Location for on_load :: erl_anno:anno(), - clashes=[], %Exported functions named as BIFs + clashes=[], %Exported functions named as BIFs not_deprecated=[], %Not considered deprecated not_removed=gb_sets:empty() %Not considered removed :: gb_sets:set(module_or_mfa()), func=[], %Current function type_id=[], %Current type id - warn_format=0, %Warn format calls - enabled_warnings=[], %All enabled warnings (ordset). + diagnostics :: erl_diagnostic:state(), nowarn_bif_clash=[], %All no warn bif clashes (ordset). - errors=[] :: [{file:filename(),error_info()}], %Current errors - warnings=[] :: [{file:filename(),error_info()}], %Current warnings file = "" :: string(), %From last file attribute recdef_top=false :: boolean(), %true in record initialisation %outside any fun or lc @@ -746,12 +743,19 @@ module(Forms, FileName, Opts0) -> %% FIXME Hmm, this is not coherent with the semantics of features %% We want the options given on the command line to take %% precedence over options in the module. - Opts = Opts0 ++ compiler_options(Forms), + Opts = Opts0 ++ [O || {O, _} <- compiler_options(Forms)], St = forms(Forms, start(FileName, Opts)), return_status(St). +-doc false. +module(Forms, FileName, Opts0, Ds) -> + OptionsSource = proplists:get_value(options_source, Opts0), + Opts = [{O, OptionsSource} || O <- Opts0] ++ compiler_options(eval_file_attribute(Forms, #lint{file = FileName})), + St = forms(Forms, start1(FileName, Opts, Ds)), + St#lint.diagnostics. + compiler_options(Forms) -> - lists:flatten([C || {attribute,_,compile,C} <- Forms]). + lists:flatten([[{O, A} || O <- lists:flatten([Os])] || {attribute,A,compile,Os} <- Forms]). %% start() -> State %% start(FileName, [Option]) -> State @@ -760,10 +764,12 @@ start() -> start("nofile", []). start(File, Opts) -> - Enabled0 = [Category || {Category,true} <- bool_options()], - Enabled1 = ordsets:from_list(Enabled0), - Enabled = parse_options(Opts, Enabled1), - Calls = case ordsets:is_element(unused_function, Enabled) of + start1(File, [{O, undefined} || O <- Opts], + erl_diagnostic:init(#{})). +start1(File, Opts, InitialDiag) -> + Diag = erl_diagnostic:add_diagnostics(InitialDiag, diagnostics()), + D = parse_options(Opts, Diag), + Calls = case erl_diagnostic:get_value(D, unused_function) of true -> #{{module_info,1} => pseudolocals()}; false -> @@ -771,19 +777,17 @@ start(File, Opts) -> end, #lint{state = start, exports = gb_sets:from_list([{module_info,0},{module_info,1}]), - compile = Opts, + compile = [O || {O, _} <- Opts], %% Internal pseudo-functions must appear as defined/reached. defined = gb_sets:from_list(pseudolocals()), called = [{F,0} || F <- pseudolocals()], usage = #usage{calls=Calls}, - warn_format = value_option(warn_format, 1, warn_format, 1, - nowarn_format, 0, Opts), - enabled_warnings = Enabled, + diagnostics = D, nowarn_bif_clash = nowarn_function(nowarn_bif_clash, Opts), file = File }. -parse_options([Opt0|Opts], Enabled0) when is_atom(Opt0) -> +parse_options([{Opt0, Anno}|Opts], D) when is_atom(Opt0) -> {Opt2,Enable} = case atom_to_binary(Opt0) of <<"warn_",Opt1/binary>> -> {Opt1,true}; @@ -792,104 +796,661 @@ parse_options([Opt0|Opts], Enabled0) when is_atom(Opt0) -> _ -> {none,none} end, - Opt = try - binary_to_existing_atom(Opt2) - catch - _:_ -> - [] - end, - Enabled = - maybe - true ?= is_atom(Opt), - true ?= lists:keymember(Opt, 1, bool_options()), - if - Enable -> - ordsets:add_element(Opt, Enabled0); - not Enable -> - ordsets:del_element(Opt, Enabled0) - end - else - _ -> - Enabled0 - end, - parse_options(Opts, Enabled); + try + Opt = binary_to_existing_atom(Opt2), + case erl_diagnostic:get_diagnostic(D, Opt) of + {ok, _} -> + parse_options(Opts, erl_diagnostic:push_diagnostic(D, Opt, Enable, Anno)); + undefined -> + parse_options(Opts, D) + end + catch + error:badarg -> + parse_options(Opts, D) + end; parse_options([_|Opts], Enabled) -> parse_options(Opts, Enabled); parse_options([], Enabled) -> Enabled. -bool_options() -> - [{unused_vars,true}, - {underscore_match,true}, - {export_all,true}, - {export_vars,false}, - {shadow_vars,true}, - {unused_import,false}, - {unused_function,true}, - {unused_type,true}, - {bif_clash,true}, - {unused_record,true}, - {deprecated_function,true}, - {deprecated_type,true}, - {deprecated_callback,true}, - {deprecated_catch,false}, - {obsolete_guard,true}, - {untyped_record,false}, - {missing_spec,false}, - {missing_spec_documented,false}, - {missing_spec_all,false}, - {removed,true}, - {nif_inline,true}, - {keywords,false}, - {redefined_builtin_type,true}, - {match_float_zero,true}, - {update_literal,true}, - {behaviours,true}, - {conflicting_behaviours,true}, - {undefined_behaviour_func,true}, - {undefined_behaviour,true}, - {undefined_behaviour_callbacks,true}, - {ill_defined_behaviour_callbacks,true}, - {ill_defined_optional_callbacks,true}, - {unexported_function,true}]. +diagnostics() -> + Ds = + [ + #{ key => undefined_module, type => error, + format => fun() -> + ~"no module definition" + end}, + #{ key => redefine_module, type => error, + format => fun() -> + ~"redefining module" + end}, + #{ key => pmod_unsupported, type => error, + format => fun() -> + ~"parameterized modules are no longer supported" + end}, + #{ key => non_latin, type => error, + format => fun() -> + ~"module names with non-latin1 characters are not supported" + end}, + #{ key => empty_module_name, type => error, + format => fun() -> + ~"the module name must not be empty" + end}, + #{ key => blank_module_name, type => error, + format => fun() -> + ~"the module name must contain at least one visible character" + end}, + #{ key => ctrl_chars_in_module_name, type => error, + format => fun() -> + ~"the module name must not contain control characters" + end}, + + #{ key => invalid_call, type => error, + format => fun() -> + ~"invalid function call" + end}, + #{ key => invalid_record, type => error, + format => fun() -> + ~"invalid record expression" + end}, + + #{ key => keywords, type => warning, default => false, + format => fun({Ftr, Atom}) -> + {~"atom '~p' is reserved in the experimental feature '~p'", + [Atom, Ftr]} + end}, + #{ key => attribute, type => error, + format => fun(A) -> + {~"attribute ~tw after function definitions", [A]} + end}, + #{ key => missing_qlc_hrl, type => error, + format => fun(A) -> + {~"qlc:q/~w called, but \"qlc.hrl\" not included", [A]} + end}, + #{ key => redefine_import, type => error, + format => fun({{F,A},M}) -> + {~"function ~tw/~w already imported from ~w", [F,A,M]} + end}, + #{ key => bad_inline, type => error, + format => fun({F,A}) when is_integer(A) -> + {~"inlined function ~tw/~w undefined", [F,A]}; + ({{F,A},GuessFA}) -> + {~"inlined function ~tw/~w undefined, did you mean ~s?", [F,A,format_fa(GuessFA)]} + end}, + #{ key => undefined_nif, type => error, + format => fun({F,A}) when is_integer(A) -> + {~"nif ~tw/~w undefined", [F,A]}; + ({{F,A},GuessFA}) -> + {~"nif ~tw/~w undefined, did you mean ~s?", [F,A,format_fa(GuessFA)]} + end}, + #{ key => no_load_nif, type => warning, + format => fun() -> + {~"nifs defined, but no call to erlang:load_nif/2", []} + end}, + #{ key => invalid_deprecated, type => error, + format => fun(D) -> + {~"badly formed deprecated attribute ~tw", [D]} + end}, + #{ key => bad_deprecated, type => error, + format => fun({F,A}) -> + {~"deprecated function ~tw/~w undefined or not exported", + [F,A]} + end}, + #{ key => invalid_removed, type => error, + format => fun(D) -> + {~"badly formed removed attribute ~tw", [D]} + end}, + #{ key => bad_removed, type => error, + format => fun({F,A}) when F =:= '_'; A =:= '_' -> + {~"at least one function matching ~tw/~w is still exported", [F,A]}; + ({F,A}) -> + {~"removed function ~tw/~w is still exported", [F,A]} + end}, + #{ key => bad_nowarn_unused_function, type => error, + format => fun({F,A}) when is_integer(A) -> + {~"function ~tw/~w undefined", [F,A]}; + ({{F,A},GuessFA}) -> + {~"function ~tw/~w undefined, did you mean ~s?", [F,A,format_fa(GuessFA)]} + end}, + #{ key => bad_nowarn_bif_clash, type => error, + format => fun({F,A}) when is_integer(A) -> + {~"function ~tw/~w undefined", [F,A]}; + ({{F,A},GuessFA}) -> + {~"function ~tw/~w undefined, did you mean ~s?", [F,A,format_fa(GuessFA)]} + end}, + #{ key => disallowed_nowarn_bif_clash, type => error, + format => fun() -> + ~""" + compile directive nowarn_bif_clash is no longer allowed -- + use explicit module names or -compile({no_auto_import, [F/A]}) + """ + end}, + #{ key => bad_on_load, type => error, + format => fun(Term) -> + {~"badly formed on_load attribute: ~tw", [Term]} + end}, + #{ key => multiple_on_loads, type => error, + format => fun() -> + ~"more than one on_load attribute" + end}, + #{ key => bad_on_load_arity, type => error, + format => fun({F,A}) -> + {~"function ~tw/~w has wrong arity (must be 0)", [F,A]} + end}, + #{ key => duplicate_doc_attribute, type => error, + format => fun({Tag, PrevLine}) -> + {~"redefining documentation attribute (~p) previously set at line ~p", + [Tag, PrevLine]} + end}, + #{ key => undefined_on_load, type => error, + format => fun({F,A}) when is_integer(A) -> + {~"function ~tw/~w undefined", [F,A]}; + ({{F,A},GuessF}) -> + {~"function ~tw/~w undefined, did you mean ~ts/~w?", [F,A,GuessF,A]} + end}, + #{ key => nif_inline, type => warning, + format => fun() -> + ~"inlining is enabled - local calls to NIFs may call their Erlang implementation instead" + end}, + + #{ key => export_all, type => warning, + format => fun() -> + ~"export_all flag enabled - all functions will be exported" + end}, + #{ key => duplicated_export, type => warning, + format => fun({F,A}) -> + {~"function ~tw/~w already exported", [F,A]} + end}, + #{ key => unused_import, type => warning, default => false, + format => fun({{F,A},M}) -> + {~"import ~w:~tw/~w is unused", [M,F,A]} + end}, + #{ key => undefined_function, type => error, + format => fun({F,A}) -> + {~"function ~tw/~w undefined", [F,A]} + end}, + #{ key => redefine_function, type => error, + format => fun({F,A}) -> + {~"function ~tw/~w already defined", [F,A]} + end}, + #{ key => define_import, type => error, + format => fun({F,A}) -> + {~"defining imported function ~tw/~w", [F,A]} + end}, + #{ key => fun_import, type => error, + format => fun({F,A}) -> + {~"creating a fun from imported name ~tw/~w is not allowed", [F,A]} + end}, + #{ key => unused_function, type => warning, + code => ~"ERL-1001", + format => fun({F,A}) -> + {~"function ~tw/~w is unused", [F,A]} + end}, + #{ key => unexported_function, type => warning, + format => fun(MFA) -> + {~"function ~ts is not exported", [format_mfa(MFA)]} + end}, + #{ key => bif_clash, type => warning, + format => fun({function,F,A}) -> + {~""" + ambiguous call of overridden auto-imported BIF ~w/~w -- + use erlang:~w/~w or "-compile({no_auto_import,[~w/~w]})." to resolve name clash + """, [F,A,F,A,F,A]}; + ({import,F,A}) -> + {~""" + import directive overrides auto-imported BIF ~w/~w -- + use "-compile({no_auto_import,[~w/~w]})." to resolve name clash + """, [F,A,F,A]} + end}, + #{ key => deprecated_function, type => warning, + format => fun({MFA, String, Rel}) -> + {~"~s is deprecated and will be removed in ~s; ~s", + [format_mfa(MFA), Rel, String]}; + ({MFA, String}) when is_list(String) -> + {~"~s is deprecated; ~s", [format_mfa(MFA), String]} + end}, + #{ key => deprecated_type, type => warning, + format => fun({{M1, F1, A1}, String, Rel}) -> + {~"the type ~p:~p~s is deprecated and will be removed in ~s; ~s", + [M1, F1, gen_type_paren(A1), Rel, String]}; + ({{M1, F1, A1}, String}) when is_list(String) -> + {~"the type ~p:~p~s is deprecated; ~s", + [M1, F1, gen_type_paren(A1), String]} + end}, + #{ key => deprecated_callback, type => warning, + format => fun({{M1, F1, A1}, String, Rel}) -> + {~"the callback ~p:~p~s is deprecated and will be removed in ~s; ~s", + [M1, F1, gen_type_paren(A1), Rel, String]}; + ({{M1, F1, A1}, String}) when is_list(String) -> + {~"the callback ~p:~p~s is deprecated; ~s", + [M1, F1, gen_type_paren(A1), String]} + end}, + #{ key => removed, type => warning, + format => fun({MFA, ReplacementMFA, Rel}) -> + {~"call to ~s will fail, since it was removed in ~s; use ~s", + [format_mfa(MFA), Rel, format_mfa(ReplacementMFA)]}; + ({MFA, String}) when is_list(String) -> + {~"~s is removed; ~s", [format_mfa(MFA), String]} + end}, + #{ key => removed_type, type => warning, + format => fun({MNA, String}) -> + {~"the type ~s is removed; ~s", [format_mna(MNA), String]} + end}, + #{ key => removed_callback, type => warning, + format => fun({MNA, String}) -> + {~"the callback ~s is removed; ~s", [format_mna(MNA), String]} + end}, + #{ key => obsolete_guard, type => warning, + format => fun({F, A}) -> + {~"~p/~p obsolete (use is_~p/~p)", [F, A, F, A]} + end}, + #{ key => obsolete_guard_overridden, type => error, + format => fun(Test) -> + {~""" + obsolete ~s/1 (meaning is_~s/1) is illegal when there is a + local/imported function named is_~p/1 + """, [Test,Test,Test]} + end}, + #{ key => too_many_arguments, type => error, + format => fun(Arity) -> + {~"too many arguments (~w) -- maximum allowed is ~w", [Arity,?MAX_ARGUMENTS]} + end}, + #{ key => update_literal, type => warning, + format => fun() -> + ~"expression updates a literal" + end}, + #{ key => illegal_zip_generator, type => error, + format => fun() -> + ~"only generators are allowed in a zip generator." + end}, + %% --- patterns and guards --- + #{ key => illegal_map_assoc_in_pattern, type => error, + format => fun() -> + ~"illegal pattern, did you mean to use `:=`?" + end}, + #{ key => illegal_pattern, type => error, + format => fun() -> ~"illegal pattern" + end}, + #{ key => illegal_expr, type => error, + format => fun() -> ~"illegal expression" + end}, + #{ key => illegal_guard_local_call, type => error, + format => fun({F,A}) -> + {~"call to local/imported function ~tw/~w is illegal in guard", + [F,A]} + end}, + #{ key => illegal_guard_expr, type => error, + format => fun() -> ~"illegal guard expression" + end}, + #{ key => match_float_zero, type => error, + format => fun() -> + ~""" + matching on the float 0.0 will no longer also match -0.0 in OTP 27. + If you specifically intend to match 0.0 alone, write +0.0 instead. + """ + end}, + %% --- maps --- + #{ key => illegal_map_construction, type => error, + format => fun() -> + ~"only association operators '=>' are allowed in map construction" + end}, + %% --- records --- + #{ key => undefined_record, type => error, + format => fun({T}) -> + {~"record ~tw undefined", [T]}; + ({T,GuessT}) -> + {~"record ~tw undefined, did you mean ~ts?", [T,GuessT]} + end}, + #{ key => redefine_record, type => error, + format => fun(T) -> + {~"record ~tw already defined", [T]} + end}, + #{ key => redefine_fields, type => error, + format => fun({T,F}) -> + {~"field ~tw already defined in record ~tw", [F,T]} + end}, + #{ key => bad_multi_field_init, type => error, + format => fun() -> + {~"'_' initializes no omitted fields", []} + end}, + #{ key => undefined_field, type => error, + format => fun({T,F}) -> + {~"field ~tw undefined in record ~tw", [F,T]}; + ({T,F,GuessF}) -> + {~"field ~tw undefined in record ~tw, did you mean ~ts?", [F,T,GuessF]} + end}, + #{ key => illegal_record_info, type => error, + format => fun() -> + ~"illegal record info" + end}, + #{ key => field_name_is_variable, type => error, + format => fun({T,F}) -> + {~"field ~tw is not an atom or _ in record ~tw", [F,T]} + end}, + #{ key => wildcard_in_update, type => error, + format => fun(T) -> + {~"meaningless use of _ in update of record ~tw", [T]} + end}, + #{ key => unused_record, type => warning, + format => fun(T) -> + {~"record ~tw is unused", [T]} + end}, + #{ key => untyped_record, type => warning, default => false, + format => fun(T) -> + {~"record ~tw has field(s) without type information", [T]} + end}, + %% --- variables ---- + #{ key => unbound_var, type => error, + format => fun(V) when not is_tuple(V) -> + {~"variable ~w is unbound", [V]}; + ({V,GuessV}) -> + {~"variable ~w is unbound, did you mean '~s'?", [V,GuessV]} + end}, + #{ key => unsafe_var, type => error, + format => fun({V,{What,Where}}) -> + {~"variable ~w unsafe in ~w ~s", + [V,What,format_where(Where)]}; + (V) -> + {~"variable ~w unsafe",[V]} + end}, + #{ key => export_vars, type => warning, default => false, + format => fun({V,{What,Where}}) -> + {~"variable ~w exported from ~w ~s", + [V,What,format_where(Where)]} + end}, + #{ key => underscore_match, type => warning, + format => fun({var,V}) -> + {~""" + variable ~w is already bound. If you mean to ignore this + value, use '_' or a different underscore-prefixed name + """, [V]}; + ({var_pat, V}) -> + {~""" + variable ~w is bound multiple times in this pattern. + If you mean to ignore this value, use '_' or + a different underscore-prefixed name + """, [V]} + end}, + #{ key => shadow_vars, type => warning, + format => fun({V,In}) -> + {~"variable ~w shadowed in ~w", [V,In]} + end}, + #{ key => unused_vars, type => warning, + format => fun(V) -> + {~"variable ~w is unused", [V]} + end}, + #{ key => variable_in_record_def, type => error, + format => fun(V) -> + {~"variable ~w in record definition", [V]} + end}, + #{ key => stacktrace_guard, type => error, + format => fun(V) -> + {~"stacktrace variable ~w must not be used in a guard", [V]} + end}, + #{ key => stacktrace_bound, type => error, + format => fun(V) -> + {~"stacktrace variable ~w must not be previously bound", [V]} + end}, + %% --- binaries --- + #{ key => undefined_bittype, type => error, + format => fun(Type) -> + {~"bit type ~tw undefined", [Type]} + end}, + #{ key => bittype_mismatch, type => error, + format => fun({Val1,Val2,What}) -> + {~"conflict in ~s specification for bit field: '~p' and '~p'", + [What,Val1,Val2]} + end}, + #{ key => bittype_unit, type => error, + format => fun() -> + ~"a bit unit size must not be specified unless a size is specified too" + end}, + #{ key => illegal_bitsize, type => error, + format => fun() -> + ~"illegal bit size" + end}, + #{ key => illegal_bitsize_local_call, type => error, + format => fun({F,A}) -> + {~""" + call to local/imported function ~tw/~w is illegal + in a size expression for a binary segment + """, [F,A]} + end}, + #{ key => non_integer_bitsize, type => error, + format => fun() -> + ~""" + a size expression in a pattern evaluates to a non-integer value; + this pattern cannot possibly match + """ + end}, + #{ key => unsized_binary_not_at_end, type => error, + format => fun() -> + ~"a binary field without size is only allowed at the end of a binary pattern" + end}, + #{ key => typed_literal_string, type => error, + format => fun() -> + ~"a literal string in a binary pattern must not have a type or a size" + end}, + #{ key => utf_bittype_size_or_unit, type => error, + format => fun() -> + ~"neither size nor unit must be given for segments of type utf8/utf16/utf32" + end}, + #{ key => bad_bitsize, type => error, + format => fun(Type) -> + {~"bad ~s bit size", [Type]} + end}, + #{ key => unsized_binary_in_bin_gen_pattern, type => error, + format => fun() -> + ~"binary fields without size are not allowed in patterns of bit string generators" + end}, + %% --- behaviours --- + #{ key => behaviours, type => warning}, + #{ key => conflicting_behaviours, type => warning, + format => fun({{Name,Arity},B,FirstL,FirstB}) -> + {~"conflicting behaviours -- callback ~tw/~w required by both '~p' and '~p' ~s", + [Name,Arity,B,FirstB,format_where(FirstL)]} + end}, + #{ key => undefined_behaviour_func, type => warning, + format => fun({{Func,Arity}, Behaviour}) -> + {~"undefined callback function ~tw/~w (behaviour '~w')", + [Func,Arity,Behaviour]} + end}, + #{ key => undefined_behaviour, type => warning, + format => fun(Behaviour) -> + {~"behaviour ~tw undefined", [Behaviour]} + end}, + #{ key => undefined_behaviour_callbacks, type => warning, + format => fun(Behaviour) -> + {~"behaviour ~w callback functions are undefined", + [Behaviour]} + end}, + #{ key => ill_defined_behaviour_callbacks, type => error, + format => fun(Behaviour) -> + {~"behaviour ~w callback functions erroneously defined", + [Behaviour]} + end}, + #{ key => ill_defined_optional_callbacks, type => error, + format => fun(Behaviour) -> + {~"behaviour ~w optional callback functions erroneously defined", + [Behaviour]} + end}, + #{ key => behaviour_info, type => error, + format => fun({_M,F,A}) -> + {~"cannot define callback attibute for ~tw/~w when behaviour_info is defined", + [F,A]} + end}, + #{ key => redefine_optional_callback, type => error, + format => fun({F, A}) -> + {~"optional callback ~tw/~w duplicated", [F, A]} + end}, + #{ key => undefined_callback, type => error, + format => fun({_M, F, A}) -> + {~"callback ~tw/~w is undefined", [F, A]} + end}, + %% --- types and specs --- + #{ key => singleton_typevar, type => error, + format => fun(Name) -> + {~"type variable ~w is only used once (is unbound)", [Name]} + end}, + #{ key => bad_export_type, type => error, + format => fun(_ETs) -> + {~"bad export_type declaration", []} + end}, + #{ key => duplicated_export_type, type => warning, + format => fun({T, A}) -> + {~"type ~tw/~w already exported", [T, A]} + end}, + #{ key => undefined_type, type => error, + format => fun({TypeName, Arity}) -> + {~"type ~tw~s undefined", [TypeName, gen_type_paren(Arity)]} + end}, + #{ key => unused_type, type => warning, + format => fun({TypeName, Arity}) -> + {~"type ~tw~s is unused", [TypeName, gen_type_paren(Arity)]} + end}, + #{ key => redefine_builtin_type, type => warning, + format => fun({TypeName, Arity}) -> + {~"local redefinition of built-in type: ~w~s", + [TypeName, gen_type_paren(Arity)]} + end}, + #{ key => renamed_type, type => warning, + format => fun({OldName, NewName}) -> + {~"type ~w() is now called ~w(); please use the new name instead", + [OldName, NewName]} + end}, + #{ key => redefine_type, type => error, + format => fun({TypeName, Arity}) -> + {~"type ~tw~s already defined", + [TypeName, gen_type_paren(Arity)]} + end}, + #{ key => type_syntax, type => error, + format => fun(Constr) -> + {~"bad ~tw type", [Constr]} + end}, + #{ key => old_abstract_code, type => error, + format => fun() -> + ~""" + abstract code generated before Erlang/OTP 19.0 and + having typed record fields cannot be compiled + """ + end}, + #{ key => redefine_spec, type => error, + format => fun({M, F, A}) -> + {~"spec for ~tw:~tw/~w already defined", [M, F, A]}; + ({F, A}) -> + {~"spec for ~tw/~w already defined", [F, A]} + end}, + #{ key => redefine_callback, type => error, + format => fun({F, A}) -> + {~"callback ~tw/~w already defined", [F, A]} + end}, + #{ key => bad_callback, type => error, + format => fun({M, F, A}) -> + {~"explicit module not allowed for callback ~tw:~tw/~w", + [M, F, A]} + end}, + #{ key => bad_module, type => error, + format => fun({M, F, A}) -> + {~"spec for function ~w:~tw/~w from other module", [M, F, A]} + end}, + #{ key => spec_fun_undefined, type => error, + format => fun({F, A}) -> + {~"spec for undefined function ~tw/~w", [F, A]} + end}, + #{ key => missing_spec, type => warning, default => false, + code => ~"ERL-1002", + format => fun({F,A}) -> + {~"missing specification for function ~tw/~w", [F, A]} + end}, + #{ key => missing_spec_documented, type => warning, default => false, + code => ~"ERL-1002", + format => fun({F,A}) -> + {~"missing specification for function ~tw/~w", [F, A]} + end}, + #{ key => missing_spec_all, type => warning, default => false, + code => ~"ERL-1002", + format => fun({F,A}) -> + {~"missing specification for function ~tw/~w", [F, A]} + end}, + #{ key => spec_wrong_arity, type => error, + format => fun() -> + ~"spec has wrong arity" + end}, + #{ key => callback_wrong_arity, type => error, + format => fun() -> + ~"callback has wrong arity" + end}, + #{ key => deprecated_builtin_type, type => warning, + format => fun({{Name, Arity}, Replacement, Rel}) -> + UseS = case Replacement of + {Mod, NewName} -> + io_lib:format(~"use ~w:~w/~w", [Mod, NewName, Arity]); + {Mod, NewName, NewArity} -> + io_lib:format(~"use ~w:~w/~w or preferably ~w:~w/~w", + [Mod, NewName, Arity, + Mod, NewName, NewArity]) + end, + {~"type ~w/~w is deprecated and will be removed in ~s; use ~s", + [Name, Arity, Rel, UseS]} + end}, + #{ key => deprecated_catch, type => warning, default => false, + format => fun() -> + ~""" + 'catch ...' is deprecated and will be removed in a + future version of Erlang/OTP; please use 'try ... catch ... end' instead. + Compile directive 'nowarn_deprecated_catch' can be used to suppress + warnings in selected modules. + """ + end}, + #{ key => not_exported_opaque, type => error, + format => fun({TypeName, Arity}) -> + {~"opaque type ~tw~s is not exported", + [TypeName, gen_type_paren(Arity)]} + end}, + #{ key => bad_dialyzer_attribute, type => error, + format => fun(Term) -> + {~"badly formed dialyzer attribute: ~tw", [Term]} + end}, + #{ key => bad_dialyzer_option, type => error, + format => fun(Term) -> + {~"unknown dialyzer warning option: ~tw", [Term]} + end}, + #{ key => warn_format, type => warning, default => 1, + format => fun({Fmt, Args}) -> + {Fmt, Args} + end} + ], + lists:map( + fun(#{ key := Key, type := warning } = W) -> + W#{ on => <<"warn_",(atom_to_binary(Key))/binary>>, + off => <<"nowarn_",(atom_to_binary(Key))/binary>>}; + (Error) -> + Error + end, Ds). %% is_warn_enabled(Category, St) -> boolean(). %% Check whether a warning of category Category is enabled. -is_warn_enabled(Type, #lint{enabled_warnings=Enabled}) -> - ordsets:is_element(Type, Enabled). +is_warn_enabled(Type, #lint{diagnostics = D}) -> + erl_diagnostic:get_value(D, Type). %% return_status(State) -> %% {ok,[Warning]} | {error,[Error],[Warning]} %% Pack errors and warnings properly and return ok | error. return_status(St) -> - Ws = pack_warnings(St#lint.warnings), - case pack_errors(St#lint.errors) of + Ws = erl_diagnostic:get_warnings(St#lint.diagnostics), + case erl_diagnostic:get_errors(St#lint.diagnostics) of [] -> {ok,Ws}; Es -> {error,Es,Ws} end. -%% pack_errors([{File,ErrD}]) -> [{File,[ErrD]}]. -%% Sort on (reversed) insertion order. - -pack_errors(Es) -> - {Es1,_} = mapfoldl(fun ({File,E}, I) -> {{File,{I,E}}, I-1} end, -1, Es), - map(fun ({File,EIs}) -> {File, map(fun ({_I,E}) -> E end, EIs)} end, - pack_warnings(Es1)). - -%% pack_warnings([{File,ErrD}]) -> [{File,[ErrD]}] -%% Sort on line number. - -pack_warnings(Ws) -> - [{File,lists:sort([W || {F,W} <- Ws, F =:= File])} || - File <- lists:usort([F || {F,_} <- Ws])]. - %% add_error(ErrorDescriptor, State) -> State' %% add_error(Anno, Error, State) -> State' %% add_warning(ErrorDescriptor, State) -> State' %% add_warning(Anno, Error, State) -> State' -add_error(E, St) -> add_lint_error(E, St#lint.file, St). +add_error(E, St) -> + add_lint_diag(E, erl_anno:new(0), St). add_error(Anno, E0, #lint{gexpr_context=Context}=St) -> E = case {E0,Context} of @@ -899,38 +1460,26 @@ add_error(Anno, E0, #lint{gexpr_context=Context}=St) -> {illegal_bitsize_local_call,FA}; {_,_} -> E0 end, - {File,Location} = loc(Anno, St), - add_lint_error({Location,erl_lint,E}, File, St). - -add_lint_error(E, File, St) -> - St#lint{errors=[{File,E}|St#lint.errors]}. + add_lint_diag(E, Anno, St). -add_warning(W, St) -> add_lint_warning(W, St#lint.file, St). +add_error(Anno, Key, Args, Opts, St) -> + D = erl_diagnostic:emit(St#lint.diagnostics, Key, loc(Anno, St), Args, Opts), + St#lint{ diagnostics = D }. add_warning(Anno, W, St) -> - {File,Location} = loc(Anno, St), - add_lint_warning({Location,erl_lint,W}, File, St). + add_lint_diag(W, Anno, St). -add_lint_warning(W, File, St) -> - St#lint{warnings=[{File,W}|St#lint.warnings]}. - -maybe_add_warning(Anno, W, St) -> - Tag = if - is_tuple(W) -> element(1, W); - is_atom(W) -> W - end, - case is_warn_enabled(Tag, St) of - true -> - add_warning(Anno, W, St); - false -> - St - end. +add_lint_diag({Key, Arg}, Anno, St) when not is_list(Arg) -> + D = erl_diagnostic:emit(St#lint.diagnostics, Key, loc(Anno, St), [Arg]), + St#lint{ diagnostics = D }; +add_lint_diag(Key, Anno, St) when is_atom(Key) -> + D = erl_diagnostic:emit(St#lint.diagnostics, Key, loc(Anno, St)), + St#lint{ diagnostics = D }. loc(Anno, St) -> - Location = erl_anno:location(Anno), case erl_anno:file(Anno) of - undefined -> {St#lint.file,Location}; - File -> {File,Location} + undefined -> erl_anno:set_file(St#lint.file, Anno); + _File -> Anno end. %% forms([Form], State) -> State' @@ -942,7 +1491,7 @@ forms(Forms0, St0) -> AutoImportSuppressed = auto_import_suppressed(St0#lint.compile), StDeprecated = disallowed_compile_flags(Forms,St0), St1 = includes_qlc_hrl(Forms, StDeprecated#lint{locals = Locals, - no_auto = AutoImportSuppressed}), + no_auto = AutoImportSuppressed}), St2 = bif_clashes(Forms, St1), St3 = not_deprecated(Forms, St2), St4 = not_removed(Forms, St3), @@ -1002,8 +1551,9 @@ anno_set_file(T, File) -> %% form(Form, State) -> State' %% Check a form returning the updated State. Handle generic cases here. -form({error,E}, St) -> add_error(E, St); -form({warning,W}, St) -> add_warning(W, St); +form({Type,{Anno, Mod, Reason}}, St) when Type =:= error; Type =:= warning -> + NewD = erl_diagnostic:emit_legacy(St#lint.diagnostics, Type, loc(Anno, St), Mod, Reason), + St#lint{ diagnostics = NewD }; form({attribute,_A,file,{File,_Line}}, St) -> St#lint{file = File}; form({attribute,_A,compile,_}, St) -> @@ -1212,22 +1762,21 @@ not_removed(Forms, #lint{compile=Opts}=St0) -> %% The nowarn_bif_clash directive is not only deprecated, it's actually an error from R14A disallowed_compile_flags(Forms, St0) -> %% There are (still) no line numbers in St0#lint.compile. - Errors0 = [ {St0#lint.file,{L,erl_lint,disallowed_nowarn_bif_clash}} || - {attribute,A,compile,nowarn_bif_clash} <- Forms, - {_,L} <- [loc(A, St0)] ], - Errors1 = [ {St0#lint.file,{L,erl_lint,disallowed_nowarn_bif_clash}} || - {attribute,A,compile,{nowarn_bif_clash, {_,_}}} <- Forms, - {_,L} <- [loc(A, St0)] ], + Errors0 = [ A || {attribute,A,compile,nowarn_bif_clash} <- Forms], + Errors1 = [ A || {attribute,A,compile,{nowarn_bif_clash, {_,_}}} <- Forms], Disabled = (not is_warn_enabled(bif_clash, St0)), - Errors = if - Disabled andalso Errors0 =:= [] -> - [{St0#lint.file,{erl_lint,disallowed_nowarn_bif_clash}} | St0#lint.errors]; - Disabled -> - Errors0 ++ Errors1 ++ St0#lint.errors; - true -> - Errors1 ++ St0#lint.errors - end, - St0#lint{errors=Errors}. + if + Disabled andalso Errors0 =:= [] -> + add_error(disallowed_nowarn_bif_clash, St0); + Disabled -> + lists:foldl(fun(A, St) -> + add_error(A, disallowed_nowarn_bif_clash, St) + end, St0, Errors0 ++ Errors1); + true -> + lists:foldl(fun(A, St) -> + add_error(A, disallowed_nowarn_bif_clash, St) + end, St0, Errors1) + end. %% post_traversal_check(Forms, State0) -> State. %% Do some further checking after the forms have been traversed and @@ -1288,13 +1837,10 @@ all_behaviour_callbacks([{Anno,B}|Bs], Acc, St0) -> all_behaviour_callbacks(Bs, [{{Anno,B},Bfs0,OBfs0}|Acc], St); all_behaviour_callbacks([], Acc, St) -> {reverse(Acc),St}. -add_behaviour_warning(Anno, Warning, St) when is_tuple(Warning) -> - maybe_add_warning(Anno, Warning, St). - behaviour_callbacks(Anno, B, St0) -> try B:behaviour_info(callbacks) of undefined -> - St1 = add_behaviour_warning(Anno, + St1 = add_warning(Anno, {undefined_behaviour_callbacks, B}, St0), {[], [], St1}; @@ -1312,7 +1858,7 @@ behaviour_callbacks(Anno, B, St0) -> {Funcs, OptFuncs, St0}; false -> W = {ill_defined_optional_callbacks, B}, - St1 = add_behaviour_warning(Anno, W, St0), + St1 = add_warning(Anno, W, St0), {Funcs, [], St1} end catch @@ -1320,14 +1866,14 @@ behaviour_callbacks(Anno, B, St0) -> {Funcs, [], St0} end; false -> - St1 = add_behaviour_warning(Anno, + St1 = add_warning(Anno, {ill_defined_behaviour_callbacks, B}, St0), {[], [], St1} end catch _:_ -> - St1 = add_behaviour_warning(Anno, {undefined_behaviour, B}, St0), + St1 = add_warning(Anno, {undefined_behaviour, B}, St0), St2 = check_module_name(B, Anno, St1), {[], [], St2} end. @@ -1347,9 +1893,9 @@ behaviour_deprecated(Anno, B, [{F, A} | T], Exports, St0) -> true -> case otp_internal:obsolete_callback(B, F, A) of {deprecated, String} when is_list(String) -> - maybe_add_warning(Anno, {deprecated_callback, {B, F, A}, String}, St0); + add_warning(Anno, {deprecated_callback, {{B, F, A}, String}}, St0); {removed, String} -> - add_warning(Anno, {removed_callback, {B, F, A}, String}, St0); + add_warning(Anno, {removed_callback, {{B, F, A}, String}}, St0); no -> St0 end @@ -1366,7 +1912,7 @@ behaviour_missing_callbacks([{{Anno,B},Bfs0,OBfs}|T], St0) -> case is_fa(F) of true -> M = {undefined_behaviour_func,F,B}, - add_behaviour_warning(Anno, M, S0); + add_warning(Anno, M, S0); false -> S0 % ill_defined_behaviour_callbacks end @@ -1384,15 +1930,13 @@ behaviour_conflicting(AllBfs, St) -> behaviour_add_conflicts(R, St). behaviour_add_conflicts([{Cb,[{FirstAnno,FirstB}|Cs]}|T], St0) -> - FirstL = element(2, loc(FirstAnno, St0)), + FirstL = erl_anno:line(loc(FirstAnno, St0)), St = behaviour_add_conflict(Cs, Cb, FirstL, FirstB, St0), behaviour_add_conflicts(T, St); behaviour_add_conflicts([], St) -> St. behaviour_add_conflict([{Anno,B}|Cs], Cb, FirstL, FirstB, St0) -> - St = add_behaviour_warning(Anno, - {conflicting_behaviours,Cb,B,FirstL,FirstB}, - St0), + St = add_warning(Anno,{conflicting_behaviours,Cb,B,FirstL,FirstB},St0), behaviour_add_conflict(Cs, Cb, FirstL, FirstB, St); behaviour_add_conflict([], _, _, _, St) -> St. @@ -1681,7 +2225,7 @@ check_unexported_functions_1({F, A}=Key, Annos, Acc0) -> end. nowarn_function(Tag, Opts) -> - ordsets:from_list([FA || {Tag1,FAs} <- Opts, + ordsets:from_list([FA || {{Tag1,FAs},_} <- Opts, Tag1 =:= Tag, FA <- lists:flatten([FAs])]). @@ -1693,17 +2237,11 @@ func_location_error(Type, [{F,Anno}|Fs], St0, FAList) -> PossibleAs = lists:sort([A || {FName, A} <:- FAList, FName =:= Name]), case PossibleAs of [] -> - PossibleFs = [atom_to_list(Func) || - {Func, A} <:- FAList, A =:= Arity], - St1 = case most_possible_string(Name, PossibleFs) of - [] -> - add_error(Anno, {Type,F}, St0); - GuessF -> - add_error(Anno, {Type,F,{GuessF,[Arity]}}, St0) - end, + PossibleFs = [Func || {Func, A} <:- FAList, A =:= Arity], + St1 = add_error(Anno, Type, [F], #{ possible => {Name, PossibleFs} }, St0), func_location_error(Type, Fs, St1, FAList); _ -> - St1 = add_error(Anno, {Type,F,{Name,PossibleAs}}, St0), + St1 = add_error(Anno, {Type,{F,{Name,PossibleAs}}}, St0), func_location_error(Type, Fs, St1, FAList) end; func_location_error(_, [], St, _) -> @@ -1753,7 +2291,7 @@ check_unused_records(Forms, St0) -> end, St1#lint.records, UsedRecords), Unused = [{Name,Anno} || Name := {Anno,_Fields} <- URecs, - element(1, loc(Anno, St1)) =:= FirstFile], + erl_anno:file(loc(Anno, St1)) =:= FirstFile], foldl(fun ({N,Anno}, St) -> add_warning(Anno, {unused_record, N}, St) end, St1, Unused); @@ -1860,7 +2398,7 @@ import(Anno, {Mod,Fs}, St00) -> Warn and (not AutoImpSup) -> add_warning (Anno, - {redefine_bif_import, {F,A}}, + {bif_clash, {import, {F,A}}}, St0); true -> St0 @@ -2046,7 +2584,7 @@ pattern({integer,_Anno,_I}, _Vt, _Old, St) -> {[],[],St}; pattern({float,Anno,F}, _Vt, _Old, St0) -> St = if F == 0 -> - maybe_add_warning(Anno, match_float_zero, St0); + add_warning(Anno, match_float_zero, St0); true -> St0 end, @@ -2934,7 +3472,7 @@ check_call(Anno, F, As, _Aa, St0) -> false ?= AutoSuppressed, true ?= is_warn_enabled(bif_clash, St0), false ?= bif_clash_specifically_disabled(St0, {F,A}), - add_warning(Anno, {call_to_redefined_bif, {F,A}}, St0) + add_warning(Anno, {bif_clash, {function,F,A}}, St0) else _ -> St0 @@ -2955,7 +3493,7 @@ check_call(Anno, F, As, _Aa, St0) -> expr_check_match_zero({float,Anno,F}, St) -> if F == 0 -> - maybe_add_warning(Anno, match_float_zero, St); + add_warning(Anno, match_float_zero, St); true -> St end; @@ -3096,13 +3634,13 @@ def_fields(Fs0, Name, St0) -> {_,St2} = expr(V, [], St1), %% Warnings and errors found are kept, but %% updated calls, records, etc. are discarded. - St3 = St1#lint{warnings = St2#lint.warnings, - errors = St2#lint.errors, + St3 = St1#lint{diagnostics = St2#lint.diagnostics, called = St2#lint.called, recdef_top = false}, %% This is one way of avoiding a loop for %% "recursive" definitions. - NV = case St2#lint.errors =:= St1#lint.errors of + NV = case erl_diagnostic:get_errors(St2#lint.diagnostics) + =:= erl_diagnostic:get_errors(St1#lint.diagnostics) of true -> V; false -> {atom,Aa,undefined} end, @@ -3277,10 +3815,9 @@ init_fields(Ifs, Anno, Name, Dfs, Vt0, St0) -> ginit_fields(Ifs, Anno, Name, Dfs, Vt0, St0) -> {Vt1,St1} = check_fields(Ifs, Name, Dfs, Vt0, St0, fun gexpr/3), Defs = init_fields(Ifs, Anno, Dfs), - St2 = St1#lint{errors = []}, - {_,St3} = check_fields(Defs, Name, Dfs, Vt1, St2, fun gexpr/3), - #lint{usage = Usage, errors = IllErrors} = St3, - St4 = St1#lint{usage = Usage, errors = IllErrors ++ St1#lint.errors}, + {_,St3} = check_fields(Defs, Name, Dfs, Vt1, St1, fun gexpr/3), + #lint{usage = Usage, diagnostics = IllDiag} = St3, + St4 = St1#lint{usage = Usage, diagnostics = IllDiag}, {Vt1,St4}. %% Default initializations to be carried out @@ -3750,15 +4287,15 @@ check_specs_without_function(#lint{module=Mod,defined=Funcs,specs=Specs}=St) -> check_functions_without_spec(Forms, St0) -> case is_warn_enabled(missing_spec_all, St0) of true -> - add_missing_spec_warnings(Forms, St0, all); + add_missing_spec_warnings(Forms, St0, missing_spec_all); false -> case is_warn_enabled(missing_spec, St0) of true -> - add_missing_spec_warnings(Forms, St0, exported); + add_missing_spec_warnings(Forms, St0, missing_spec); false -> case is_warn_enabled(missing_spec_documented, St0) of true -> - add_missing_spec_warnings(Forms, St0, documented); + add_missing_spec_warnings(Forms, St0, missing_spec_documented); false -> St0 end @@ -3769,15 +4306,15 @@ add_missing_spec_warnings(Forms, St0, Type) -> Specs = [{F,A} || {_M,F,A} <- maps:keys(St0#lint.specs)], Warns = %% functions + line numbers for which we should warn case Type of - all -> + missing_spec_all -> [{FA,Anno} || {function,Anno,F,A,_} <- Forms, not lists:member(FA = {F,A}, Specs)]; _ -> Exps0 = gb_sets:to_list(exports(St0)) -- pseudolocals(), Exps1 = - if Type =:= documented -> + if Type =:= missing_spec_documented -> Exps0 -- sets:to_list(St0#lint.hidden_docs); - true -> + Type =:= missing_spec -> Exps0 end, Exps = Exps1 -- Specs, @@ -3785,7 +4322,7 @@ add_missing_spec_warnings(Forms, St0, Type) -> member(FA = {F,A}, Exps)] end, foldl(fun ({FA,Anno}, St) -> - add_warning(Anno, {missing_spec,FA}, St) + add_warning(Anno, {Type,FA}, St) end, St0, Warns). check_unused_types(Forms, St) -> @@ -3803,8 +4340,8 @@ check_unused_types_1(Forms, #lint{types=Ts}=St) -> fun({{record, _}=_Type, 0}, _, AccSt) -> AccSt; % Before Erlang/OTP 19.0 (Type, #typeinfo{anno = Anno}, AccSt) -> - case loc(Anno, AccSt) of - {FirstFile, _} -> + case erl_anno:file(loc(Anno, AccSt)) of + FirstFile -> case gb_sets:is_member(Type, UsedTypes) of true -> AccSt; false -> @@ -3972,7 +4509,7 @@ taint_stack_var(_, Vt, St) -> {Vt, St}. icrt_export(Vts, Vt, {Tag,Attrs}, St) -> - {_File,Loc} = loc(Attrs, St), + Loc = erl_anno:location(loc(Attrs, St)), icrt_export(lists:merge(Vts), Vt, {Tag,Loc}, length(Vts), []). icrt_export([{V,{{export,_},_,_}}|Vs0], [{V,{{export,_}=S0,_,As}}|Vt], @@ -4299,12 +4836,12 @@ pat_var(V, Anno, Vt, New, St0) -> {[{V,{bound,used,Ls}}],[],St}; {ok,{{unsafe,In},_Usage,Ls}} -> {[{V,{bound,used,Ls}}],[], - add_error(Anno, {unsafe_var,V,In}, St0)}; + add_error(Anno, {unsafe_var,{V,In}}, St0)}; {ok,{{export,From},_Usage,Ls}} -> St = warn_underscore_match(V, Anno, St0), {[{V,{bound,used,Ls}}],[], %% As this is matching, exported vars are risky. - add_warning(Anno, {exported_var,V,From}, St)}; + add_warning(Anno, {exported_var,{V,From}}, St)}; error when St0#lint.recdef_top -> {[],[{V,{bound,unused,[Anno]}}], add_error(Anno, {variable_in_record_def,V}, St0)}; @@ -4354,7 +4891,7 @@ pat_binsize_var(V, Anno, Vt, New, St) -> {[{V,{bound,used,As}}],[],St}; {ok,{{unsafe,In},_Used,As}} -> {[{V,{bound,used,As}}],[], - add_error(Anno, {unsafe_var,V,In}, St)}; + add_error(Anno, {unsafe_var,{V,In}}, St)}; {ok,{{export,From},_Used,As}} -> {[{V,{bound,used,As}}],[], %% As this is not matching, exported vars are @@ -4368,7 +4905,7 @@ pat_binsize_var(V, Anno, Vt, New, St) -> add_error(Anno, {unbound_var,V}, St)}; GuessV -> {[{V,{bound,used,[Anno]}}],[], - add_error(Anno, {unbound_var,V,GuessV}, St)} + add_error(Anno, {unbound_var,{V,GuessV}}, St)} end end end. @@ -4392,14 +4929,17 @@ do_expr_var(V, Anno, Vt, St) -> case orddict:find(V, Vt) of {ok,{bound,_Usage,As}} -> {[{V,{bound,used,As}}],St}; - {ok,{{unsafe,In},_Usage,As}} -> + {ok,{{unsafe,{What, Where}},_Usage,As}} -> {[{V,{bound,used,As}}], - add_error(Anno, {unsafe_var,V,In}, St)}; + St#lint{ diagnostics = + erl_diagnostic:emit(St#lint.diagnostics, unsafe_var, loc(Anno, St), + [V], + #{ related => [{loc(Where, St), io_lib:format("in ~w",[What])}]})}}; {ok,{{export,From},_Usage,As}} -> case is_warn_enabled(export_vars, St) of true -> {[{V,{bound,used,As}}], - add_warning(Anno, {exported_var,V,From}, St)}; + add_warning(Anno, {export_vars,{V,From}}, St)}; false -> {[{V,{{export,From},used,As}}],St} end; @@ -4414,13 +4954,13 @@ do_expr_var(V, Anno, Vt, St) -> add_error(Anno, {unbound_var,V}, St)}; GuessV -> {[{V,{bound,used,[Anno]}}], - add_error(Anno, {unbound_var,V,GuessV}, St)} + add_error(Anno, {unbound_var,{V,GuessV}}, St)} end end. exported_var(Anno, V, From, St) -> case is_warn_enabled(export_vars, St) of - true -> add_warning(Anno, {exported_var,V,From}, St); + true -> add_warning(Anno, {export_vars,{V,From}}, St); false -> St end. @@ -4428,7 +4968,7 @@ shadow_vars(Vt, Vt0, In, St0) -> case is_warn_enabled(shadow_vars, St0) of true -> foldl(fun ({V,{_,_,[A | _]}}, St) -> - add_warning(A, {shadowed_var,V,In}, St); + add_warning(A, {shadow_vars,{V,In}}, St); (_, St) -> St end, St0, vtold(Vt, vt_no_unsafe(Vt0))); false -> St0 @@ -4460,7 +5000,7 @@ warn_unused_vars(U, Vt, St0) -> true -> foldl(fun ({V,{_,unused,As}}, St) -> foldl(fun (A, St2) -> - add_warning(A, {unused_var,V}, + add_warning(A, {unused_vars,V}, St2) end, St, As) end, St0, U) @@ -4689,12 +5229,12 @@ deprecated_function(Anno, M, F, As, St) -> true -> St; false -> - add_warning(Anno, {deprecated, MFA, Replacement, Rel}, St) + add_warning(Anno, {deprecated_function, MFA, Replacement, Rel}, St) end; {removed, String} when is_list(String) -> - add_removed_warning(Anno, MFA, {removed, MFA, String}, St); + add_removed_warning(Anno, MFA, {removed_function, MFA, String}, St); {removed, Replacement, Rel} -> - add_removed_warning(Anno, MFA, {removed, MFA, Replacement, Rel}, St); + add_removed_warning(Anno, MFA, {removed_function, MFA, Replacement, Rel}, St); no -> St end. @@ -4715,7 +5255,7 @@ deprecated_type(Anno, M, N, As, St) -> NAs = length(As), case otp_internal:obsolete_type(M, N, NAs) of {deprecated, String} when is_list(String) -> - maybe_add_warning(Anno, {deprecated_type, {M,N,NAs}, String}, St); + add_warning(Anno, {deprecated_type, {M,N,NAs}, String}, St); {removed, String} -> add_warning(Anno, {removed_type, {M,N,NAs}, String}, St); no -> @@ -4728,7 +5268,7 @@ obsolete_guard({call,Anno,{atom,Ar,F},As}, St0) -> false -> deprecated_function(Anno, erlang, F, As, St0); true -> - St = maybe_add_warning(Ar, {obsolete_guard, {F, Arity}}, St0), + St = add_warning(Ar, {obsolete_guard, {F, Arity}}, St0), test_overriden_by_local(Ar, F, Arity, St) end; obsolete_guard(_G, St) -> @@ -4780,13 +5320,14 @@ keyword_warning(Anno, Atom, St) -> format_function(DefAnno, M, F, As, St) -> maybe true ?= is_format_function(M, F), - Lev = St#lint.warn_format, + Lev = erl_diagnostic:get_value(St#lint.diagnostics, warn_format), + true = 0 =< Lev andalso Lev =< 3, true ?= Lev > 0, case check_format_1(As) of {warn,Level,Fmt,Fas} when Level =< Lev -> - add_warning(DefAnno, {format_error,{Fmt,Fas}}, St); + add_warning(DefAnno, {warn_format,{Fmt,Fas}}, St); {warn,Level,Anno,Fmt,Fas} when Level =< Lev -> - add_warning(Anno, {format_error,{Fmt,Fas}}, St); + add_warning(Anno, {warn_format,{Fmt,Fas}}, St); _ -> St end else diff --git a/lib/stdlib/test/erl_lint_SUITE.erl b/lib/stdlib/test/erl_lint_SUITE.erl index c897eb348e4e..9240ec01d2c2 100644 --- a/lib/stdlib/test/erl_lint_SUITE.erl +++ b/lib/stdlib/test/erl_lint_SUITE.erl @@ -5826,7 +5826,17 @@ call_format_error(L) -> %% Smoke test of format_error/1 to make sure that no crashes %% slip through. _ = [Mod:format_error(Term) || {_,Mod,Term} <- L], - L. + diagnostic_to_legacy(L). + +diagnostic_to_legacy([{Anno, erl_diagnostic, {#{ diagnostic := D}, Args}} | T]) -> + #{ key := Key, format := Fmt } = D, + {module, Mod} = erlang:fun_info(Fmt, module), + As = case Args of + [] -> Key; + _ -> list_to_tuple([Key] ++ Args) + end, + [{Anno, Mod, As} | diagnostic_to_legacy(T)]; +diagnostic_to_legacy([]) -> []. print_diagnostics(Warnings, Source) -> case binary:match(Source, <<"-file(">>) of From 56a5820fdcbe8f52279d27c233ed14743ab554bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= Date: Wed, 7 May 2025 17:13:04 +0200 Subject: [PATCH 15/15] dialyzer: Start using erl_diagnostic --- lib/dialyzer/src/dialyzer.erl | 12 +++++++++--- lib/dialyzer/src/dialyzer_cl.erl | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/dialyzer/src/dialyzer.erl b/lib/dialyzer/src/dialyzer.erl index 4650e3d34332..16d1073cd5e0 100644 --- a/lib/dialyzer/src/dialyzer.erl +++ b/lib/dialyzer/src/dialyzer.erl @@ -395,7 +395,7 @@ for the function `f/0`, include the following line: To turn off warnings for improper lists, add the following line to the source file: -```text +```erlang -dialyzer(no_improper_lists). ``` @@ -448,7 +448,8 @@ line can help in assuring that no new unmatched return warnings are introduced: run_report_modules_changed_and_analyzed/1, plt_info/1, format_warning/1, - format_warning/2]). + format_warning/2, + message_to_string/1]). -include("dialyzer.hrl"). @@ -775,6 +776,11 @@ pos(Line) when is_integer(Line) -> %% categories and in more or less alphabetical ordering within each category. %%----------------------------------------------------------------------------- +-doc false. +-spec message_to_string(term()) -> unicode:chardata(). +message_to_string(Msg) -> + message_to_string(Msg, true, basename). + %%----- Warnings for general discrepancies ---------------- message_to_string({apply, [Args, ArgNs, FailReason, SigArgs, SigRet, Contract]}, I, _E) -> @@ -1158,7 +1164,7 @@ sig(Src, true) -> a(""=Args, _I) -> Args; a(Args, I) -> - t(Args, I). + string:trim(t(Args, I), leading). c(Cerl, _I) -> Cerl. diff --git a/lib/dialyzer/src/dialyzer_cl.erl b/lib/dialyzer/src/dialyzer_cl.erl index 839c526076cc..b34b870e42f9 100644 --- a/lib/dialyzer/src/dialyzer_cl.erl +++ b/lib/dialyzer/src/dialyzer_cl.erl @@ -823,14 +823,24 @@ print_warnings(#cl_state{output = Output, [_|_] -> PrWarningsId = set_warning_id(PrWarnings, EOpt), S = case Format of - formatted -> + formatted -> + D = erl_diagnostic:init(#{}), + NewD = + lists:foldl( + fun({Key, {File, Loc}, Msg}, Acc) -> + erl_diagnostic:emit(Acc, Key, erl_anno:set_file(File, erl_anno:new(Loc)), + [], #{}, #{ key => Key, type => warning, + format => fun() -> dialyzer:message_to_string(Msg) end}) + end, D, PrWarningsId), + erl_diagnostic:report(Output, NewD, ansi, #{}), + % ""; Opts = [{filename_opt, FOpt}, {indent_opt, IOpt}, {error_location, EOpt}], - [dialyzer:format_warning(W, Opts) || W <- PrWarningsId]; + [dialyzer:format_warning(W, Opts) || W <- PrWarningsId]; raw -> [io_lib:format("~tp. \n", - [W]) || W <- set_warning_id(PrWarningsId, EOpt)] + [W]) || W <- PrWarningsId] end, io:format(Output, "\n~ts", [S]) end.