From fe5f5545d35f5d92576b161c22ac750a02462c15 Mon Sep 17 00:00:00 2001 From: Gaith Hallak Date: Wed, 24 Nov 2021 11:46:21 +0200 Subject: [PATCH] Add compiler warnings (#346) * Add compiler warnings Add include_type annotation to position Add warning for unused includes Add warning for unused stateful annotation Add warning for unused functions Add warning for shadowed variables Add division by zero warning Add warning for negative spends Add warning for unused variables Add warning for unused parameters Change the ets table type to set for unused vars Add warning for unused type defs Move unused variables warning to the top level Temporarily disable unused functions warnings Add all kinds of warnings to a single ets table Enable warnings separately through options Use when_option instead of enabled_warnings Turn warnings into type errors with warn_error option Enable warning package warn_all Re-enable unused functions warnings Report warnings as type errors in a separate function Make unused_function a recognized warning Report warnings as a result of compilation Fix tests and error for unknown warnings options Fix dialyzer warnings Do not show warning for variables called "_" Move warnings handling into a separate module Do not show warning for unused public functions in namespaces Add src file name to unused include warning Mark public functions in namespaces as used Add tests for added warnings Add warning for unused return value Add test for turning warnings into type errors * Update CHANGELOG --- CHANGELOG.md | 1 + src/aeso_aci.erl | 2 +- src/aeso_ast_infer_types.erl | 263 +++++++++++++++++++++++++++++++++-- src/aeso_compiler.erl | 20 ++- src/aeso_errors.erl | 1 + src/aeso_parse_lib.erl | 10 +- src/aeso_parser.erl | 17 ++- src/aeso_warnings.erl | 27 ++++ test/aeso_compiler_tests.erl | 89 +++++++++++- test/contracts/warnings.aes | 49 +++++++ 10 files changed, 451 insertions(+), 28 deletions(-) create mode 100644 src/aeso_warnings.erl create mode 100644 test/contracts/warnings.aes diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2bb1c..c434593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Compiler warnings for the follwing: shadowing, negative spends, division by zero, unused functions, unused includes, unused stateful annotations, unused variables, unused parameters, unused user-defined type, dead return value. ### Changed ### Removed diff --git a/src/aeso_aci.erl b/src/aeso_aci.erl index 0382593..ae696a4 100644 --- a/src/aeso_aci.erl +++ b/src/aeso_aci.erl @@ -70,7 +70,7 @@ do_contract_interface(Type, Contract, Options) when is_binary(Contract) -> do_contract_interface(Type, ContractString, Options) -> try Ast = aeso_compiler:parse(ContractString, Options), - {TypedAst, _} = aeso_ast_infer_types:infer(Ast, [dont_unfold | Options]), + {TypedAst, _, _} = aeso_ast_infer_types:infer(Ast, [dont_unfold | Options]), from_typed_ast(Type, TypedAst) catch throw:{error, Errors} -> {error, Errors} diff --git a/src/aeso_ast_infer_types.erl b/src/aeso_ast_infer_types.erl index b188a21..11d970e 100644 --- a/src/aeso_ast_infer_types.erl +++ b/src/aeso_ast_infer_types.erl @@ -171,7 +171,8 @@ on_scopes(Env = #env{ scopes = Scopes }, Fun) -> Env#env{ scopes = maps:map(fun(_, Scope) -> Fun(Scope) end, Scopes) }. -spec bind_var(aeso_syntax:id(), utype(), env()) -> env(). -bind_var({id, Ann, X}, T, Env) -> +bind_var({id, Ann, X}, T, Env = #env{ vars = Vars }) -> + when_warning(warn_shadowing, fun() -> warn_potential_shadowing(Ann, X, Vars) end), Env#env{ vars = [{X, {Ann, T}} | Env#env.vars] }. -spec bind_vars([{aeso_syntax:id(), utype()}], env()) -> env(). @@ -368,7 +369,9 @@ lookup_env(Env, Kind, Ann, Name) -> Names = [ Qual ++ [lists:last(Name)] || Qual <- possible_scopes(Env, Name) ], case [ Res || QName <- Names, Res <- [lookup_env1(Env, Kind, Ann, QName)], Res /= false] of [] -> false; - [Res] -> Res; + [Res = {_, {AnnR, _}}] -> + when_warning(warn_unused_includes, fun() -> used_include(AnnR) end), + Res; Many -> type_error({ambiguous_name, [{qid, A, Q} || {Q, {A, _}} <- Many]}), false @@ -775,7 +778,7 @@ global_env() -> option_t(As, T) -> {app_t, As, {id, As, "option"}, [T]}. map_t(As, K, V) -> {app_t, As, {id, As, "map"}, [K, V]}. --spec infer(aeso_syntax:ast()) -> {aeso_syntax:ast(), aeso_syntax:ast()} | {env(), aeso_syntax:ast(), aeso_syntax:ast()}. +-spec infer(aeso_syntax:ast()) -> {aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]} | {env(), aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]}. infer(Contracts) -> infer(Contracts, []). @@ -785,7 +788,7 @@ infer(Contracts) -> init_env(_Options) -> global_env(). -spec infer(aeso_syntax:ast(), list(option())) -> - {aeso_syntax:ast(), aeso_syntax:ast()} | {env(), aeso_syntax:ast(), aeso_syntax:ast()}. + {aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]} | {env(), aeso_syntax:ast(), aeso_syntax:ast(), [aeso_warnings:warning()]}. infer([], Options) -> create_type_errors(), type_error({no_decls, proplists:get_value(src_file, Options, no_file)}), @@ -797,11 +800,16 @@ infer(Contracts, Options) -> create_options(Options), ets_new(defined_contracts, [bag]), ets_new(type_vars, [set]), + ets_new(warnings, [bag]), + when_warning(warn_unused_functions, fun() -> create_unused_functions() end), check_modifiers(Env, Contracts), create_type_errors(), Contracts1 = identify_main_contract(Contracts, Options), destroy_and_report_type_errors(Env), {Env1, Decls} = infer1(Env, Contracts1, [], Options), + when_warning(warn_unused_functions, fun() -> destroy_and_report_unused_functions() end), + when_option(warn_error, fun() -> destroy_and_report_warnings_as_type_errors() end), + Warnings = lists:map(fun mk_warning/1, ets_tab2list(warnings)), {Env2, DeclsFolded, DeclsUnfolded} = case proplists:get_value(dont_unfold, Options, false) of true -> {Env1, Decls, Decls}; @@ -809,8 +817,8 @@ infer(Contracts, Options) -> {E, Decls, unfold_record_types(E, Decls)} end, case proplists:get_value(return_env, Options, false) of - false -> {DeclsFolded, DeclsUnfolded}; - true -> {Env2, DeclsFolded, DeclsUnfolded} + false -> {DeclsFolded, DeclsUnfolded, Warnings}; + true -> {Env2, DeclsFolded, DeclsUnfolded, Warnings} end after clean_up_ets() @@ -838,6 +846,7 @@ infer1(Env, [{Contract, Ann, ConName, Code} | Rest], Acc, Options) Env3 = bind_contract(Contract1, Env2), infer1(Env3, Rest, [Contract1 | Acc], Options); infer1(Env, [{namespace, Ann, Name, Code} | Rest], Acc, Options) -> + when_warning(warn_unused_includes, fun() -> potential_unused_include(Ann, proplists:get_value(src_file, Options, no_file)) end), check_scope_name_clash(Env, namespace, Name), {Env1, Code1} = infer_contract_top(push_scope(namespace, Name, Env), namespace, Code, Options), Namespace1 = {namespace, Ann, Name, Code1}, @@ -907,6 +916,7 @@ infer_contract(Env0, What, Defs0, Options) -> OldUsedNamespaces = Env#env.used_namespaces, Env01 = check_usings(Env, Get(using, Defs)), {Env1, TypeDefs} = check_typedefs(Env01, Get(type, Defs)), + when_warning(warn_unused_typedefs, fun() -> potential_unused_typedefs(Env#env.namespace, TypeDefs) end), create_type_errors(), check_unexpected(Get(unexpected, Defs)), Env2 = @@ -1345,7 +1355,10 @@ infer_letrec(Env, Defs) -> [print_typesig(S) || S <- TypeSigs], {TypeSigs, NewDefs}. -infer_letfun(Env, {fun_clauses, Ann, Fun = {id, _, Name}, Type, Clauses}) -> +infer_letfun(Env = #env{ namespace = Namespace }, {fun_clauses, Ann, Fun = {id, _, Name}, Type, Clauses}) -> + when_warning(warn_unused_stateful, fun() -> potential_unused_stateful(Ann, Fun) end), + when_warning(warn_unused_functions, + fun() -> potential_unused_function(Env, Ann, Namespace ++ qname(Fun), Fun) end), Type1 = check_type(Env, Type), {NameSigs, Clauses1} = lists:unzip([ infer_letfun1(Env, Clause) || Clause <- Clauses ]), {_, Sigs = [Sig | _]} = lists:unzip(NameSigs), @@ -1354,13 +1367,17 @@ infer_letfun(Env, {fun_clauses, Ann, Fun = {id, _, Name}, Type, Clauses}) -> unify(Env, ClauseT, Type1, {check_typesig, Name, ClauseT, Type1}) end || ClauseSig <- Sigs ], {{Name, Sig}, desugar_clauses(Ann, Fun, Sig, Clauses1)}; -infer_letfun(Env, LetFun = {letfun, Ann, Fun, _, _, _}) -> +infer_letfun(Env = #env{ namespace = Namespace }, LetFun = {letfun, Ann, Fun, _, _, _}) -> + when_warning(warn_unused_stateful, fun() -> potential_unused_stateful(Ann, Fun) end), + when_warning(warn_unused_functions, fun() -> potential_unused_function(Env, Ann, Namespace ++ qname(Fun), Fun) end), {{Name, Sig}, Clause} = infer_letfun1(Env, LetFun), {{Name, Sig}, desugar_clauses(Ann, Fun, Sig, [Clause])}. -infer_letfun1(Env0, {letfun, Attrib, Fun = {id, NameAttrib, Name}, Args, What, GuardedBodies}) -> + +infer_letfun1(Env0 = #env{ namespace = NS }, {letfun, Attrib, Fun = {id, NameAttrib, Name}, Args, What, GuardedBodies}) -> Env = Env0#env{ stateful = aeso_syntax:get_ann(stateful, Attrib, false), current_function = Fun }, {NewEnv, {typed, _, {tuple, _, TypedArgs}, {tuple_t, _, ArgTypes}}} = infer_pattern(Env, {tuple, [{origin, system} | NameAttrib], Args}), + when_warning(warn_unused_variables, fun() -> potential_unused_variables(NS, Name, free_vars(Args)) end), ExpectedType = check_type(Env, arg_type(NameAttrib, What)), InferGuardedBodies = fun({guarded, Ann, Guards, Body}) -> NewGuards = lists:map(fun(Guard) -> @@ -1415,12 +1432,13 @@ app_t(Ann, Name, Args) -> {app_t, Ann, Name, Args}. lookup_name(Env, As, Name) -> lookup_name(Env, As, Name, []). -lookup_name(Env, As, Id, Options) -> +lookup_name(Env = #env{ namespace = NS, current_function = {id, _, Fun} }, As, Id, Options) -> case lookup_env(Env, term, As, qname(Id)) of false -> type_error({unbound_variable, Id}), {Id, fresh_uvar(As)}; {QId, {_, Ty}} -> + when_warning(warn_unused_variables, fun() -> used_variable(NS, Fun, QId) end), Freshen = proplists:get_value(freshen, Options, false), check_stateful(Env, Id, Ty), Ty1 = case Ty of @@ -1443,7 +1461,9 @@ check_stateful(#env{ stateful = false, current_function = Fun }, Id, Type = {typ true -> type_error({stateful_not_allowed, Id, Fun}) end; -check_stateful(_Env, _Id, _Type) -> ok. +check_stateful(#env { current_function = Fun }, _Id, _Type) -> + when_warning(warn_unused_stateful, fun() -> used_stateful(Fun) end), + ok. %% Hack: don't allow passing the 'value' named arg if not stateful. This only %% works since the user can't create functions with named arguments. @@ -1601,16 +1621,21 @@ infer_expr(Env, {app, Ann, Fun, Args0} = App) -> prefix -> infer_op(Env, Ann, Fun, Args, fun infer_prefix/1); _ -> + CurrentFun = Env#env.current_function, + Namespace = Env#env.namespace, NamedArgsVar = fresh_uvar(Ann), NamedArgs1 = [ infer_named_arg(Env, NamedArgsVar, Arg) || Arg <- NamedArgs ], NewFun0 = infer_expr(Env, Fun), NewArgs = [infer_expr(Env, A) || A <- Args], ArgTypes = [T || {typed, _, _, T} <- NewArgs], - NewFun1 = {typed, _, _, FunType} = infer_var_args_fun(Env, NewFun0, NamedArgs1, ArgTypes), + NewFun1 = {typed, _, Name, FunType} = infer_var_args_fun(Env, NewFun0, NamedArgs1, ArgTypes), When = {infer_app, Fun, NamedArgs1, Args, FunType, ArgTypes}, GeneralResultType = fresh_uvar(Ann), ResultType = fresh_uvar(Ann), + when_warning(warn_unused_functions, + fun() -> register_function_call(Namespace ++ qname(CurrentFun), Name) end), unify(Env, FunType, {fun_t, [], NamedArgsVar, ArgTypes, GeneralResultType}, When), + when_warning(warn_negative_spend, fun() -> warn_potential_negative_spend(Ann, NewFun1, NewArgs) end), add_named_argument_constraint( #dependent_type_constraint{ named_args_t = NamedArgsVar, named_args = NamedArgs1, @@ -1835,6 +1860,7 @@ infer_op(Env, As, Op, Args, InferOp) -> ArgTypes = [T || {typed, _, _, T} <- TypedArgs], Inferred = {fun_t, _, _, OperandTypes, ResultType} = InferOp(Op), unify(Env, ArgTypes, OperandTypes, {infer_app, Op, [], Args, Inferred, ArgTypes}), + when_warning(warn_division_by_zero, fun() -> warn_potential_division_by_zero(As, Op, Args) end), {typed, As, {app, As, Op, TypedArgs}, ResultType}. infer_pattern(Env, Pattern) -> @@ -1848,8 +1874,9 @@ infer_pattern(Env, Pattern) -> NewPattern = infer_expr(NewEnv, Pattern), {NewEnv#env{ in_pattern = Env#env.in_pattern }, NewPattern}. -infer_case(Env, Attrs, Pattern, ExprType, GuardedBranches, SwitchType) -> +infer_case(Env = #env{ namespace = NS, current_function = {id, _, Fun} }, Attrs, Pattern, ExprType, GuardedBranches, SwitchType) -> {NewEnv, NewPattern = {typed, _, _, PatType}} = infer_pattern(Env, Pattern), + when_warning(warn_unused_variables, fun() -> potential_unused_variables(NS, Fun, free_vars(Pattern)) end), InferGuardedBranches = fun({guarded, Ann, Guards, Branch}) -> NewGuards = lists:map(fun(Guard) -> check_expr(NewEnv#env{ in_guard = true }, Guard, {id, Attrs, "bool"}) @@ -1879,7 +1906,9 @@ infer_block(Env, _, [{letval, Attrs, Pattern, E}|Rest], BlockType) -> infer_block(Env, Attrs, [Using = {using, _, _, _, _} | Rest], BlockType) -> infer_block(check_usings(Env, Using), Attrs, Rest, BlockType); infer_block(Env, Attrs, [E|Rest], BlockType) -> - [infer_expr(Env, E)|infer_block(Env, Attrs, Rest, BlockType)]. + NewE = infer_expr(Env, E), + when_warning(warn_unused_return_value, fun() -> potential_unused_return_value(NewE) end), + [NewE|infer_block(Env, Attrs, Rest, BlockType)]. infer_infix({BoolOp, As}) when BoolOp =:= '&&'; BoolOp =:= '||' -> @@ -1959,7 +1988,8 @@ next_count() -> ets_tables() -> [options, type_vars, type_defs, record_fields, named_argument_constraints, - field_constraints, freshen_tvars, type_errors, defined_contracts]. + field_constraints, freshen_tvars, type_errors, defined_contracts, + warnings, function_calls, all_functions]. clean_up_ets() -> [ catch ets_delete(Tab) || Tab <- ets_tables() ], @@ -1971,6 +2001,13 @@ clean_up_ets() -> ets_init() -> put(aeso_ast_infer_types, #{}). +ets_tab_exists(Name) -> + Tabs = get(aeso_ast_infer_types), + case maps:find(Name, Tabs) of + {ok, _} -> true; + error -> false + end. + ets_tabid(Name) -> #{Name := TabId} = get(aeso_ast_infer_types), TabId. @@ -1996,6 +2033,10 @@ ets_lookup(Name, Key) -> TabId = ets_tabid(Name), ets:lookup(TabId, Key). +ets_match_delete(Name, Pattern) -> + TabId = ets_tabid(Name), + ets:match_delete(TabId, Pattern). + ets_tab2list(Name) -> TabId = ets_tabid(Name), ets:tab2list(TabId). @@ -2461,6 +2502,7 @@ unfold_types_in_type(Env, {app_t, Ann, Id = {id, _, "map"}, Args = [KeyType0, _] [ type_error({map_in_map_key, Ann1, KeyType0}) || has_maps(KeyType) ], {app_t, Ann, Id, Args1}; unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) -> + when_warning(warn_unused_typedefs, fun() -> used_typedef(Id, length(Args)) end), UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false), case lookup_type(Env, Id) of @@ -2481,6 +2523,7 @@ unfold_types_in_type(Env, {app_t, Ann, Id, Args}, Options) when ?is_type_id(Id) end; unfold_types_in_type(Env, Id, Options) when ?is_type_id(Id) -> %% Like the case above, but for types without parameters. + when_warning(warn_unused_typedefs, fun() -> used_typedef(Id, 0) end), UnfoldRecords = proplists:get_value(unfold_record_types, Options, false), UnfoldVariants = proplists:get_value(unfold_variant_types, Options, false), case lookup_type(Env, Id) of @@ -2733,6 +2776,154 @@ integer_to_tvar(X) when X < 26 -> integer_to_tvar(X) -> [integer_to_tvar(X div 26)] ++ [$a + (X rem 26)]. +%% Warnings + +all_warnings() -> + [ warn_unused_includes + , warn_unused_stateful + , warn_unused_variables + , warn_unused_typedefs + , warn_unused_return_value + , warn_unused_functions + , warn_shadowing + , warn_division_by_zero + , warn_negative_spend ]. + +when_warning(Warn, Do) -> + case lists:member(Warn, all_warnings()) of + false -> + create_type_errors(), + type_error({unknown_warning, Warn}), + destroy_and_report_type_errors(global_env()); + true -> + case ets_tab_exists(warnings) of + true -> + IsEnabled = get_option(Warn, false), + IsAll = get_option(warn_all, false) andalso lists:member(Warn, all_warnings()), + if + IsEnabled orelse IsAll -> Do(); + true -> ok + end; + false -> + ok + end + end. + +%% Warnings (Unused includes) + +potential_unused_include(Ann, SrcFile) -> + case aeso_syntax:get_ann(file, Ann, no_file) of + no_file -> ok; + File -> ets_insert(warnings, {unused_include, File, SrcFile}) + end. + +used_include(Ann) -> + case aeso_syntax:get_ann(file, Ann, no_file) of + no_file -> ok; + File -> ets_match_delete(warnings, {unused_include, File, '_'}) + end. + +%% Warnings (Unused stateful) + +potential_unused_stateful(Ann, Fun) -> + case aeso_syntax:get_ann(stateful, Ann, false) of + false -> ok; + true -> ets_insert(warnings, {unused_stateful, Ann, Fun}) + end. + +used_stateful(Fun) -> + ets_match_delete(warnings, {unused_stateful, '_', Fun}). + +%% Warnings (Unused type defs) + +potential_unused_typedefs(Namespace, TypeDefs) -> + lists:map(fun({type_def, Ann, Id, Args, _}) -> + ets_insert(warnings, {unused_typedef, Ann, Namespace ++ qname(Id), length(Args)}) end, TypeDefs). + +used_typedef(TypeAliasId, Arity) -> + ets_match_delete(warnings, {unused_typedef, '_', qname(TypeAliasId), Arity}). + +%% Warnings (Unused variables) + +potential_unused_variables(Namespace, Fun, Vars0) -> + Vars = [ Var || Var = {id, _, VarName} <- Vars0, VarName /= "_" ], + lists:map(fun({id, Ann, VarName}) -> + ets_insert(warnings, {unused_variable, Ann, Namespace, Fun, VarName}) end, Vars). + +used_variable(Namespace, Fun, [VarName]) -> + ets_match_delete(warnings, {unused_variable, '_', Namespace, Fun, VarName}); +used_variable(_, _, _) -> ok. + +%% Warnings (Unused return value) + +potential_unused_return_value({typed, Ann, {app, _, {typed, _, _, {fun_t, _, _, _, {id, _, Type}}}, _}, _}) when Type /= "unit" -> + ets_insert(warnings, {unused_return_value, Ann}); +potential_unused_return_value(_) -> ok. + +%% Warnings (Unused functions) + +create_unused_functions() -> + ets_new(function_calls, [bag]), + ets_new(all_functions, [set]). + +register_function_call(_Caller, {proj, _, _, _}) -> ok; +register_function_call(Caller, Callee) -> + ets_insert(function_calls, {Caller, qname(Callee)}). + +potential_unused_function(#env{ what = namespace }, Ann, FunQName, FunId) -> + ets_insert(all_functions, {Ann, FunQName, FunId, not aeso_syntax:get_ann(private, Ann, false)}); +potential_unused_function(_Env, Ann, FunQName, FunId) -> + ets_insert(all_functions, {Ann, FunQName, FunId, aeso_syntax:get_ann(entrypoint, Ann, false)}). + +remove_used_funs(All) -> + {Used, Unused} = lists:partition(fun({_, _, _, IsUsed}) -> IsUsed end, All), + CallsByUsed = lists:flatmap(fun({_, F, _, _}) -> ets_lookup(function_calls, F) end, Used), + CalledFuns = sets:from_list(lists:map(fun({_, Callee}) -> Callee end, CallsByUsed)), + MarkUsedFun = fun(Fun, Acc) -> + case lists:keyfind(Fun, 2, Acc) of + false -> Acc; + T -> lists:keyreplace(Fun, 2, Acc, setelement(4, T, true)) + end + end, + NewUnused = sets:fold(MarkUsedFun, Unused, CalledFuns), + case lists:keyfind(true, 4, NewUnused) of + false -> NewUnused; + _ -> remove_used_funs(NewUnused) + end. + +destroy_and_report_unused_functions() -> + AllFuns = ets_tab2list(all_functions), + lists:map(fun({Ann, _, FunId, _}) -> ets_insert(warnings, {unused_function, Ann, name(FunId)}) end, + remove_used_funs(AllFuns)), + ets_delete(all_functions), + ets_delete(function_calls). + +%% Warnings (Shadowing) + +warn_potential_shadowing(_, "_", _) -> ok; +warn_potential_shadowing(Ann, Name, Vars) -> + case proplists:get_value(Name, Vars, false) of + false -> ok; + {AnnOld, _} -> ets_insert(warnings, {shadowing, Ann, Name, AnnOld}) + end. + +%% Warnings (Division by zero) + +warn_potential_division_by_zero(Ann, Op, Args) -> + case {Op, Args} of + {{'/', _}, [_, {int, _, 0}]} -> ets_insert(warnings, {division_by_zero, Ann}); + _ -> ok + end. + +%% Warnings (Negative spends) + +warn_potential_negative_spend(Ann, Fun, Args) -> + case {Fun, Args} of + { {typed, _, {qid, _, ["Chain", "spend"]}, _} + , [_, {typed, _, {app, _, {'-', _}, [{typed, _, {int, _, X}, _}]}, _}]} when X > 0 -> + ets_insert(warnings, {negative_spend, Ann}); + _ -> ok + end. %% Save unification failures for error messages. @@ -2752,6 +2943,11 @@ destroy_and_report_type_errors(Env) -> Errors = [ mk_error(unqualify(Env, Err)) || Err <- Errors0 ], aeso_errors:throw(Errors). %% No-op if Errors == [] +destroy_and_report_warnings_as_type_errors() -> + Warnings = [ mk_warning(Warn) || Warn <- ets_tab2list(warnings) ], + Errors = lists:map(fun mk_t_err_from_warn/1, Warnings), + aeso_errors:throw(Errors). %% No-op if Warnings == [] + %% Strip current namespace from error message for nicer printing. unqualify(#env{ namespace = NS }, {qid, Ann, Xs}) -> qid(Ann, unqualify1(NS, Xs)); @@ -2774,6 +2970,9 @@ mk_t_err(Pos, Msg) -> mk_t_err(Pos, Msg, Ctxt) -> aeso_errors:new(type_error, Pos, lists:flatten(Msg), lists:flatten(Ctxt)). +mk_t_err_from_warn(Warn) -> + aeso_warnings:warn_to_err(type_error, Warn). + mk_error({no_decls, File}) -> Pos = aeso_errors:pos(File, 0, 0), mk_t_err(Pos, "Empty contract\n"); @@ -3112,10 +3311,44 @@ mk_error({using_undefined_namespace_parts, Ann, Namespace, Parts}) -> PartsStr = lists:concat(lists:join(", ", Parts)), Msg = io_lib:format("The namespace ~s does not define the following names: ~s", [Namespace, PartsStr]), mk_t_err(pos(Ann), Msg); +mk_error({unknown_warning, Warning}) -> + Msg = io_lib:format("Trying to report unknown warning: ~p", [Warning]), + mk_t_err(pos(0, 0), Msg); mk_error(Err) -> Msg = io_lib:format("Unknown error: ~p\n", [Err]), mk_t_err(pos(0, 0), Msg). +mk_warning({unused_include, FileName, SrcFile}) -> + Msg = io_lib:format("The file ~s is included but not used", [FileName]), + aeso_warnings:new(aeso_errors:pos(SrcFile, 0, 0), Msg); +mk_warning({unused_stateful, Ann, FunName}) -> + Msg = io_lib:format("The function ~s is unnecessarily marked as stateful at ~s", [name(FunName), pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({unused_variable, Ann, _Namespace, _Fun, VarName}) -> + Msg = io_lib:format("The variable ~s is defined at ~s but never used", [VarName, pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({unused_typedef, Ann, QName, _Arity}) -> + Msg = io_lib:format("The type ~s is defined at ~s but never used", [lists:last(QName), pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({unused_return_value, Ann}) -> + Msg = io_lib:format("Unused return value at ~s", [pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({unused_function, Ann, FunName}) -> + Msg = io_lib:format("The function ~s is defined at ~s but never used", [FunName, pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({shadowing, Ann, VarName, AnnOld}) -> + Msg = io_lib:format("The definition of ~s at ~s shadows an older definition at ~s", [VarName, pp_loc(Ann), pp_loc(AnnOld)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({division_by_zero, Ann}) -> + Msg = io_lib:format("Division by zero at ~s", [pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning({negative_spend, Ann}) -> + Msg = io_lib:format("Negative spend at ~s", [pp_loc(Ann)]), + aeso_warnings:new(pos(Ann), Msg); +mk_warning(Warn) -> + Msg = io_lib:format("Unknown warning: ~p\n", [Warn]), + aeso_warnings:new(Msg). + mk_entrypoint(Decl) -> Ann = [entrypoint | lists:keydelete(public, 1, lists:keydelete(private, 1, diff --git a/src/aeso_compiler.erl b/src/aeso_compiler.erl index 28ee133..567099f 100644 --- a/src/aeso_compiler.erl +++ b/src/aeso_compiler.erl @@ -120,7 +120,8 @@ from_string(Backend, ContractString, Options) -> from_string1(aevm, ContractString, Options) -> #{ icode := Icode - , folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options), + , folded_typed_ast := FoldedTypedAst + , warnings := Warnings } = string_to_code(ContractString, Options), TypeInfo = extract_type_info(Icode), Assembler = assemble(Icode, Options), pp_assembler(aevm, Assembler, Options), @@ -133,13 +134,15 @@ from_string1(aevm, ContractString, Options) -> contract_source => ContractString, type_info => TypeInfo, abi_version => aeb_aevm_abi:abi_version(), - payable => maps:get(payable, Icode) + payable => maps:get(payable, Icode), + warnings => Warnings }, {ok, maybe_generate_aci(Res, FoldedTypedAst, Options)}; from_string1(fate, ContractString, Options) -> #{ fcode := FCode , fcode_env := #{child_con_env := ChildContracts} - , folded_typed_ast := FoldedTypedAst } = string_to_code(ContractString, Options), + , folded_typed_ast := FoldedTypedAst + , warnings := Warnings } = string_to_code(ContractString, Options), FateCode = aeso_fcode_to_fate:compile(ChildContracts, FCode, Options), pp_assembler(fate, FateCode, Options), ByteCode = aeb_fate_code:serialize(FateCode, []), @@ -150,7 +153,8 @@ from_string1(fate, ContractString, Options) -> type_info => [], fate_code => FateCode, abi_version => aeb_fate_abi:abi_version(), - payable => maps:get(payable, FCode) + payable => maps:get(payable, FCode), + warnings => Warnings }, {ok, maybe_generate_aci(Res, FoldedTypedAst, Options)}. @@ -168,7 +172,7 @@ string_to_code(ContractString, Options) -> Ast = parse(ContractString, Options), pp_sophia_code(Ast, Options), pp_ast(Ast, Options), - {TypeEnv, FoldedTypedAst, UnfoldedTypedAst} = aeso_ast_infer_types:infer(Ast, [return_env | Options]), + {TypeEnv, FoldedTypedAst, UnfoldedTypedAst, Warnings} = aeso_ast_infer_types:infer(Ast, [return_env | Options]), pp_typed_ast(UnfoldedTypedAst, Options), case proplists:get_value(backend, Options, aevm) of aevm -> @@ -178,7 +182,8 @@ string_to_code(ContractString, Options) -> , unfolded_typed_ast => UnfoldedTypedAst , folded_typed_ast => FoldedTypedAst , type_env => TypeEnv - , ast => Ast }; + , ast => Ast + , warnings => Warnings}; fate -> {Env, Fcode} = aeso_ast_to_fcode:ast_to_fcode(UnfoldedTypedAst, [{original_src, ContractString}|Options]), #{ fcode => Fcode @@ -186,7 +191,8 @@ string_to_code(ContractString, Options) -> , unfolded_typed_ast => UnfoldedTypedAst , folded_typed_ast => FoldedTypedAst , type_env => TypeEnv - , ast => Ast } + , ast => Ast + , warnings => Warnings } end. -define(CALL_NAME, "__call"). diff --git a/src/aeso_errors.erl b/src/aeso_errors.erl index 0dc9eda..79d4a6b 100644 --- a/src/aeso_errors.erl +++ b/src/aeso_errors.erl @@ -36,6 +36,7 @@ , pos/2 , pos/3 , pp/1 + , pp_pos/1 , to_json/1 , throw/1 , type/1 diff --git a/src/aeso_parse_lib.erl b/src/aeso_parse_lib.erl index 31065f6..4c50527 100644 --- a/src/aeso_parse_lib.erl +++ b/src/aeso_parse_lib.erl @@ -15,7 +15,8 @@ many/1, many1/1, sep/2, sep1/2, infixl/2, infixr/2]). --export([current_file/0, set_current_file/1]). +-export([current_file/0, set_current_file/1, + current_include_type/0, set_current_include_type/1]). %% -- Types ------------------------------------------------------------------ @@ -465,6 +466,13 @@ merge_with(Fun, Map1, Map2) -> end, Map2, maps:to_list(Map1)) end. +%% Current include type +current_include_type() -> + get('$current_include_type'). + +set_current_include_type(IncludeType) -> + put('$current_include_type', IncludeType). + %% Current source file current_file() -> get('$current_file'). diff --git a/src/aeso_parser.erl b/src/aeso_parser.erl index 3d388c2..79d7a8b 100644 --- a/src/aeso_parser.erl +++ b/src/aeso_parser.erl @@ -18,7 +18,8 @@ run_parser/3]). -include("aeso_parse_lib.hrl"). --import(aeso_parse_lib, [current_file/0, set_current_file/1]). +-import(aeso_parse_lib, [current_file/0, set_current_file/1, + current_include_type/0, set_current_include_type/1]). -type parse_result() :: aeso_syntax:ast() | {aeso_syntax:ast(), sets:set(include_hash())} | none(). @@ -57,6 +58,7 @@ run_parser(P, Inp, Opts) -> parse_and_scan(P, S, Opts) -> set_current_file(proplists:get_value(src_file, Opts, no_file)), + set_current_include_type(proplists:get_value(include_type, Opts, none)), case aeso_scan:scan(S) of {ok, Tokens} -> aeso_parse_lib:parse(P, Tokens); {error, {{Input, Pos}, _}} -> @@ -523,7 +525,11 @@ bracket_list(P) -> brackets(comma_sep(P)). -type ann_col() :: aeso_syntax:ann_col(). -spec pos_ann(ann_line(), ann_col()) -> ann(). -pos_ann(Line, Col) -> [{file, current_file()}, {line, Line}, {col, Col}]. +pos_ann(Line, Col) -> + [ {file, current_file()} + , {include_type, current_include_type()} + , {line, Line} + , {col, Col} ]. ann_pos(Ann) -> {proplists:get_value(file, Ann), @@ -665,9 +671,14 @@ expand_includes([{include, Ann, {string, _SAnn, File}} | AST], Included, Acc, Op Hashed = hash_include(File, Code), case sets:is_element(Hashed, Included) of false -> + IncludeType = case proplists:get_value(file, Ann) of + no_file -> direct; + _ -> indirect + end, Opts1 = lists:keystore(src_file, 1, Opts, {src_file, File}), + Opts2 = lists:keystore(include_type, 1, Opts1, {include_type, IncludeType}), Included1 = sets:add_element(Hashed, Included), - case parse_and_scan(file(), Code, Opts1) of + case parse_and_scan(file(), Code, Opts2) of {ok, AST1} -> expand_includes(AST1 ++ AST, Included1, Acc, Opts); Err = {error, _} -> diff --git a/src/aeso_warnings.erl b/src/aeso_warnings.erl new file mode 100644 index 0000000..bf16edf --- /dev/null +++ b/src/aeso_warnings.erl @@ -0,0 +1,27 @@ +-module(aeso_warnings). + +-record(warn, { pos :: aeso_errors:pos() + , message :: iolist() + }). + +-opaque warning() :: #warn{}. + +-export_type([warning/0]). + +-export([ new/1 + , new/2 + , warn_to_err/2 + , pp/1 + ]). + +new(Msg) -> + new(aeso_errors:pos(0, 0), Msg). + +new(Pos, Msg) -> + #warn{ pos = Pos, message = Msg }. + +warn_to_err(Kind, #warn{ pos = Pos, message = Msg }) -> + aeso_errors:new(Kind, Pos, lists:flatten(Msg)). + +pp(#warn{ pos = Pos, message = Msg }) -> + lists:flatten(io_lib:format("Warning~s:\n~s", [aeso_errors:pp_pos(Pos), Msg])). diff --git a/test/aeso_compiler_tests.erl b/test/aeso_compiler_tests.erl index 19d9ac1..26a0c74 100644 --- a/test/aeso_compiler_tests.erl +++ b/test/aeso_compiler_tests.erl @@ -46,7 +46,7 @@ simple_compile_test_() -> end} ] ++ [ {"Testing error messages of " ++ ContractName, fun() -> - Errors = compile(aevm, ContractName), + Errors = compile(aevm, ContractName, [warn_all, warn_error]), check_errors(ExpectedErrors, Errors) end} || {ContractName, ExpectedErrors} <- failing_contracts() ] ++ @@ -88,6 +88,11 @@ simple_compile_test_() -> ?assertMatch({_, _, true}, {SizeDeadCode, SizeNoDeadCode, SizeDeadCode + Delta < SizeNoDeadCode}), ok end} || Backend <- [aevm, fate] ] ++ + [ {"Testing warning messages", + fun() -> + #{ warnings := Warnings } = compile(Backend, "warnings", [warn_all]), + check_warnings(warnings(), Warnings) + end} || Backend <- [aevm, fate] ] ++ []. %% Check if all modules in the standard library compile @@ -119,6 +124,15 @@ check_errors(Expect0, Actual0) -> {Missing, Extra} -> ?assertEqual(Missing, Extra) end. +check_warnings(Expect0, Actual0) -> + Expect = lists:sort(Expect0), + Actual = [ list_to_binary(string:trim(aeso_warnings:pp(Warn))) || Warn <- Actual0 ], + case {Expect -- Actual, Actual -- Expect} of + {[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra}); + {Missing, []} -> ?assertMatch({missing, []}, {missing, Missing}); + {Missing, Extra} -> ?assertEqual(Missing, Extra) + end. + compile(Backend, Name) -> compile(Backend, Name, [{include, {file_system, [aeso_test_utils:contract_path()]}}]). @@ -228,6 +242,49 @@ debug_mode_contracts() -> -define(TYPE_ERROR(Name, Errs), ?ERROR("Type", Name, Errs)). -define(PARSE_ERROR(Name, Errs), ?ERROR("Parse", Name, Errs)). +-define(PosW(Kind, File, Line, Col), (list_to_binary(Kind))/binary, " in '", + (list_to_binary(File))/binary, ".aes' at line " ??Line ", col " ??Col ":\n"). +-define(PosW(Line, Col), ?PosW(__Kind, __File, Line, Col)). + +-define(WARNING(Name, Warns), + (fun() -> + __Kind = "Warning", + __File = ??Name, + Warns + end)()). + +warnings() -> + ?WARNING(warnings, + [<>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <> + ]). + failing_contracts() -> {ok, V} = aeso_compiler:numeric_version(), Version = list_to_binary(string:join([integer_to_list(N) || N <- V], ".")), @@ -817,6 +874,36 @@ failing_contracts() -> [<> ]) + , ?TYPE_ERROR(warnings, + [<>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <>, + <> + ]) ]. -define(Path(File), "code_errors/" ??File). diff --git a/test/contracts/warnings.aes b/test/contracts/warnings.aes new file mode 100644 index 0000000..51834b5 --- /dev/null +++ b/test/contracts/warnings.aes @@ -0,0 +1,49 @@ +// Used include +include "Pair.aes" +// Unused include +include "Triple.aes" + +namespace UnusedNamespace = + function f() = 1 + g() + + // Used in f + private function g() = 2 + + // Unused + private function h() = 3 + +contract Warnings = + + type state = int + + type unused_type = bool + + entrypoint init(p) = Pair.fst(p) + Pair.snd(p) + + stateful entrypoint negative_spend(to : address) = Chain.spend(to, -1) + + entrypoint shadowing() = + let x = 1 + let x = 2 + x + + entrypoint division_by_zero(x) = x / 0 + + stateful entrypoint unused_stateful() = 1 + stateful entrypoint used_stateful(x : int) = put(x) + + entrypoint unused_variables(unused_arg : int) = + let unused_var = 10 + let z = 20 + z + + // Unused functions + function unused_function() = () + function recursive_unused_function() = recursive_unused_function() + function called_unused_function1() = called_unused_function2() + function called_unused_function2() = called_unused_function1() + + function rv() = 1 + entrypoint unused_return_value() = + rv() + 2