diff --git a/bootstrap/lib/kernel/ebin/prim_tty.beam b/bootstrap/lib/kernel/ebin/prim_tty.beam index 1d9fbd99a1d4..3dd9fbaa6fe0 100644 Binary files a/bootstrap/lib/kernel/ebin/prim_tty.beam and b/bootstrap/lib/kernel/ebin/prim_tty.beam differ 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/erts/preloaded/ebin/init.beam b/erts/preloaded/ebin/init.beam index a64afe58c654..7327c25548c4 100644 Binary files a/erts/preloaded/ebin/init.beam and b/erts/preloaded/ebin/init.beam differ 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),[]}. 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/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/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/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/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/compile.erl b/lib/compiler/src/compile.erl index eedd993f84c1..c59584d449ff 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}. @@ -2174,24 +2185,24 @@ 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,St1#compile{warnings=St1#compile.warnings ++ Ws}}; - {error,Es,Ws} -> - {error,St#compile{warnings=St#compile.warnings ++ Ws, - errors=St#compile.errors ++ Es}} + {ok, Code, St1#compile{ diagnostics = LintDs }}; + true -> + {error,St#compile{ diagnostics = LintDs }} 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 +2353,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 +2363,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 +2395,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 +2429,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 +2437,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 +2491,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 +2621,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 +2638,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 +2709,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 +2725,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 +2742,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 +2777,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 +2868,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 +3038,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..7bcb1bd3aa59 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,12 +80,12 @@ sys_core_inline, sys_core_prepare, sys_coverage, - sys_messages, sys_pre_attributes, v3_core ]}, {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"]}]}. 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), 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. 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/lib/kernel/src/group.erl b/lib/kernel/src/group.erl index b613e3709d4f..19b702bdd6e8 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), @@ -608,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 a9457801710b..bbafa2634b21 100644 --- a/lib/kernel/src/prim_tty.erl +++ b/lib/kernel/src/prim_tty.erl @@ -111,21 +111,25 @@ 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]). +%% 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]). @@ -142,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. @@ -154,18 +159,6 @@ 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 }). @@ -199,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 @@ -267,7 +234,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,93 +330,16 @@ 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 - {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 - {ok, U} -> U; - false -> error(enotsup) - end, - Down = case tgetstr("do") of - false -> (#state{})#state.down; - {ok, D} -> D - end, - Left = case {tgetflag("bs"),tgetstr("bc")} of - {true,_} -> (#state{})#state.left; - {_,false} -> (#state{})#state.left; - {_,{ok, L}} -> L - end, - - Right = case tgetstr("nd") of - {ok, R} -> R; - false -> error(enotsup) - end, - Insert = - case tgetstr("IC") of - {ok, IC} -> IC; - false -> (#state{})#state.insert - end, - - Tab = case tgetstr("ta") of - {ok, TA} -> TA; - false -> (#state{})#state.tab - end, - - Delete = case tgetstr("DC") of - {ok, DC} -> DC; - false -> (#state{})#state.delete - end, - - Position = case tgetstr("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"), - <<"\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 tgetstr("cd") of - {ok, DA} -> - DA; - false -> - (#state{})#state.delete_after_cursor - end, State#state{ cols = Cols, - clear = Clear, - xn = tgetflag("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{ @@ -732,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), @@ -776,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 = [], @@ -843,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 -> @@ -1006,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}. @@ -1097,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) -> @@ -1249,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) @@ -1304,8 +1190,8 @@ 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); - [$\e | Rest] -> + insert_buf(State, Rest, [io_ansi:tab() | LineAcc], Acc); + [CSI | Rest] when CSI =:= $\e; CSI =:= 155 -> case ansi_sgr(Bin) of none -> case re:run(Bin, State#state.ansi_regexp) of @@ -1390,7 +1276,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, @@ -1498,36 +1384,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..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 \ @@ -96,6 +97,7 @@ MODULES= \ gen_server \ gen_statem \ io \ + io_ansi \ io_lib \ io_lib_format \ io_lib_fread \ 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/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/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/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 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 -> 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/src/shell_docs.erl b/lib/stdlib/src/shell_docs.erl index 9eda7d7b9ea4..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 @@ -928,13 +934,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 +988,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 +1003,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 +1226,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 +1253,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 +1274,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 +1295,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/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: 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/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 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 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), 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/ ])