%%% -*- erlang-indent-level: 4 -*- %% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1996-2023. 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% %% %% Do necessary checking of Erlang code. -module(erl_lint). -feature(maybe_expr, enable). -export([module/1,module/2,module/3,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]). -export([bool_option/4,value_option/3,value_option/7]). -export([check_format_string/1]). -export_type([error_info/0, error_description/0]). -export_type([fun_used_vars/0]). % Used from erl_eval.erl. -import(lists, [all/2,any/2, foldl/3,foldr/3, map/2,mapfoldl/3,member/2, reverse/1]). %% Removed functions -removed([{modify_line,2,"use erl_parse:map_anno/2 instead"}]). %% bool_option(OnOpt, OffOpt, Default, Options) -> boolean(). %% value_option(Flag, Default, Options) -> Value. %% value_option(Flag, Default, OnOpt, OnVal, OffOpt, OffVal, Options) -> %% Value. %% The option handling functions. -spec bool_option(atom(), atom(), boolean(), [compile:option()]) -> boolean(). bool_option(On, Off, Default, Opts) -> foldl(fun (Opt, _Def) when Opt =:= On -> true; (Opt, _Def) when Opt =:= Off -> false; (_Opt, Def) -> Def end, Default, Opts). value_option(Flag, Default, Opts) -> foldl(fun ({Opt,Val}, _Def) when Opt =:= Flag -> Val; (_Opt, Def) -> Def end, Default, Opts). value_option(Flag, Default, On, OnVal, Off, OffVal, Opts) -> foldl(fun ({Opt,Val}, _Def) when Opt =:= Flag -> Val; (Opt, _Def) when Opt =:= On -> OnVal; (Opt, _Def) when Opt =:= Off -> OffVal; (_Opt, Def) -> Def end, Default, Opts). %% The maximum number of arguments allowed for a function. -define(MAX_ARGUMENTS, 255). %% The error and warning info structures, {Location,Module,Descriptor}, %% are kept in their seperate fields in the lint state record together %% with the name of the file (when a new file is entered, marked by %% the 'file' attribute, then the field 'file' of the lint record is %% set). At the end of the run these lists are packed into a list of %% {FileName,ErrorDescList} pairs which are returned. -include_lib("stdlib/include/erl_bits.hrl"). %%-define(DEBUGF(X,Y), io:format(X, Y)). -define(DEBUGF(X,Y), void). -type anno() :: erl_anno:anno(). % a convenient alias -type fa() :: {atom(), arity()}. % function+arity -type ta() :: {atom(), arity()}. % type+arity -type module_or_mfa() :: module() | mfa(). -type gexpr_context() :: 'guard' | 'bin_seg_size' | 'map_key'. -record(typeinfo, {attr, anno}). -type type_id() :: {'export', []} | {'record', atom()} | {'spec', mfa()} | {'type', ta()}. -record(used_type, {anno :: erl_anno:anno(), at = {export, []} :: type_id()}). -type used_type() :: #used_type{}. -type fun_used_vars() :: #{erl_parse:abstract_expr() => {[atom()], fun_used_vars()}}. %% Usage of records, functions, and imports. The variable table, which %% is passed on as an argument, holds the usage of variables. -record(usage, { calls = maps:new(), %Who calls who imported = [], %Actually imported functions used_records = gb_sets:new() %Used record definitions :: gb_sets:set(atom()), used_types = maps:new() %Used type definitions :: #{ta() := [used_type()]} }). %% Define the lint state record. %% 'called' and 'exports' contain {Anno, {Function, Arity}}, %% the other function collections contain {Function, Arity}. -record(lint, {state=start :: 'start' | 'attribute' | 'function', module='', %Module behaviour=[], %Behaviour exports=gb_sets:empty() :: gb_sets:set(fa()),%Exports imports=[] :: orddict:orddict(fa(), module()),%Imports compile=[], %Compile flags records=maps:new() %Record definitions :: #{atom() => {anno(),Fields :: term()}}, locals=gb_sets:empty() %All defined functions (prescanned) :: gb_sets:set(fa()), no_auto=gb_sets:empty() %Functions explicitly not autoimported :: gb_sets:set(fa()) | 'all', defined=gb_sets:empty() %Defined fuctions :: gb_sets:set(fa()), 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 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). 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 xqlc= false :: boolean(), %true if qlc.hrl included called= [] :: [{fa(),anno()}], %Called functions fun_used_vars = undefined %Funs used vars :: fun_used_vars() | undefined, usage = #usage{} :: #usage{}, specs = maps:new() %Type specifications :: #{mfa() => anno()}, callbacks = maps:new() %Callback types :: #{mfa() => anno()}, optional_callbacks = maps:new() %Optional callbacks :: #{mfa() => anno()}, types = maps:new() %Type definitions :: #{ta() => #typeinfo{}}, exp_types=gb_sets:empty() %Exported types :: gb_sets:set(ta()), feature_keywords = %Keywords in %configurable features feature_keywords() :: #{atom() => atom()}, bvt = none :: 'none' | [any()], %Variables in binary pattern gexpr_context = guard %Context of guard expression :: gexpr_context(), load_nif=false :: boolean() %true if calls erlang:load_nif/2 }). -type lint_state() :: #lint{}. -type error_description() :: term(). -type error_info() :: {erl_anno:location()|'none', module(), error_description()}. %% format_error(Error) %% Return a string describing the error. -spec format_error(ErrorDescriptor) -> io_lib:chars() when ErrorDescriptor :: error_description(). format_error(undefined_module) -> "no module definition"; format_error(redefine_module) -> "redefining module"; format_error(pmod_unsupported) -> "parameterized modules are no longer supported"; %% format_error({redefine_mod_import, M, P}) -> %% io_lib:format("module '~s' already imported from package '~s'", [M, P]); format_error(non_latin1_module_unsupported) -> "module names with non-latin1 characters are not supported"; format_error(empty_module_name) -> "the module name must not be empty"; format_error(blank_module_name) -> "the module name must contain at least one visible character"; format_error(ctrl_chars_in_module_name) -> "the module name must not contain control characters"; format_error(invalid_call) -> "invalid function call"; format_error(invalid_record) -> "invalid record expression"; format_error({future_feature, Ftr, Atom}) -> io_lib:format("atom '~p' is reserved in the experimental feature '~p'", [Atom, Ftr]); format_error({attribute,A}) -> io_lib:format("attribute ~tw after function definitions", [A]); format_error({missing_qlc_hrl,A}) -> io_lib:format("qlc:q/~w called, but \"qlc.hrl\" not included", [A]); format_error({redefine_import,{{F,A},M}}) -> io_lib:format("function ~tw/~w already imported from ~w", [F,A,M]); format_error({bad_inline,{F,A}}) -> io_lib:format("inlined function ~tw/~w undefined", [F,A]); format_error({undefined_nif,{F,A}}) -> io_lib:format("nif ~tw/~w undefined", [F,A]); format_error(no_load_nif) -> io_lib:format("nifs defined, but no call to erlang:load_nif/2", []); format_error({invalid_deprecated,D}) -> io_lib:format("badly formed deprecated attribute ~tw", [D]); format_error({bad_deprecated,{F,A}}) -> io_lib:format("deprecated function ~tw/~w undefined or not exported", [F,A]); format_error({invalid_removed,D}) -> io_lib:format("badly formed removed attribute ~tw", [D]); format_error({bad_removed,{F,A}}) when F =:= '_'; A =:= '_' -> io_lib:format("at least one function matching ~tw/~w is still exported", [F,A]); format_error({bad_removed,{F,A}}) -> io_lib:format("removed function ~tw/~w is still exported", [F,A]); format_error({bad_nowarn_unused_function,{F,A}}) -> io_lib:format("function ~tw/~w undefined", [F,A]); format_error({bad_nowarn_bif_clash,{F,A}}) -> io_lib:format("function ~tw/~w undefined", [F,A]); format_error(disallowed_nowarn_bif_clash) -> io_lib:format("compile directive nowarn_bif_clash is no longer allowed,~n" " - use explicit module names or -compile({no_auto_import, [F/A]})", []); format_error({bad_on_load,Term}) -> io_lib:format("badly formed on_load attribute: ~tw", [Term]); format_error(multiple_on_loads) -> "more than one on_load attribute"; format_error({bad_on_load_arity,{F,A}}) -> io_lib:format("function ~tw/~w has wrong arity (must be 0)", [F,A]); format_error({undefined_on_load,{F,A}}) -> io_lib:format("function ~tw/~w undefined", [F,A]); format_error(nif_inline) -> "inlining is enabled - local calls to NIFs may call their Erlang " "implementation instead"; format_error(export_all) -> "export_all flag enabled - all functions will be exported"; format_error({duplicated_export, {F,A}}) -> io_lib:format("function ~tw/~w already exported", [F,A]); format_error({unused_import,{{F,A},M}}) -> io_lib:format("import ~w:~tw/~w is unused", [M,F,A]); format_error({undefined_function,{F,A}}) -> io_lib:format("function ~tw/~w undefined", [F,A]); format_error({redefine_function,{F,A}}) -> io_lib:format("function ~tw/~w already defined", [F,A]); format_error({define_import,{F,A}}) -> io_lib:format("defining imported function ~tw/~w", [F,A]); format_error({unused_function,{F,A}}) -> io_lib:format("function ~tw/~w is unused", [F,A]); format_error({call_to_redefined_bif,{F,A}}) -> io_lib:format("ambiguous call of overridden auto-imported BIF ~w/~w~n" " - use erlang:~w/~w or \"-compile({no_auto_import,[~w/~w]}).\" " "to resolve name clash", [F,A,F,A,F,A]); format_error({call_to_redefined_old_bif,{F,A}}) -> io_lib:format("ambiguous call of overridden pre R14 auto-imported BIF ~w/~w~n" " - use erlang:~w/~w or \"-compile({no_auto_import,[~w/~w]}).\" " "to resolve name clash", [F,A,F,A,F,A]); format_error({redefine_old_bif_import,{F,A}}) -> io_lib:format("import directive overrides pre R14 auto-imported BIF ~w/~w~n" " - use \"-compile({no_auto_import,[~w/~w]}).\" " "to resolve name clash", [F,A,F,A]); format_error({redefine_bif_import,{F,A}}) -> io_lib:format("import directive overrides auto-imported BIF ~w/~w~n" " - use \"-compile({no_auto_import,[~w/~w]}).\" to resolve name clash", [F,A,F,A]); format_error({deprecated, MFA, String, Rel}) -> io_lib:format("~s is deprecated and will be removed in ~s; ~s", [format_mfa(MFA), Rel, String]); format_error({deprecated, MFA, String}) when is_list(String) -> io_lib:format("~s is deprecated; ~s", [format_mfa(MFA), String]); format_error({deprecated_type, {M1, F1, A1}, String, Rel}) -> io_lib:format("the type ~p:~p~s is deprecated and will be removed in ~s; ~s", [M1, F1, gen_type_paren(A1), Rel, String]); format_error({deprecated_type, {M1, F1, A1}, String}) when is_list(String) -> io_lib:format("the type ~p:~p~s is deprecated; ~s", [M1, F1, gen_type_paren(A1), String]); format_error({removed, MFA, ReplacementMFA, Rel}) -> io_lib:format("call to ~s will fail, since it was removed in ~s; " "use ~s", [format_mfa(MFA), Rel, format_mfa(ReplacementMFA)]); format_error({removed, MFA, String}) when is_list(String) -> io_lib:format("~s is removed; ~s", [format_mfa(MFA), String]); format_error({removed_type, MNA, String}) -> io_lib:format("the type ~s is removed; ~s", [format_mna(MNA), String]); format_error({obsolete_guard, {F, A}}) -> io_lib:format("~p/~p obsolete (use is_~p/~p)", [F, A, F, A]); format_error({obsolete_guard_overridden,Test}) -> io_lib:format("obsolete ~s/1 (meaning is_~s/1) is illegal when " "there is a local/imported function named is_~p/1 ", [Test,Test,Test]); format_error({too_many_arguments,Arity}) -> io_lib:format("too many arguments (~w) - " "maximum allowed is ~w", [Arity,?MAX_ARGUMENTS]); %% --- patterns and guards --- format_error(illegal_pattern) -> "illegal pattern"; format_error(illegal_map_key) -> "illegal map key in pattern"; format_error(illegal_expr) -> "illegal expression"; format_error({illegal_guard_local_call, {F,A}}) -> io_lib:format("call to local/imported function ~tw/~w is illegal in guard", [F,A]); format_error(illegal_guard_expr) -> "illegal guard expression"; %% --- maps --- format_error(illegal_map_construction) -> "only association operators '=>' are allowed in map construction"; %% --- records --- format_error({undefined_record,T}) -> io_lib:format("record ~tw undefined", [T]); format_error({redefine_record,T}) -> io_lib:format("record ~tw already defined", [T]); format_error({redefine_field,T,F}) -> io_lib:format("field ~tw already defined in record ~tw", [F,T]); format_error(bad_multi_field_init) -> io_lib:format("'_' initializes no omitted fields", []); format_error({undefined_field,T,F}) -> io_lib:format("field ~tw undefined in record ~tw", [F,T]); format_error(illegal_record_info) -> "illegal record info"; format_error({field_name_is_variable,T,F}) -> io_lib:format("field ~tw is not an atom or _ in record ~tw", [F,T]); format_error({wildcard_in_update,T}) -> io_lib:format("meaningless use of _ in update of record ~tw", [T]); format_error({unused_record,T}) -> io_lib:format("record ~tw is unused", [T]); format_error({untyped_record,T}) -> io_lib:format("record ~tw has field(s) without type information", [T]); %% --- variables ---- format_error({unbound_var,V}) -> io_lib:format("variable ~w is unbound", [V]); format_error({unsafe_var,V,{What,Where}}) -> io_lib:format("variable ~w unsafe in ~w ~s", [V,What,format_where(Where)]); format_error({exported_var,V,{What,Where}}) -> io_lib:format("variable ~w exported from ~w ~s", [V,What,format_where(Where)]); format_error({match_underscore_var, V}) -> io_lib:format("variable ~w is already bound. If you mean to ignore this " "value, use '_' or a different underscore-prefixed name", [V]); format_error({match_underscore_var_pat, V}) -> io_lib:format("variable ~w is bound multiple times in this pattern. If " "you mean to ignore this value, use '_' or a different " "underscore-prefixed name", [V]); format_error({shadowed_var,V,In}) -> io_lib:format("variable ~w shadowed in ~w", [V,In]); format_error({unused_var, V}) -> io_lib:format("variable ~w is unused", [V]); format_error({variable_in_record_def,V}) -> io_lib:format("variable ~w in record definition", [V]); format_error({stacktrace_guard,V}) -> io_lib:format("stacktrace variable ~w must not be used in a guard", [V]); format_error({stacktrace_bound,V}) -> io_lib:format("stacktrace variable ~w must not be previously bound", [V]); %% --- binaries --- format_error({undefined_bittype,Type}) -> io_lib:format("bit type ~tw undefined", [Type]); format_error({bittype_mismatch,Val1,Val2,What}) -> io_lib:format("conflict in ~s specification for bit field: '~p' and '~p'", [What,Val1,Val2]); format_error(bittype_unit) -> "a bit unit size must not be specified unless a size is specified too"; format_error(illegal_bitsize) -> "illegal bit size"; format_error({illegal_bitsize_local_call, {F,A}}) -> io_lib:format("call to local/imported function ~tw/~w is illegal in a size " "expression for a binary segment", [F,A]); format_error(non_integer_bitsize) -> "a size expression in a pattern evaluates to a non-integer value; " "this pattern cannot possibly match"; format_error(unsized_binary_not_at_end) -> "a binary field without size is only allowed at the end of a binary pattern"; format_error(typed_literal_string) -> "a literal string in a binary pattern must not have a type or a size"; format_error(utf_bittype_size_or_unit) -> "neither size nor unit must be given for segments of type utf8/utf16/utf32"; format_error({bad_bitsize,Type}) -> io_lib:format("bad ~s bit size", [Type]); format_error(unsized_binary_in_bin_gen_pattern) -> "binary fields without size are not allowed in patterns of bit string generators"; %% --- behaviours --- format_error({conflicting_behaviours,{Name,Arity},B,FirstL,FirstB}) -> io_lib:format("conflicting behaviours - callback ~tw/~w required by both '~p' " "and '~p' ~s", [Name,Arity,B,FirstB,format_where(FirstL)]); format_error({undefined_behaviour_func, {Func,Arity}, Behaviour}) -> io_lib:format("undefined callback function ~tw/~w (behaviour '~w')", [Func,Arity,Behaviour]); format_error({undefined_behaviour,Behaviour}) -> io_lib:format("behaviour ~tw undefined", [Behaviour]); format_error({undefined_behaviour_callbacks,Behaviour}) -> io_lib:format("behaviour ~w callback functions are undefined", [Behaviour]); format_error({ill_defined_behaviour_callbacks,Behaviour}) -> io_lib:format("behaviour ~w callback functions erroneously defined", [Behaviour]); format_error({ill_defined_optional_callbacks,Behaviour}) -> io_lib:format("behaviour ~w optional callback functions erroneously defined", [Behaviour]); format_error({behaviour_info, {_M,F,A}}) -> io_lib:format("cannot define callback attibute for ~tw/~w when " "behaviour_info is defined",[F,A]); format_error({redefine_optional_callback, {F, A}}) -> io_lib:format("optional callback ~tw/~w duplicated", [F, A]); format_error({undefined_callback, {_M, F, A}}) -> io_lib:format("callback ~tw/~w is undefined", [F, A]); %% --- types and specs --- format_error({singleton_typevar, Name}) -> io_lib:format("type variable ~w is only used once (is unbound)", [Name]); format_error({bad_export_type, _ETs}) -> io_lib:format("bad export_type declaration", []); format_error({duplicated_export_type, {T, A}}) -> io_lib:format("type ~tw/~w already exported", [T, A]); format_error({undefined_type, {TypeName, Arity}}) -> io_lib:format("type ~tw~s undefined", [TypeName, gen_type_paren(Arity)]); format_error({unused_type, {TypeName, Arity}}) -> io_lib:format("type ~tw~s is unused", [TypeName, gen_type_paren(Arity)]); format_error({redefine_builtin_type, {TypeName, Arity}}) -> io_lib:format("local redefinition of built-in type: ~w~s", [TypeName, gen_type_paren(Arity)]); format_error({renamed_type, OldName, NewName}) -> io_lib:format("type ~w() is now called ~w(); " "please use the new name instead", [OldName, NewName]); format_error({redefine_type, {TypeName, Arity}}) -> io_lib:format("type ~tw~s already defined", [TypeName, gen_type_paren(Arity)]); format_error({type_syntax, Constr}) -> io_lib:format("bad ~tw type", [Constr]); format_error(old_abstract_code) -> io_lib:format("abstract code generated before Erlang/OTP 19.0 and " "having typed record fields cannot be compiled", []); format_error({redefine_spec, {M, F, A}}) -> io_lib:format("spec for ~tw:~tw/~w already defined", [M, F, A]); format_error({redefine_spec, {F, A}}) -> io_lib:format("spec for ~tw/~w already defined", [F, A]); format_error({redefine_callback, {F, A}}) -> io_lib:format("callback ~tw/~w already defined", [F, A]); format_error({bad_callback, {M, F, A}}) -> io_lib:format("explicit module not allowed for callback ~tw:~tw/~w", [M, F, A]); format_error({bad_module, {M, F, A}}) -> io_lib:format("spec for function ~w:~tw/~w from other module", [M, F, A]); format_error({spec_fun_undefined, {F, A}}) -> io_lib:format("spec for undefined function ~tw/~w", [F, A]); format_error({missing_spec, {F,A}}) -> io_lib:format("missing specification for function ~tw/~w", [F, A]); format_error(spec_wrong_arity) -> "spec has wrong arity"; format_error(callback_wrong_arity) -> "callback has wrong arity"; format_error({deprecated_builtin_type, {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, io_lib:format("type ~w/~w is deprecated and will be " "removed in ~s; use ~s", [Name, Arity, Rel, UseS]); format_error({not_exported_opaque, {TypeName, Arity}}) -> io_lib:format("opaque type ~tw~s is not exported", [TypeName, gen_type_paren(Arity)]); format_error({bad_dialyzer_attribute,Term}) -> io_lib:format("badly formed dialyzer attribute: ~tw", [Term]); format_error({bad_dialyzer_option,Term}) -> io_lib:format("unknown dialyzer warning option: ~tw", [Term]); %% --- obsolete? unused? --- format_error({format_error, {Fmt, Args}}) -> io_lib:format(Fmt, Args). gen_type_paren(Arity) when is_integer(Arity), Arity >= 0 -> gen_type_paren_1(Arity, ")"). gen_type_paren_1(0, Acc) -> "(" ++ Acc; gen_type_paren_1(1, Acc) -> "(_" ++ Acc; gen_type_paren_1(N, Acc) -> gen_type_paren_1(N - 1, ",_" ++ Acc). format_mfa({M, F, [_|_]=As}) -> ","++ArityString = lists:append([[$,|integer_to_list(A)] || A <- As]), format_mf(M, F, ArityString); format_mfa({M, F, A}) when is_integer(A) -> format_mf(M, F, integer_to_list(A)). format_mf(M, F, ArityString) when is_atom(M), is_atom(F) -> atom_to_list(M) ++ ":" ++ atom_to_list(F) ++ "/" ++ ArityString. format_mna({M, N, A}) when is_integer(A) -> atom_to_list(M) ++ ":" ++ atom_to_list(N) ++ gen_type_paren(A). format_where(L) when is_integer(L) -> io_lib:format("(line ~p)", [L]); format_where({L,C}) when is_integer(L), is_integer(C) -> io_lib:format("(line ~p, column ~p)", [L, C]). %% Local functions that are somehow automatically generated. pseudolocals() -> [{module_info,0}, {module_info,1}, {record_info,2}]. %% %% Used by erl_eval.erl to check commands. %% exprs(Exprs, BindingsList) -> exprs_opt(Exprs, BindingsList, []). exprs_opt(Exprs, BindingsList, Opts) -> {St0,Vs} = foldl(fun({{record,_SequenceNumber,_Name},Attr0}, {St1,Vs1}) -> Attr = set_file(Attr0, "none"), {attribute_state(Attr, St1),Vs1}; ({V,_}, {St1,Vs1}) -> {St1,[{V,{bound,unused,[]}} | Vs1]} end, {start("nofile",Opts),[]}, BindingsList), Vt = orddict:from_list(Vs), {_Evt,St} = exprs(set_file(Exprs, "nofile"), Vt, St0), return_status(St). used_vars(Exprs, BindingsList) -> Vs = foldl(fun({{record,_SequenceNumber,_Name},_Attr}, Vs0) -> Vs0; ({V,_Val}, Vs0) -> [{V,{bound,unused,[]}} | Vs0] end, [], BindingsList), Vt = orddict:from_list(Vs), St0 = (start())#lint{fun_used_vars=maps:new()}, {_Evt,St1} = exprs(Exprs, Vt, St0), St1#lint.fun_used_vars. %% module([Form]) -> %% module([Form], FileName) -> %% module([Form], FileName, [CompileOption]) -> %% {ok,[Warning]} | {error,[Error],[Warning]} %% Start processing a module. Define predefined functions and exports and %% apply_lambda/2 has been called to shut lint up. N.B. these lists are %% really all ordsets! -spec(module(AbsForms) -> {ok, Warnings} | {error, Errors, Warnings} when AbsForms :: [erl_parse:abstract_form() | erl_parse:form_info()], Warnings :: [{SourceFile,[ErrorInfo]}], Errors :: [{SourceFile,[ErrorInfo]}], SourceFile :: file:filename(), ErrorInfo :: error_info()). module(Forms) -> Opts = compiler_options(Forms), St = forms(Forms, start("nofile", Opts)), return_status(St). -spec(module(AbsForms, FileName) -> {ok, Warnings} | {error, Errors, Warnings} when AbsForms :: [erl_parse:abstract_form() | erl_parse:form_info()], FileName :: atom() | string(), Warnings :: [{SourceFile,[ErrorInfo]}], Errors :: [{SourceFile,[ErrorInfo]}], SourceFile :: file:filename(), ErrorInfo :: error_info()). module(Forms, FileName) -> Opts = compiler_options(Forms), St = forms(Forms, start(FileName, Opts)), return_status(St). -spec(module(AbsForms, FileName, CompileOptions) -> {ok, Warnings} | {error, Errors, Warnings} when AbsForms :: [erl_parse:abstract_form() | erl_parse:form_info()], FileName :: atom() | string(), CompileOptions :: [compile:option()], Warnings :: [{SourceFile,[ErrorInfo]}], Errors :: [{SourceFile,[ErrorInfo]}], SourceFile :: file:filename(), ErrorInfo :: error_info()). 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 = compiler_options(Forms) ++ Opts0, St = forms(Forms, start(FileName, Opts)), return_status(St). compiler_options(Forms) -> lists:flatten([C || {attribute,_,compile,C} <- Forms]). %% start() -> State %% start(FileName, [Option]) -> State start() -> start("nofile", []). start(File, Opts) -> Enabled0 = [{unused_vars, bool_option(warn_unused_vars, nowarn_unused_vars, true, Opts)}, {underscore_match, bool_option(warn_underscore_match, nowarn_underscore_match, true, Opts)}, {export_all, bool_option(warn_export_all, nowarn_export_all, true, Opts)}, {export_vars, bool_option(warn_export_vars, nowarn_export_vars, false, Opts)}, {shadow_vars, bool_option(warn_shadow_vars, nowarn_shadow_vars, true, Opts)}, {unused_import, bool_option(warn_unused_import, nowarn_unused_import, false, Opts)}, {unused_function, bool_option(warn_unused_function, nowarn_unused_function, true, Opts)}, {unused_type, bool_option(warn_unused_type, nowarn_unused_type, true, Opts)}, {bif_clash, bool_option(warn_bif_clash, nowarn_bif_clash, true, Opts)}, {unused_record, bool_option(warn_unused_record, nowarn_unused_record, true, Opts)}, {deprecated_function, bool_option(warn_deprecated_function, nowarn_deprecated_function, true, Opts)}, {deprecated_type, bool_option(warn_deprecated_type, nowarn_deprecated_type, true, Opts)}, {obsolete_guard, bool_option(warn_obsolete_guard, nowarn_obsolete_guard, true, Opts)}, {untyped_record, bool_option(warn_untyped_record, nowarn_untyped_record, false, Opts)}, {missing_spec, bool_option(warn_missing_spec, nowarn_missing_spec, false, Opts)}, {missing_spec_all, bool_option(warn_missing_spec_all, nowarn_missing_spec_all, false, Opts)}, {removed, bool_option(warn_removed, nowarn_removed, true, Opts)}, {nif_inline, bool_option(warn_nif_inline, nowarn_nif_inline, true, Opts)}, {keyword_warning, bool_option(warn_keywords, nowarn_keywords, false, Opts)}, {redefined_builtin_type, bool_option(warn_redefined_builtin_type, nowarn_redefined_builtin_type, true, Opts)}, {singleton_typevar, bool_option(warn_singleton_typevar, nowarn_singleton_typevar, true, Opts)} ], Enabled1 = [Category || {Category,true} <- Enabled0], Enabled = ordsets:from_list(Enabled1), Calls = case ordsets:is_element(unused_function, Enabled) of true -> #{{module_info,1} => pseudolocals()}; false -> undefined end, #lint{state = start, exports = gb_sets:from_list([{module_info,0},{module_info,1}]), compile = 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, nowarn_bif_clash = nowarn_function(nowarn_bif_clash, Opts), file = File }. %% 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). %% 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 [] -> {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(Anno, E0, #lint{gexpr_context=Context}=St) -> E = case {E0,Context} of {illegal_guard_expr,bin_seg_size} -> illegal_bitsize; {{illegal_guard_local_call,FA},bin_seg_size} -> {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_warning(W, St) -> add_lint_warning(W, St#lint.file, St). add_warning(Anno, W, St) -> {File,Location} = loc(Anno, St), add_lint_warning({Location,erl_lint,W}, File, St). add_lint_warning(W, File, St) -> St#lint{warnings=[{File,W}|St#lint.warnings]}. loc(Anno, St) -> Location = erl_anno:location(Anno), case erl_anno:file(Anno) of undefined -> {St#lint.file,Location}; File -> {File,Location} end. %% forms([Form], State) -> State' forms(Forms0, St0) -> Forms = eval_file_attribute(Forms0, St0), %% Annotations from now on include the 'file' item. Locals = local_functions(Forms), 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}), St2 = bif_clashes(Forms, St1), St3 = not_deprecated(Forms, St2), St4 = not_removed(Forms, St3), St5 = foldl(fun form/2, pre_scan(Forms, St4), Forms), post_traversal_check(Forms, St5). pre_scan([{attribute,A,compile,C} | Fs], St) -> case is_warn_enabled(export_all, St) andalso member(export_all, lists:flatten([C])) of true -> pre_scan(Fs, add_warning(A, export_all, St)); false -> pre_scan(Fs, St) end; pre_scan([_ | Fs], St) -> pre_scan(Fs, St); pre_scan([], St) -> St. includes_qlc_hrl(Forms, St) -> %% QLC calls erl_lint several times, sometimes with the compile %% attribute removed. The file attribute, however, is left as is. QH = [File || {attribute,_,file,{File,_line}} <- Forms, filename:basename(File) =:= "qlc.hrl"], St#lint{xqlc = QH =/= []}. eval_file_attribute(Forms, St) -> eval_file_attr(Forms, St#lint.file). eval_file_attr([{attribute,_A,file,{File,_Line}}=Form | Forms], _File) -> [Form | eval_file_attr(Forms, File)]; eval_file_attr([Form0 | Forms], File) -> Form = set_form_file(Form0, File), [Form | eval_file_attr(Forms, File)]; eval_file_attr([], _File) -> []. %% Sets the file only on the form. This is used on post-traversal. %% For the remaining of the AST we rely on #lint.file. set_form_file({attribute,A,K,V}, File) -> {attribute,erl_anno:set_file(File, A),K,V}; set_form_file({function,Anno,N,A,C}, File) -> {function,erl_anno:set_file(File, Anno),N,A,C}; set_form_file(Form, _File) -> Form. set_file(Ts, File) when is_list(Ts) -> [anno_set_file(T, File) || T <- Ts]; set_file(T, File) -> anno_set_file(T, File). anno_set_file(T, File) -> F = fun(Anno) -> erl_anno:set_file(File, Anno) end, erl_parse:map_anno(F, T). %% 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({attribute,_A,file,{File,_Line}}, St) -> St#lint{file = File}; form({attribute,_A,compile,_}, St) -> St; form(Form, #lint{state=State}=St) -> case State of start -> start_state(Form, St); attribute -> attribute_state(Form, St); function -> function_state(Form, St) end. %% start_state(Form, State) -> State' start_state({attribute,Anno,module,{_,_}}=Form, St0) -> St1 = add_error(Anno, pmod_unsupported, St0), attribute_state(Form, St1#lint{state=attribute}); start_state({attribute,Anno,module,M}, St0) -> St1 = St0#lint{module=M}, St2 = St1#lint{state=attribute}, check_module_name(M, Anno, St2); start_state(Form, St) -> Anno = case Form of {eof, Location} -> erl_anno:new(Location); %% {warning, Warning} and {error, Error} not possible here. _ -> element(2, Form) end, St1 = add_error(Anno, undefined_module, St), attribute_state(Form, St1#lint{state=attribute}). %% attribute_state(Form, State) -> %% State' attribute_state({attribute,_A,module,_M}, #lint{module=''}=St) -> St; attribute_state({attribute,A,module,_M}, St) -> add_error(A, redefine_module, St); attribute_state({attribute,A,export,Es}, St) -> export(A, Es, St); attribute_state({attribute,A,export_type,Es}, St) -> export_type(A, Es, St); attribute_state({attribute,A,import,Is}, St) -> import(A, Is, St); attribute_state({attribute,A,record,{Name,Fields}}, St) -> record_def(A, Name, Fields, St); attribute_state({attribute,Aa,behaviour,Behaviour}, St) -> St#lint{behaviour=St#lint.behaviour ++ [{Aa,Behaviour}]}; attribute_state({attribute,Aa,behavior,Behaviour}, St) -> St#lint{behaviour=St#lint.behaviour ++ [{Aa,Behaviour}]}; attribute_state({attribute,A,type,{TypeName,TypeDef,Args}}, St) -> type_def(type, A, TypeName, TypeDef, Args, St); attribute_state({attribute,A,opaque,{TypeName,TypeDef,Args}}, St) -> type_def(opaque, A, TypeName, TypeDef, Args, St); attribute_state({attribute,A,spec,{Fun,Types}}, St) -> spec_decl(A, Fun, Types, St); attribute_state({attribute,A,callback,{Fun,Types}}, St) -> callback_decl(A, Fun, Types, St); attribute_state({attribute,A,optional_callbacks,Es}, St) -> optional_callbacks(A, Es, St); attribute_state({attribute,A,on_load,Val}, St) -> on_load(A, Val, St); attribute_state({attribute,_A,_Other,_Val}, St) -> % Ignore others St; attribute_state(Form, St) -> function_state(Form, St#lint{state=function}). %% function_state(Form, State) -> %% State' %% Allow for record, type and opaque type definitions and spec %% declarations to be intersperced within function definitions. %% Dialyzer attributes are also allowed everywhere. function_state({attribute,A,record,{Name,Fields}}, St) -> record_def(A, Name, Fields, St); function_state({attribute,A,type,{TypeName,TypeDef,Args}}, St) -> type_def(type, A, TypeName, TypeDef, Args, St); function_state({attribute,A,opaque,{TypeName,TypeDef,Args}}, St) -> type_def(opaque, A, TypeName, TypeDef, Args, St); function_state({attribute,A,spec,{Fun,Types}}, St) -> spec_decl(A, Fun, Types, St); function_state({attribute,_A,dialyzer,_Val}, St) -> St; function_state({attribute,Aa,Attr,_Val}, St) -> add_error(Aa, {attribute,Attr}, St); function_state({function,Anno,N,A,Cs}, St) -> function(Anno, N, A, Cs, St); function_state({eof,Location}, St) -> eof(Location, St). %% eof(LastLocation, State) -> %% State' eof(_Location, St0) -> St0. %% bif_clashes(Forms, State0) -> State. bif_clashes(Forms, #lint{nowarn_bif_clash=Nowarn} = St) -> Clashes0 = [{Name,Arity} || {function,_A,Name,Arity,_Cs} <- Forms, erl_internal:bif(Name, Arity)], Clashes = ordsets:subtract(ordsets:from_list(Clashes0), Nowarn), St#lint{clashes=Clashes}. %% not_deprecated(Forms, State0) -> State not_deprecated(Forms, #lint{compile=Opts}=St0) -> %% There are no line numbers in St0#lint.compile. MFAsAnno = [{MFA,Anno} || {attribute, Anno, compile, Args} <- Forms, {nowarn_deprecated_function, MFAs0} <- lists:flatten([Args]), MFA <- lists:flatten([MFAs0])], Nowarn = [MFA || {nowarn_deprecated_function, MFAs0} <- Opts, MFA <- lists:flatten([MFAs0])], MAnno = [{M,Anno} || {{M,_F,_A},Anno} <- MFAsAnno, is_atom(M)], St1 = foldl(fun ({M,Anno}, St2) -> check_module_name(M, Anno, St2) end, St0, MAnno), St1#lint{not_deprecated = ordsets:from_list(Nowarn)}. %% not_removed(Forms, State0) -> State not_removed(Forms, #lint{compile=Opts}=St0) -> %% There are no line numbers in St0#lint.compile. MFAsAnno = [{MFA,Anno} || {attribute, Anno, compile, Args} <- Forms, {nowarn_removed, MFAs0} <- lists:flatten([Args]), MFA <- lists:flatten([MFAs0])], Nowarn = [MFA || {nowarn_removed, MFAs0} <- Opts, MFA <- lists:flatten([MFAs0])], St1 = foldl(fun ({{M, _F, _A}, Anno}, St2) -> check_module_name(M, Anno, St2); ({M,Anno}, St2) -> check_module_name(M, Anno, St2) end, St0, MFAsAnno), St1#lint{not_removed = gb_sets:from_list(Nowarn)}. %% 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)] ], 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}. %% post_traversal_check(Forms, State0) -> State. %% Do some further checking after the forms have been traversed and %% data about calls etc. have been collected. post_traversal_check(Forms, St0) -> St1 = check_behaviour(St0), St2 = check_deprecated(Forms, St1), St3 = check_imports(Forms, St2), St4 = check_inlines(Forms, St3), St5 = check_undefined_functions(St4), St6 = check_unused_functions(Forms, St5), St7 = check_bif_clashes(Forms, St6), St8 = check_specs_without_function(St7), St9 = check_functions_without_spec(Forms, St8), StA = check_undefined_types(St9), StB = check_unused_types(Forms, StA), StC = check_untyped_records(Forms, StB), StD = check_on_load(StC), StE = check_unused_records(Forms, StD), StF = check_local_opaque_types(StE), StG = check_dialyzer_attribute(Forms, StF), StH = check_callback_information(StG), StI = check_nifs(Forms, StH), check_removed(Forms, StI). %% check_behaviour(State0) -> State %% Check that the behaviour attribute is valid. check_behaviour(St0) -> behaviour_check(St0#lint.behaviour, St0). %% behaviour_check([{Anno,Behaviour}], State) -> State' %% Check behaviours for existence and defined functions. behaviour_check(Bs, St0) -> {AllBfs0, St1} = all_behaviour_callbacks(Bs, [], St0), St = behaviour_missing_callbacks(AllBfs0, St1), Exports = exports(St0), F = fun(Bfs, OBfs) -> [B || B <- Bfs, not lists:member(B, OBfs) orelse gb_sets:is_member(B, Exports)] end, %% After fixing missing callbacks new warnings may be emitted. AllBfs = [{Item,F(Bfs0, OBfs0)} || {Item,Bfs0,OBfs0} <- AllBfs0], behaviour_conflicting(AllBfs, St). all_behaviour_callbacks([{Anno,B}|Bs], Acc, St0) -> {Bfs0,OBfs0,St} = behaviour_callbacks(Anno, B, St0), all_behaviour_callbacks(Bs, [{{Anno,B},Bfs0,OBfs0}|Acc], St); all_behaviour_callbacks([], Acc, St) -> {reverse(Acc),St}. behaviour_callbacks(Anno, B, St0) -> try B:behaviour_info(callbacks) of undefined -> St1 = add_warning(Anno, {undefined_behaviour_callbacks, B}, St0), {[], [], St1}; Funcs -> case is_fa_list(Funcs) of true -> try B:behaviour_info(optional_callbacks) of undefined -> {Funcs, [], St0}; OptFuncs -> %% OptFuncs should always be OK thanks to %% sys_pre_expand. case is_fa_list(OptFuncs) of true -> {Funcs, OptFuncs, St0}; false -> W = {ill_defined_optional_callbacks, B}, St1 = add_warning(Anno, W, St0), {Funcs, [], St1} end catch _:_ -> {Funcs, [], St0} end; false -> St1 = add_warning(Anno, {ill_defined_behaviour_callbacks, B}, St0), {[], [], St1} end catch _:_ -> St1 = add_warning(Anno, {undefined_behaviour, B}, St0), St2 = check_module_name(B, Anno, St1), {[], [], St2} end. behaviour_missing_callbacks([{{Anno,B},Bfs0,OBfs}|T], St0) -> Bfs = ordsets:subtract(ordsets:from_list(Bfs0), ordsets:from_list(OBfs)), Exports = gb_sets:to_list(exports(St0)), Missing = ordsets:subtract(Bfs, Exports), St = foldl(fun (F, S0) -> case is_fa(F) of true -> M = {undefined_behaviour_func,F,B}, add_warning(Anno, M, S0); false -> S0 % ill_defined_behaviour_callbacks end end, St0, Missing), behaviour_missing_callbacks(T, St); behaviour_missing_callbacks([], St) -> St. behaviour_conflicting(AllBfs, St) -> R0 = sofs:relation(AllBfs, [{item,[callback]}]), R1 = sofs:family_to_relation(R0), R2 = sofs:converse(R1), R3 = sofs:relation_to_family(R2), R4 = sofs:family_specification(fun(S) -> sofs:no_elements(S) > 1 end, R3), R = sofs:to_external(R4), behaviour_add_conflicts(R, St). behaviour_add_conflicts([{Cb,[{FirstAnno,FirstB}|Cs]}|T], St0) -> FirstL = element(2, 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_warning(Anno, {conflicting_behaviours,Cb,B,FirstL,FirstB}, St0), behaviour_add_conflict(Cs, Cb, FirstL, FirstB, St); behaviour_add_conflict([], _, _, _, St) -> St. %% check_deprecated(Forms, State0) -> State check_deprecated(Forms, St0) -> Exports = exports(St0), X = ignore_predefined_funcs(gb_sets:to_list(Exports)), #lint{module = Mod} = St0, Bad = [{E,Anno} || {attribute, Anno, deprecated, Depr} <- Forms, D <- lists:flatten([Depr]), E <- depr_cat(D, X, Mod)], foldl(fun ({E,Anno}, St1) -> add_error(Anno, E, St1) end, St0, Bad). depr_cat({F, A, Flg}=D, X, Mod) -> case deprecated_flag(Flg) of false -> [{invalid_deprecated,D}]; true -> depr_fa(F, A, X, Mod) end; depr_cat({F, A}, X, Mod) -> depr_fa(F, A, X, Mod); depr_cat(module, _X, _Mod) -> []; depr_cat(D, _X, _Mod) -> [{invalid_deprecated,D}]. depr_fa('_', '_', _X, _Mod) -> []; depr_fa(F, '_', X, _Mod) when is_atom(F) -> %% Don't use this syntax for built-in functions. case lists:filter(fun({F1,_}) -> F1 =:= F end, X) of [] -> [{bad_deprecated,{F,'_'}}]; _ -> [] end; depr_fa(F, A, X, Mod) when is_atom(F), is_integer(A), A >= 0 -> case lists:member({F,A}, X) of true -> []; false -> case erlang:is_builtin(Mod, F, A) of true -> []; false -> [{bad_deprecated,{F,A}}] end end; depr_fa(F, A, _X, _Mod) -> [{invalid_deprecated,{F,A}}]. deprecated_flag(next_version) -> true; deprecated_flag(next_major_release) -> true; deprecated_flag(eventually) -> true; deprecated_flag(String) -> deprecated_desc(String). deprecated_desc([Char | Str]) when is_integer(Char) -> deprecated_desc(Str); deprecated_desc([]) -> true; deprecated_desc(_) -> false. %% check_removed(Forms, State0) -> State check_removed(Forms, St0) -> Exports = exports(St0), X = ignore_predefined_funcs(gb_sets:to_list(Exports)), #lint{module = Mod} = St0, Bad = [{E,Anno} || {attribute, Anno, removed, Removed} <- Forms, R <- lists:flatten([Removed]), E <- removed_cat(R, X, Mod)], foldl(fun ({E,Anno}, St1) -> add_error(Anno, E, St1) end, St0, Bad). removed_cat({F, A, Desc}=R, X, Mod) -> case removed_desc(Desc) of false -> [{invalid_removed,R}]; true -> removed_fa(F, A, X, Mod) end; removed_cat({F, A}, X, Mod) -> removed_fa(F, A, X, Mod); removed_cat(module, X, Mod) -> removed_fa('_', '_', X, Mod); removed_cat(R, _X, _Mod) -> [{invalid_removed,R}]. removed_fa('_', '_', X, _Mod) -> case X of [_|_] -> [{bad_removed,{'_','_'}}]; [] -> [] end; removed_fa(F, '_', X, _Mod) when is_atom(F) -> %% Don't use this syntax for built-in functions. case lists:filter(fun({F1,_}) -> F1 =:= F end, X) of [_|_] -> [{bad_removed,{F,'_'}}]; _ -> [] end; removed_fa(F, A, X, Mod) when is_atom(F), is_integer(A), A >= 0 -> case lists:member({F,A}, X) of true -> [{bad_removed,{F,A}}]; false -> case erlang:is_builtin(Mod, F, A) of true -> [{bad_removed,{F,A}}]; false -> [] end end; removed_fa(F, A, _X, _Mod) -> [{invalid_removed,{F,A}}]. removed_desc([Char | Str]) when is_integer(Char) -> removed_desc(Str); removed_desc([]) -> true; removed_desc(_) -> false. %% Ignores functions added by erl_internal:add_predefined_functions/1 ignore_predefined_funcs([{behaviour_info,1} | Fs]) -> ignore_predefined_funcs(Fs); ignore_predefined_funcs([{module_info,0} | Fs]) -> ignore_predefined_funcs(Fs); ignore_predefined_funcs([{module_info,1} | Fs]) -> ignore_predefined_funcs(Fs); ignore_predefined_funcs([Other | Fs]) -> [Other | ignore_predefined_funcs(Fs)]; ignore_predefined_funcs([]) -> []. %% check_imports(Forms, State0) -> State check_imports(Forms, St0) -> case is_warn_enabled(unused_import, St0) of false -> St0; true -> Usage = St0#lint.usage, Unused = ordsets:subtract(St0#lint.imports, Usage#usage.imported), Imports = [{{FA,Mod},Anno} || {attribute,Anno,import,{Mod,Fs}} <- Forms, FA <- lists:usort(Fs)], Bad = [{FM,Anno} || FM <- Unused, {FM2,Anno} <- Imports, FM =:= FM2], func_location_warning(unused_import, Bad, St0) end. %% check_inlines(Forms, State0) -> State check_inlines(Forms, St0) -> check_option_functions(Forms, inline, bad_inline, St0). %% check_unused_functions(Forms, State0) -> State check_unused_functions(Forms, St0) -> St1 = check_option_functions(Forms, nowarn_unused_function, bad_nowarn_unused_function, St0), Opts = St1#lint.compile, case member(export_all, Opts) orelse not is_warn_enabled(unused_function, St1) of true -> St1; false -> Nowarn = nowarn_function(nowarn_unused_function, Opts), Usage = St1#lint.usage, Used = reached_functions(initially_reached(St1), Usage#usage.calls), UsedOrNowarn = ordsets:union(Used, Nowarn), Unused = ordsets:subtract(gb_sets:to_list(St1#lint.defined), UsedOrNowarn), Functions = [{{N,A},Anno} || {function,Anno,N,A,_} <- Forms], Bad = [{FA,Anno} || FA <- Unused, {FA2,Anno} <- Functions, FA =:= FA2], func_location_warning(unused_function, Bad, St1) end. initially_reached(#lint{exports=Exp,on_load=OnLoad}) -> OnLoad ++ gb_sets:to_list(Exp). %% reached_functions(RootSet, CallRef) -> [ReachedFunc]. %% reached_functions(RootSet, CallRef, [ReachedFunc]) -> [ReachedFunc]. reached_functions(Root, Ref) -> reached_functions(Root, [], Ref, gb_sets:empty()). reached_functions([R|Rs], More0, Ref, Reached0) -> case gb_sets:is_element(R, Reached0) of true -> reached_functions(Rs, More0, Ref, Reached0); false -> Reached = gb_sets:add_element(R, Reached0), %It IS reached case maps:find(R, Ref) of {ok,More} -> reached_functions(Rs, [More|More0], Ref, Reached); error -> reached_functions(Rs, More0, Ref, Reached) end end; reached_functions([], [_|_]=More, Ref, Reached) -> reached_functions(lists:append(More), [], Ref, Reached); reached_functions([], [], _Ref, Reached) -> gb_sets:to_list(Reached). %% check_undefined_functions(State0) -> State check_undefined_functions(#lint{called=Called0,defined=Def0}=St0) -> Called = sofs:relation(Called0, [{func,location}]), Def = sofs:from_external(gb_sets:to_list(Def0), [func]), Undef = sofs:to_external(sofs:drestriction(Called, Def)), foldl(fun ({NA,Anno}, St) -> add_error(Anno, {undefined_function,NA}, St) end, St0, Undef). %% check_undefined_types(State0) -> State check_undefined_types(#lint{usage=Usage,types=Def}=St0) -> Used = Usage#usage.used_types, UTAs = maps:keys(Used), Undef = [{TA,map_get(TA, Used)} || TA <- UTAs, not is_map_key(TA, Def), not is_default_type(TA)], foldl(fun ({TA,UsedTypeList}, St) -> foldl( fun(#used_type{anno = Anno}, St1) -> add_error(Anno, {undefined_type,TA}, St1) end, St, UsedTypeList) end, St0, Undef). %% check_bif_clashes(Forms, State0) -> State check_bif_clashes(Forms, St0) -> %% St0#lint.defined is now complete. check_option_functions(Forms, nowarn_bif_clash, bad_nowarn_bif_clash, St0). check_option_functions(Forms, Tag0, Type, St0) -> %% There are no line numbers in St0#lint.compile. FAsAnno = [{FA,Anno} || {attribute, Anno, compile, Args} <- Forms, {Tag, FAs0} <- lists:flatten([Args]), Tag0 =:= Tag, FA <- lists:flatten([FAs0])], DefFunctions = (gb_sets:to_list(St0#lint.defined) -- pseudolocals()) ++ [{F,A} || {{F,A},_} <- orddict:to_list(St0#lint.imports)], Bad = [{FA,Anno} || {FA,Anno} <- FAsAnno, not member(FA, DefFunctions)], func_location_error(Type, Bad, St0). check_nifs(Forms, St0) -> FAsAnno = [{FA,Anno} || {attribute, Anno, nifs, Args} <- Forms, FA <- Args], St1 = case {FAsAnno, St0#lint.load_nif} of {[{_,Anno1}|_], false} -> add_warning(Anno1, no_load_nif, St0); _ -> St0 end, DefFunctions = gb_sets:subtract(St1#lint.defined, gb_sets:from_list(pseudolocals())), Bad = [{FA,Anno} || {FA,Anno} <- FAsAnno, not gb_sets:is_element(FA, DefFunctions)], func_location_error(undefined_nif, Bad, St1). nowarn_function(Tag, Opts) -> ordsets:from_list([FA || {Tag1,FAs} <- Opts, Tag1 =:= Tag, FA <- lists:flatten([FAs])]). func_location_warning(Type, Fs, St) -> foldl(fun ({F,Anno}, St0) -> add_warning(Anno, {Type,F}, St0) end, St, Fs). func_location_error(Type, Fs, St) -> foldl(fun ({F,Anno}, St0) -> add_error(Anno, {Type,F}, St0) end, St, Fs). check_untyped_records(Forms, St0) -> case is_warn_enabled(untyped_record, St0) of true -> %% Use the names of all records *defined* in the module (not used) RecNames = maps:keys(St0#lint.records), %% these are the records with field(s) containing type info TRecNames = [Name || {attribute,_,record,{Name,Fields}} <- Forms, lists:all(fun ({typed_record_field,_,_}) -> true; (_) -> false end, Fields)], foldl(fun (N, St) -> {Anno, Fields} = map_get(N, St0#lint.records), case Fields of [] -> St; % exclude records with no fields [_|_] -> add_warning(Anno, {untyped_record, N}, St) end end, St0, ordsets:subtract(ordsets:from_list(RecNames), ordsets:from_list(TRecNames))); false -> St0 end. check_unused_records(Forms, St0) -> AttrFiles = [File || {attribute,_A,file,{File,_Line}} <- Forms], case {is_warn_enabled(unused_record, St0),AttrFiles} of {true,[FirstFile|_]} -> %% There are no line numbers in St0#lint.compile. RecAnno = [{Rec, Anno} || {attribute, Anno, compile, Args} <- Forms, {nowarn_unused_record, Recs0} <- lists:flatten([Args]), Rec <- lists:flatten([Recs0])], St1 = foldl(fun ({Rec, Anno}, St2) -> exist_record(Anno, Rec, St2) end, St0, RecAnno), %% The check is a bit imprecise in that uses from unused %% functions count. Usage = St1#lint.usage, UsedRecords = Usage#usage.used_records, URecs = gb_sets:fold(fun (Used, Recs) -> maps:remove(Used, Recs) end, St1#lint.records, UsedRecords), Unused = [{Name,Anno} || Name := {Anno,_Fields} <- URecs, element(1, loc(Anno, St1)) =:= FirstFile], foldl(fun ({N,Anno}, St) -> add_warning(Anno, {unused_record, N}, St) end, St1, Unused); _ -> St0 end. check_callback_information(#lint{callbacks = Callbacks, optional_callbacks = OptionalCbs, defined = Defined} = St0) -> OptFun = fun(MFA, Anno, St) -> case is_map_key(MFA, Callbacks) of true -> St; false -> add_error(Anno, {undefined_callback, MFA}, St) end end, St1 = maps:fold(OptFun, St0, OptionalCbs), case gb_sets:is_member({behaviour_info, 1}, Defined) of false -> St1; true -> case map_size(Callbacks) of 0 -> St1; _ -> FoldFun = fun(Fa, Anno, St) -> add_error(Anno, {behaviour_info, Fa}, St) end, maps:fold(FoldFun, St1, Callbacks) end end. %% For storing the import list we use the orddict module. %% We know an empty set is []. -spec export(anno(), [fa()], lint_state()) -> lint_state(). %% Mark functions as exported, also as called from the export line. export(Anno, Es, #lint{exports = Es0, called = Called} = St0) -> {Es1,C1,St1} = foldl(fun (NA, {E,C,St2}) -> St = case gb_sets:is_element(NA, E) of true -> Warn = {duplicated_export,NA}, add_warning(Anno, Warn, St2); false -> St2 end, {gb_sets:add_element(NA, E), [{NA,Anno}|C], St} end, {Es0,Called,St0}, Es), St1#lint{exports = Es1, called = C1}. -spec export_type(anno(), [ta()], lint_state()) -> lint_state(). %% Mark types as exported; also mark them as used from the export line. export_type(Anno, ETs, #lint{exp_types = ETs0} = St0) -> try foldl(fun ({T,A}=TA, {E,St2}) when is_atom(T), is_integer(A) -> St = case gb_sets:is_element(TA, E) of true -> Warn = {duplicated_export_type,TA}, add_warning(Anno, Warn, St2); false -> St3 = St2#lint{type_id = {export, []}}, used_type(TA, Anno, St3) end, {gb_sets:add_element(TA, E), St} end, {ETs0,St0}, ETs) of {ETs1,St1} -> St1#lint{exp_types = ETs1} catch error:_ -> add_error(Anno, {bad_export_type, ETs}, St0) end. -spec exports(lint_state()) -> gb_sets:set(fa()). exports(#lint{compile = Opts, defined = Defs, exports = Es}) -> case lists:member(export_all, Opts) of true -> Defs; false -> Es end. -type import() :: {module(), [fa()]} | module(). -spec import(anno(), import(), lint_state()) -> lint_state(). import(Anno, {Mod,Fs}, St00) -> St = check_module_name(Mod, Anno, St00), Mfs = ordsets:from_list(Fs), case check_imports(Anno, Mfs, St#lint.imports) of [] -> St#lint{imports=add_imports(Mod, Mfs, St#lint.imports)}; Efs -> {Err, St1} = foldl(fun ({bif,{F,A},_}, {Err,St0}) -> %% BifClash - import directive Warn = is_warn_enabled(bif_clash, St0) andalso (not bif_clash_specifically_disabled(St0,{F,A})), AutoImpSup = is_autoimport_suppressed(St0#lint.no_auto,{F,A}), OldBif = erl_internal:old_bif(F,A), {Err,if Warn and (not AutoImpSup) and OldBif -> add_error (Anno, {redefine_old_bif_import, {F,A}}, St0); Warn and (not AutoImpSup) -> add_warning (Anno, {redefine_bif_import, {F,A}}, St0); true -> St0 end}; (Ef, {_Err,St0}) -> {true,add_error(Anno, {redefine_import,Ef}, St0)} end, {false,St}, Efs), if not Err -> St1#lint{imports=add_imports(Mod, Mfs, St#lint.imports)}; true -> St1 end end. check_imports(_Anno, Fs, Is) -> foldl(fun (F, Efs) -> case orddict:find(F, Is) of {ok,Mod} -> [{F,Mod}|Efs]; error -> {N,A} = F, case erl_internal:bif(N, A) of true -> [{bif,F,erlang}|Efs]; false -> Efs end end end, [], Fs). add_imports(Mod, Fs, Is) -> foldl(fun (F, Is0) -> orddict:store(F, Mod, Is0) end, Is, Fs). -spec imported(atom(), arity(), lint_state()) -> {'yes',module()} | 'no'. imported(F, A, St) -> case orddict:find({F,A}, St#lint.imports) of {ok,Mod} -> {yes,Mod}; error -> no end. -spec on_load(erl_anno:anno(), fa(), lint_state()) -> lint_state(). %% Check an on_load directive and remember it. on_load(Anno, {Name,Arity}=Fa, #lint{on_load=OnLoad0}=St0) when is_atom(Name), is_integer(Arity) -> %% Always add the function name (even if there is a problem), %% to avoid irrelevant warnings for unused functions. St = St0#lint{on_load=[Fa|OnLoad0],on_load_anno=Anno}, case St of #lint{on_load=[{_,0}]} -> %% This is the first on_load attribute seen in the module %% and it has the correct arity. St; #lint{on_load=[{_,_}]} -> %% Wrong arity. add_error(Anno, {bad_on_load_arity,Fa}, St); #lint{on_load=[_,_|_]} -> %% Multiple on_load attributes. add_error(Anno, multiple_on_loads, St) end; on_load(Anno, Val, St) -> %% Bad syntax. add_error(Anno, {bad_on_load,Val}, St). check_on_load(#lint{defined=Defined,on_load=[{_,0}=Fa], on_load_anno=Anno}=St) -> case gb_sets:is_member(Fa, Defined) of true -> St; false -> add_error(Anno, {undefined_on_load,Fa}, St) end; check_on_load(St) -> St. -spec call_function(anno(), atom(), arity(), lint_state()) -> lint_state(). %% Add to both called and calls. call_function(Anno0, F, A, #lint{usage=Usage0,called=Cd,func=Func,file=File}=St) -> #usage{calls = Cs} = Usage0, NA = {F,A}, Usage = case Cs of undefined -> Usage0; _ -> Usage0#usage{calls=maps_prepend(Func, NA, Cs)} end, Anno = erl_anno:set_file(File, Anno0), St#lint{called=[{NA,Anno}|Cd], usage=Usage}. %% function(Anno, Name, Arity, Clauses, State) -> State. function(Anno, Name, Arity, Cs, St0) -> St1 = St0#lint{func={Name,Arity}}, St2 = define_function(Anno, Name, Arity, St1), clauses(Cs, St2). -spec define_function(anno(), atom(), arity(), lint_state()) -> lint_state(). define_function(Anno, Name, Arity, St0) -> St1 = keyword_warning(Anno, Name, St0), NA = {Name,Arity}, case gb_sets:is_member(NA, St1#lint.defined) of true -> add_error(Anno, {redefine_function,NA}, St1); false -> St2 = function_check_max_args(Anno, Arity, St1), St3 = St2#lint{defined=gb_sets:add_element(NA, St2#lint.defined)}, case imported(Name, Arity, St3) of {yes,_M} -> add_error(Anno, {define_import,NA}, St3); no -> St3 end end. function_check_max_args(Anno, Arity, St) when Arity > ?MAX_ARGUMENTS -> add_error(Anno, {too_many_arguments,Arity}, St); function_check_max_args(_, _, St) -> St. %% clauses([Clause], State) -> {VarTable, State}. clauses(Cs, St) -> foldl(fun (C, St0) -> {_,St1} = clause(C, St0), St1 end, St, Cs). clause({clause,_Anno,H,G,B}, St0) -> Vt0 = [], {Hvt,Hnew,St1} = head(H, Vt0, St0), Vt1 = vtupdate(Hvt, vtupdate(Hnew, Vt0)), {Gvt,St2} = guard(G, Vt1, St1), Vt2 = vtupdate(Gvt, Vt1), {Bvt,St3} = exprs(B, Vt2, St2), Upd = vtupdate(Bvt, Vt2), check_unused_vars(Upd, Vt0, St3). %% head([HeadPattern], VarTable, State) -> %% {VarTable,NewVars,State} %% Check a patterns in head returning "all" variables. Not updating the %% known variable list will result in multiple error messages/warnings. head(Ps, Vt, St0) -> head(Ps, Vt, Vt, St0). % Old = Vt head([P|Ps], Vt0, Old, St0) -> {Pvt, Pnew, St1} = pattern(P, Vt0, Old, St0), {Psvt, Psnew, St2} = head(Ps, Vt0, Old, St1), {Vt, St3} = vtmerge_pat(Pvt, Psvt, St2), {New, St4} = vtmerge_pat(Pnew, Psnew, St3), {Vt, New, St4}; head([], _Vt, _Env, St) -> {[],[],St}. %% pattern(Pattern, VarTable, Old, State) -> %% {UpdVarTable,NewVars,State}. %% Check pattern return variables. VarTable is the set of variables %% outside the pattern, used for map keys and binary sizes. Old is empty %% if vars are shadowing, as in fun heads and comprehension generators, or %% is otherwise equal to VarTable; this is used for deciding whether an %% occurrence is a binding occurrence or a use. %% UpdVarTable is updated when a previously existing variable is used in %% the pattern. New variables and their uses are recorded in NewVars. The %% caller can then decide what to do with it depending on whether or not %% variables in the pattern shadow old variables. This separation is %% one way of dealing with these: %% A = 4, fun(<>) -> % A #2 unused %% A = 4, fun(<>) -> % A #1 unused pattern(P, Vt, St) -> pattern(P, Vt, Vt, St). % Old = Vt pattern({var,_Anno,'_'}, _Vt, _Old, St) -> {[],[],St}; %Ignore anonymous variable pattern({var,Anno,V}, _Vt, Old, St) -> pat_var(V, Anno, Old, [], St); pattern({char,_Anno,_C}, _Vt, _Old, St) -> {[],[],St}; pattern({integer,_Anno,_I}, _Vt, _Old, St) -> {[],[],St}; pattern({float,_Anno,_F}, _Vt, _Old, St) -> {[],[],St}; pattern({atom,Anno,A}, _Vt, _Old, St) -> {[],[],keyword_warning(Anno, A, St)}; pattern({string,_Anno,_S}, _Vt, _Old, St) -> {[],[],St}; pattern({nil,_Anno}, _Vt, _Old, St) -> {[],[],St}; pattern({cons,_Anno,H,T}, Vt0, Old, St0) -> {Hvt, Hnew, St1} = pattern(H, Vt0, Old, St0), {Tvt, Tnew, St2} = pattern(T, Vt0, Old, St1), {Vt1, St3} = vtmerge_pat(Hvt, Tvt, St2), {New, St4} = vtmerge_pat(Hnew, Tnew, St3), {Vt1, New, St4}; pattern({tuple,_Anno,Ps}, Vt, Old, St) -> pattern_list(Ps, Vt, Old, St); pattern({map,_Anno,Ps}, Vt, Old, St) -> pattern_map(Ps, Vt, Old, St); pattern({record_index,Anno,Name,Field}, _Vt, _Old, St) -> {Vt1,St1} = check_record(Anno, Name, St, fun (Dfs, St1) -> pattern_field(Field, Name, Dfs, St1) end), {Vt1,[],St1}; pattern({record,Anno,Name,Pfs}, Vt, Old, St) -> case maps:find(Name, St#lint.records) of {ok,{_Anno,Fields}} -> St1 = used_record(Name, St), St2 = check_multi_field_init(Pfs, Anno, Fields, St1), pattern_fields(Pfs, Name, Fields, Vt, Old, St2); error -> {[],[],add_error(Anno, {undefined_record,Name}, St)} end; pattern({bin,_,Fs}, Vt, Old, St) -> pattern_bin(Fs, Vt, Old, St); pattern({op,_Anno,'++',{nil,_},R}, Vt, Old, St) -> pattern(R, Vt, Old, St); pattern({op,_Anno,'++',{cons,Ai,{char,_A2,_C},T},R}, Vt, Old, St) -> pattern({op,Ai,'++',T,R}, Vt, Old, St); %Char unimportant here pattern({op,_Anno,'++',{cons,Ai,{integer,_A2,_I},T},R}, Vt, Old, St) -> pattern({op,Ai,'++',T,R}, Vt, Old, St); %Weird, but compatible! pattern({op,_Anno,'++',{string,_Ai,_S},R}, Vt, Old, St) -> pattern(R, Vt, Old, St); %String unimportant here pattern({match,_Anno,Pat1,Pat2}, Vt0, Old, St0) -> {Lvt, Lnew, St1} = pattern(Pat1, Vt0, Old, St0), {Rvt, Rnew, St2} = pattern(Pat2, Vt0, Old, St1), {Vt1, St3} = vtmerge_pat(Lvt, Rvt, St2), {New, St4} = vtmerge_pat(Lnew, Rnew, St3), {Vt1, New, St4}; %% Catch legal constant expressions, including unary +,-. pattern(Pat, _Vt, _Old, St) -> case is_pattern_expr(Pat) of true -> {[],[],St}; false -> {[],[],add_error(element(2, Pat), illegal_pattern, St)} end. pattern_list(Ps, Vt0, Old, St) -> foldl(fun (P, {Psvt, Psnew,St0}) -> {Pvt, Pnew, St1} = pattern(P, Vt0, Old, St0), {Vt1, St2} = vtmerge_pat(Pvt, Psvt, St1), {New, St3} = vtmerge_pat(Psnew, Pnew, St2), {Vt1, New, St3} end, {[],[],St}, Ps). %% Check for '_' initializing no fields. check_multi_field_init(Fs, Anno, Fields, St) -> case init_fields(Fs, Anno, Fields) =:= [] of true -> case has_wildcard_field(Fs) of no -> St; WildAnno -> add_error(WildAnno, bad_multi_field_init, St) end; false -> St end. %% is_pattern_expr(Expression) -> boolean(). %% Test if a general expression is a valid pattern expression. is_pattern_expr(Expr) -> case is_pattern_expr_1(Expr) of false -> false; true -> %% Expression is syntactically correct - make sure that it %% also can be evaluated. case erl_eval:partial_eval(Expr) of {integer,_,_} -> true; {char,_,_} -> true; {float,_,_} -> true; {atom,_,_} -> true; _ -> false end end. is_pattern_expr_1({char,_Anno,_C}) -> true; is_pattern_expr_1({integer,_Anno,_I}) -> true; is_pattern_expr_1({float,_Anno,_F}) -> true; is_pattern_expr_1({atom,_Anno,_A}) -> true; is_pattern_expr_1({tuple,_Anno,Es}) -> all(fun is_pattern_expr_1/1, Es); is_pattern_expr_1({nil,_Anno}) -> true; is_pattern_expr_1({cons,_Anno,H,T}) -> is_pattern_expr_1(H) andalso is_pattern_expr_1(T); is_pattern_expr_1({op,_Anno,Op,A}) -> erl_internal:arith_op(Op, 1) andalso is_pattern_expr_1(A); is_pattern_expr_1({op,_Anno,Op,A1,A2}) -> erl_internal:arith_op(Op, 2) andalso all(fun is_pattern_expr_1/1, [A1,A2]); is_pattern_expr_1(_Other) -> false. pattern_map(Ps, Vt0, Old, St0) -> foldl(fun({map_field_assoc,A,_,_}, {Psvt,Psnew,St1}) -> {Psvt,Psnew,add_error(A, illegal_pattern, St1)}; ({map_field_exact,_A,K,V}, {Psvt,Psnew,St1}) -> St2 = St1#lint{gexpr_context=map_key}, {Kvt, St3} = gexpr(K, Vt0, St2), {Vvt, Vnew, St4} = pattern(V, Vt0, Old, St3), {Vt1, St5} = vtmerge_pat(Kvt, Vvt, St4), {Vt2, St6} = vtmerge_pat(Vt1, Psvt, St5), {New, St7} = vtmerge_pat(Psnew, Vnew, St6), {Vt2, New, St7} end, {[],[], St0}, Ps). %% pattern_bin([Element], VarTable, Old, State) -> %% {UpdVarTable,NewVars,State}. %% Check a pattern group. pattern_bin(Es, Vt, Old, St0) -> {_,Esvt,Esnew,St1} = foldl(fun (E, Acc) -> pattern_element(E, Vt, Old, Acc) end, {{0,0},[],[],St0}, Es), {Esvt,Esnew,St1}. pattern_element({bin_element,Anno,{string,_,_},Size,Ts}=Be, Vt, Old, {Sz,Esvt,Esnew,St0}=Acc) -> case good_string_size_type(Size, Ts) of true -> pattern_element_1(Be, Vt, Old, Acc); false -> St = add_error(Anno, typed_literal_string, St0), {Sz,Esvt,Esnew,St} end; pattern_element(Be, Vt, Old, Acc) -> pattern_element_1(Be, Vt, Old, Acc). pattern_element_1({bin_element,Anno,E,Sz0,Ts}, Vt, Old, {{PrevSize,PrevAnno},Esvt,Esnew,St0}) -> {Pevt,Penew,St1} = pat_bit_expr(E, Old, Esnew, St0), {Sz1,Szvt,Sznew,St2} = pat_bit_size(Sz0, Vt, Esnew, St1), {Sz2,Bt,St3} = bit_type(Anno, Sz1, Ts, St2), {Sz3,St4} = bit_size_check(Anno, Sz2, Bt, St3), Sz4 = case {E,Sz3} of {{string,_,S},all} -> 8*length(S); {_,_} -> Sz3 end, St5 = case PrevSize of all -> add_error(PrevAnno, unsized_binary_not_at_end, St4); _ -> St4 end, {{Sz4,Anno},vtmerge(Szvt,vtmerge(Pevt, Esvt)), vtmerge(Sznew,vtmerge(Esnew, Penew)), St5}. good_string_size_type(default, default) -> true; good_string_size_type(default, Ts) -> lists:any(fun(utf8) -> true; (utf16) -> true; (utf32) -> true; (_) -> false end, Ts); good_string_size_type(_, _) -> false. %% pat_bit_expr(Pattern, OldVarTable, NewVars, State) -> %% {UpdVarTable,UpdNewVars,State}. %% Check pattern bit expression, only allow really valid patterns! pat_bit_expr({var,_,'_'}, _Old, _New, St) -> {[],[],St}; pat_bit_expr({var,Anno,V}, Old, New, St) -> pat_var(V, Anno, Old, New, St); pat_bit_expr({string,_,_}, _Old, _new, St) -> {[],[],St}; pat_bit_expr({bin,A,_}, _Old, _New, St) -> {[],[],add_error(A, illegal_pattern, St)}; pat_bit_expr(P, _Old, _New, St) -> case is_pattern_expr(P) of true -> {[],[],St}; false -> {[],[],add_error(element(2, P), illegal_pattern, St)} end. %% pat_bit_size(Size, VarTable, NewVars, State) -> %% {Value,UpdVarTable,UpdNewVars,State}. %% Check pattern size expression, only allow really valid sizes! pat_bit_size(default, _Vt, _New, St) -> {default,[],[],St}; pat_bit_size({var,Anno,V}, Vt0, New0, St0) -> {Vt,New,St1} = pat_binsize_var(V, Anno, Vt0, New0, St0), {unknown,Vt,New,St1}; pat_bit_size(Size, Vt0, New0, St0) -> Anno = element(2, Size), case erl_eval:partial_eval(Size) of {integer,Anno,I} -> {I,[],[],St0}; Expr -> %% The size is an expression using operators %% and/or guard BIFs calls. If the expression %% happens to evaluate to a non-integer value, the %% pattern will fail to match. St1 = St0#lint{bvt=New0,gexpr_context=bin_seg_size}, {Vt,#lint{bvt=New}=St2} = gexpr(Size, Vt0, St1), St3 = St2#lint{bvt=none,gexpr_context=St0#lint.gexpr_context}, St = case is_bit_size_illegal(Expr) of true -> %% The size is a non-integer literal or a simple %% expression that does not evaluate to an %% integer value. Issue a warning. add_warning(Anno, non_integer_bitsize, St3); false -> St3 end, {unknown,Vt,New,St} end. is_bit_size_illegal({atom,_,_}) -> true; is_bit_size_illegal({bin,_,_}) -> true; is_bit_size_illegal({cons,_,_,_}) -> true; is_bit_size_illegal({float,_,_}) -> true; is_bit_size_illegal({map,_,_}) -> true; is_bit_size_illegal({nil,_}) -> true; is_bit_size_illegal({tuple,_,_}) -> true; is_bit_size_illegal(_) -> false. %% expr_bin([Element], VarTable, State, CheckFun) -> {UpdVarTable,State}. %% Check an expression group. expr_bin(Es, Vt, St0, Check) -> {Esvt,St1} = foldl(fun (E, Acc) -> bin_element(E, Vt, Acc, Check) end, {[],St0}, Es), {Esvt,St1}. bin_element({bin_element,Anno,E,Sz0,Ts}, Vt, {Esvt,St0}, Check) -> {Vt1,St1} = Check(E, Vt, St0), {Sz1,Vt2,St2} = bit_size(Sz0, Vt, St1, Check), {Sz2,Bt,St3} = bit_type(Anno, Sz1, Ts, St2), {_Sz3,St4} = bit_size_check(Anno, Sz2, Bt, St3), {vtmerge([Vt2,Vt1,Esvt]),St4}. bit_size(default, _Vt, St, _Check) -> {default,[],St}; bit_size({atom,_Anno,all}, _Vt, St, _Check) -> {all,[],St}; bit_size(Size, Vt, St, Check) -> %% Try to safely evaluate Size if constant to get size, %% otherwise just treat it as an expression. Info = is_guard_test2_info(St), case is_gexpr(Size, Info) of true -> case erl_eval:partial_eval(Size) of {integer,_ILn,I} -> {I,[],St}; _Other -> {Evt,St1} = Check(Size, Vt, St), {unknown,Evt,St1} end; false -> {Evt,St1} = Check(Size, Vt, St), {unknown,Evt,St1} end. %% bit_type(Anno, Size, TypeList, State) -> {Size,#bittype,St}. %% Perform warning check on type and size. bit_type(Anno, Size0, Type, St) -> case erl_bits:set_bit_type(Size0, Type) of {ok,Size1,Bt} -> {Size1,Bt,St}; {error,What} -> %% Flag error and generate a default. {ok,Size1,Bt} = erl_bits:set_bit_type(default, []), {Size1,Bt,add_error(Anno, What, St)} end. %% bit_size_check(Anno, Size, BitType, State) -> {BitSize,State}. %% Do some checking & warnings on types %% float == 16 or 32 or 64 bit_size_check(_Anno, unknown, _, St) -> {unknown,St}; bit_size_check(_Anno, undefined, #bittype{type=Type}, St) -> true = (Type =:= utf8) or (Type =:= utf16) or (Type =:= utf32), %Assertion. {undefined,St}; bit_size_check(Anno, all, #bittype{type=Type}, St) -> case Type of binary -> {all,St}; _ -> {unknown,add_error(Anno, illegal_bitsize, St)} end; bit_size_check(Anno, Size, #bittype{type=Type,unit=Unit}, St) when is_integer(Size), is_integer(Unit) -> Sz = Unit * Size, %Total number of bits! St2 = elemtype_check(Anno, Type, Sz, St), {Sz,St2}. elemtype_check(_Anno, float, 16, St) -> St; elemtype_check(_Anno, float, 32, St) -> St; elemtype_check(_Anno, float, 64, St) -> St; elemtype_check(Anno, float, _Size, St) -> add_warning(Anno, {bad_bitsize,"float"}, St); elemtype_check(_Anno, _Type, _Size, St) -> St. %% guard([GuardTest], VarTable, State) -> %% {UsedVarTable,State} %% Check a guard, return all variables. %% Disjunction of guard conjunctions guard([L|R], Vt, St0) when is_list(L) -> {Gvt, St1} = guard_tests(L, Vt, St0), {Gsvt, St2} = guard(R, vtupdate(Gvt, Vt), St1), {vtupdate(Gvt, Gsvt),St2}; guard(L, Vt, St0) -> guard_tests(L, Vt, St0). %% guard conjunction guard_tests([G|Gs], Vt, St0) -> {Gvt,St1} = guard_test(G, Vt, St0), {Gsvt,St2} = guard_tests(Gs, vtupdate(Gvt, Vt), St1), {vtupdate(Gvt, Gsvt),St2}; guard_tests([], _Vt, St) -> {[],St}. %% guard_test(Test, VarTable, State) -> %% {UsedVarTable,State'} %% Check one guard test, returns NewVariables. We now allow more %% expressions in guards including the new is_XXX type tests, but %% only allow the old type tests at the top level. guard_test(G, Vt, St0) -> St1 = obsolete_guard(G, St0), guard_test2(G, Vt, St1). %% Specially handle record type test here. guard_test2({call,Anno,{atom,Ar,record},[E,A]}, Vt, St0) -> gexpr({call,Anno,{atom,Ar,is_record},[E,A]}, Vt, St0); guard_test2({call,Anno,{atom,_Aa,F},As}=G, Vt, St0) -> {Asvt,St1} = gexpr_list(As, Vt, St0), %Always check this. A = length(As), case erl_internal:type_test(F, A) of true when F =/= is_record, A =/= 2 -> case no_guard_bif_clash(St1, {F,A}) of false -> {Asvt,add_error(Anno, {illegal_guard_local_call,{F,A}}, St1)}; true -> {Asvt,St1} end; _ -> gexpr(G, Vt, St0) end; guard_test2(G, Vt, St) -> %% Everything else is a guard expression. gexpr(G, Vt, St). %% gexpr(GuardExpression, VarTable, State) -> %% {UsedVarTable,State'} %% Check a guard expression, returns NewVariables. gexpr({var,Anno,V}, Vt, St) -> expr_var(V, Anno, Vt, St); gexpr({char,_Anno,_C}, _Vt, St) -> {[],St}; gexpr({integer,_Anno,_I}, _Vt, St) -> {[],St}; gexpr({float,_Anno,_F}, _Vt, St) -> {[],St}; gexpr({atom,Anno,A}, _Vt, St) -> {[],keyword_warning(Anno, A, St)}; gexpr({string,_Anno,_S}, _Vt, St) -> {[],St}; gexpr({nil,_Anno}, _Vt, St) -> {[],St}; gexpr({cons,_Anno,H,T}, Vt, St) -> gexpr_list([H,T], Vt, St); gexpr({tuple,_Anno,Es}, Vt, St) -> gexpr_list(Es, Vt, St); gexpr({map,_Anno,Es}, Vt, St) -> map_fields(Es, Vt, check_assoc_fields(Es, St), fun gexpr_list/3); gexpr({map,_Anno,Src,Es}, Vt, St) -> {Svt,St1} = gexpr(Src, Vt, St), {Fvt,St2} = map_fields(Es, Vt, St1, fun gexpr_list/3), {vtmerge(Svt, Fvt),St2}; gexpr({record_index,Anno,Name,Field}, _Vt, St) -> check_record(Anno, Name, St, fun (Dfs, St1) -> record_field(Field, Name, Dfs, St1) end ); gexpr({record_field,Anno,Rec,Name,Field}, Vt, St0) -> {Rvt,St1} = gexpr(Rec, Vt, St0), {Fvt,St2} = check_record(Anno, Name, St1, fun (Dfs, St) -> record_field(Field, Name, Dfs, St) end), {vtmerge(Rvt, Fvt),St2}; gexpr({record,Anno,Name,Inits}, Vt, St) -> check_record(Anno, Name, St, fun (Dfs, St1) -> ginit_fields(Inits, Anno, Name, Dfs, Vt, St1) end); gexpr({bin,_Anno,Fs}, Vt,St) -> expr_bin(Fs, Vt, St, fun gexpr/3); gexpr({call,_Anno,{atom,_Ar,is_record},[E,{atom,An,Name}]}, Vt, St0) -> {Rvt,St1} = gexpr(E, Vt, St0), {Rvt,exist_record(An, Name, St1)}; gexpr({call,Anno,{atom,_Ar,is_record},[E,R]}, Vt, St0) -> {Asvt,St1} = gexpr_list([E,R], Vt, St0), {Asvt,add_error(Anno, illegal_guard_expr, St1)}; gexpr({call,Anno,{remote,_Ar,{atom,_Am,erlang},{atom,Af,is_record}},[E,A]}, Vt, St0) -> gexpr({call,Anno,{atom,Af,is_record},[E,A]}, Vt, St0); gexpr({call,Anno,{atom,_Ar,is_record},[E0,{atom,_,_Name},{integer,_,_}]}, Vt, St0) -> {E,St1} = gexpr(E0, Vt, St0), case no_guard_bif_clash(St0, {is_record,3}) of true -> {E,St1}; false -> {E,add_error(Anno, {illegal_guard_local_call,{is_record,3}}, St1)} end; gexpr({call,Anno,{atom,_Ar,is_record},[_,_,_]=Asvt0}, Vt, St0) -> {Asvt,St1} = gexpr_list(Asvt0, Vt, St0), {Asvt,add_error(Anno, illegal_guard_expr, St1)}; gexpr({call,Anno,{remote,_,{atom,_,erlang},{atom,_,is_record}=Isr},[_,_,_]=Args}, Vt, St0) -> gexpr({call,Anno,Isr,Args}, Vt, St0); gexpr({call,Anno,{atom,_Aa,F},As}, Vt, St0) -> {Asvt,St1} = gexpr_list(As, Vt, St0), A = length(As), %% BifClash - Function called in guard case erl_internal:guard_bif(F, A) andalso no_guard_bif_clash(St1,{F,A}) of true -> %% Assert that it is auto-imported. true = erl_internal:bif(F, A), {Asvt,St1}; false -> case is_local_function(St1#lint.locals,{F,A}) orelse is_imported_function(St1#lint.imports,{F,A}) of true -> {Asvt,add_error(Anno, {illegal_guard_local_call,{F,A}}, St1)}; _ -> {Asvt,add_error(Anno, illegal_guard_expr, St1)} end end; gexpr({call,Anno,{remote,_Ar,{atom,_Am,erlang},{atom,_Af,F}},As}, Vt, St0) -> {Asvt,St1} = gexpr_list(As, Vt, St0), A = length(As), case erl_internal:guard_bif(F, A) orelse is_gexpr_op(F, A) of true -> {Asvt,St1}; false -> {Asvt,add_error(Anno, illegal_guard_expr, St1)} end; gexpr({op,Anno,Op,A}, Vt, St0) -> {Avt,St1} = gexpr(A, Vt, St0), case is_gexpr_op(Op, 1) of true -> {Avt,St1}; false -> {Avt,add_error(Anno, illegal_guard_expr, St1)} end; gexpr({op,_,'andalso',L,R}, Vt, St) -> gexpr_list([L,R], Vt, St); gexpr({op,_,'orelse',L,R}, Vt, St) -> gexpr_list([L,R], Vt, St); gexpr({op,Anno,Op,L,R}, Vt, St0) -> {Avt,St1} = gexpr_list([L,R], Vt, St0), case is_gexpr_op(Op, 2) of true -> {Avt,St1}; false -> {Avt,add_error(Anno, illegal_guard_expr, St1)} end; %% Everything else is illegal! You could put explicit tests here to %% better error diagnostics. gexpr(E, _Vt, St) -> {[],add_error(element(2, E), illegal_guard_expr, St)}. %% gexpr_list(Expressions, VarTable, State) -> %% {UsedVarTable,State'} gexpr_list(Es, Vt, St) -> foldl(fun (E, {Esvt,St0}) -> {Evt,St1} = gexpr(E, Vt, St0), {vtmerge(Evt, Esvt),St1} end, {[],St}, Es). %% is_guard_test(Expression) -> boolean(). %% Test if a general expression is a guard test. %% %% Note: Only use this function in contexts where there can be %% no definition of a local function that may override a guard BIF %% (for example, in the shell). -spec is_guard_test(Expr) -> boolean() when Expr :: erl_parse:abstract_expr(). is_guard_test(E) -> is_guard_test2(E, {maps:new(),fun(_) -> false end}). %% is_guard_test(Expression, Forms) -> boolean(). is_guard_test(Expression, Forms) -> is_guard_test(Expression, Forms, fun(_) -> false end). %% is_guard_test(Expression, Forms, IsOverridden) -> boolean(). %% Test if a general expression is a guard test. %% %% IsOverridden({Name,Arity}) should return 'true' if Name/Arity is %% a local or imported function in the module. If the abstract code has %% passed through erl_expand_records, any call without an explicit %% module is to a local function, so IsOverridden can be defined as: %% %% fun(_) -> true end %% -spec is_guard_test(Expr, Forms, IsOverridden) -> boolean() when Expr :: erl_parse:abstract_expr(), Forms :: [erl_parse:abstract_form() | erl_parse:form_info()], IsOverridden :: fun((fa()) -> boolean()). is_guard_test(Expression, Forms, IsOverridden) -> NoFileExpression = set_file(Expression, "nofile"), %% Unless the expression constructs a record, the record %% definitions are not needed. Therefore, because there can be a %% huge number of record definitions in the forms, delay %% processing the forms until we'll know that the record %% definitions are truly needed. F = fun() -> St = foldl(fun({attribute, _, record, _}=Attr0, St0) -> Attr = set_file(Attr0, "none"), attribute_state(Attr, St0); (_, St0) -> St0 end, start(), Forms), St#lint.records end, is_guard_test2(NoFileExpression, {F,IsOverridden}). %% is_guard_test2(Expression, RecordDefs :: dict:dict()) -> boolean(). is_guard_test2({call,Anno,{atom,Ar,record},[E,A]}, Info) -> is_gexpr({call,Anno,{atom,Ar,is_record},[E,A]}, Info); is_guard_test2({call,_Anno,{atom,_Aa,Test},As}=Call, {_,IsOverridden}=Info) -> A = length(As), not IsOverridden({Test,A}) andalso case erl_internal:type_test(Test, A) of true -> is_gexpr_list(As, Info); false -> is_gexpr(Call, Info) end; is_guard_test2(G, Info) -> %%Everything else is a guard expression. is_gexpr(G, Info). %% is_guard_expr(Expression) -> boolean(). %% Test if an expression is a guard expression. is_guard_expr(E) -> is_gexpr(E, {[],fun({_,_}) -> false end}). is_gexpr({var,_A,_V}, _Info) -> true; is_gexpr({char,_A,_C}, _Info) -> true; is_gexpr({integer,_A,_I}, _Info) -> true; is_gexpr({float,_A,_F}, _Info) -> true; is_gexpr({atom,_A,_Atom}, _Info) -> true; is_gexpr({string,_A,_S}, _Info) -> true; is_gexpr({nil,_A}, _Info) -> true; is_gexpr({cons,_A,H,T}, Info) -> is_gexpr_list([H,T], Info); is_gexpr({tuple,_A,Es}, Info) -> is_gexpr_list(Es, Info); is_gexpr({map,_A,Es}, Info) -> is_map_fields(Es, Info); is_gexpr({map,_A,Src,Es}, Info) -> is_gexpr(Src, Info) andalso is_map_fields(Es, Info); is_gexpr({record_index,_A,_Name,Field}, Info) -> is_gexpr(Field, Info); is_gexpr({record_field,_A,Rec,_Name,Field}, Info) -> is_gexpr_list([Rec,Field], Info); is_gexpr({record,A,Name,Inits}, Info0) -> Info = case Info0 of {#{},_} -> Info0; {F,IsOverridden} when is_function(F, 0) -> {F(),IsOverridden} end, is_gexpr_fields(Inits, A, Name, Info); is_gexpr({bin,_A,Fs}, Info) -> all(fun ({bin_element,_Anno,E,Sz,_Ts}) -> is_gexpr(E, Info) and (Sz =:= default orelse is_gexpr(Sz, Info)) end, Fs); is_gexpr({call,_A,{atom,_Af,F},As}, {_,IsOverridden}=Info) -> A = length(As), not IsOverridden({F,A}) andalso erl_internal:guard_bif(F, A) andalso is_gexpr_list(As, Info); is_gexpr({call,_A,{remote,_Ar,{atom,_Am,erlang},{atom,_Af,F}},As}, Info) -> A = length(As), (erl_internal:guard_bif(F, A) orelse is_gexpr_op(F, A)) andalso is_gexpr_list(As, Info); is_gexpr({call,A,{tuple,At,[{atom,Am,erlang},{atom,Af,F}]},As}, Info) -> is_gexpr({call,A,{remote,At,{atom,Am,erlang},{atom,Af,F}},As}, Info); is_gexpr({op,_A,Op,A}, Info) -> is_gexpr_op(Op, 1) andalso is_gexpr(A, Info); is_gexpr({op,_A,'andalso',A1,A2}, Info) -> is_gexpr_list([A1,A2], Info); is_gexpr({op,_A,'orelse',A1,A2}, Info) -> is_gexpr_list([A1,A2], Info); is_gexpr({op,_A,Op,A1,A2}, Info) -> is_gexpr_op(Op, 2) andalso is_gexpr_list([A1,A2], Info); is_gexpr(_Other, _Info) -> false. is_gexpr_op(Op, A) -> try erl_internal:op_type(Op, A) of arith -> true; bool -> true; comp -> true; list -> false; send -> false catch _:_ -> false end. is_gexpr_list(Es, Info) -> all(fun (E) -> is_gexpr(E, Info) end, Es). is_map_fields([{Tag,_,K,V}|Fs], Info) when Tag =:= map_field_assoc; Tag =:= map_field_exact -> is_gexpr(K, Info) andalso is_gexpr(V, Info) andalso is_map_fields(Fs, Info); is_map_fields([], _Info) -> true; is_map_fields(_T, _Info) -> false. is_gexpr_fields(Fs, A, Name, {RDs,_}=Info) -> IFs = case maps:find(Name, RDs) of {ok,{_Anno,Fields}} -> Fs ++ init_fields(Fs, A, Fields); error -> Fs end, all(fun ({record_field,_Af,_Name,V}) -> is_gexpr(V, Info); (_Other) -> false end, IFs). %% exprs(Sequence, VarTable, State) -> %% {UsedVarTable,State'} %% Check a sequence of expressions, return all variables. exprs([E|Es], Vt, St0) -> {Evt,St1} = expr(E, Vt, St0), {Esvt,St2} = exprs(Es, vtupdate(Evt, Vt), St1), {vtupdate(Evt, Esvt),St2}; exprs([], _Vt, St) -> {[],St}. %% expr(Expression, VarTable, State) -> %% {UsedVarTable,State'} %% Check an expression, returns NewVariables. Assume naive users and %% mark illegally exported variables, e.g. from catch, as unsafe to better %% show why unbound. expr({var,Anno,V}, Vt, St) -> expr_var(V, Anno, Vt, St); expr({char,_Anno,_C}, _Vt, St) -> {[],St}; expr({integer,_Anno,_I}, _Vt, St) -> {[],St}; expr({float,_Anno,_F}, _Vt, St) -> {[],St}; expr({atom,Anno,A}, _Vt, St) -> {[],keyword_warning(Anno, A, St)}; expr({string,_Anno,_S}, _Vt, St) -> {[],St}; expr({nil,_Anno}, _Vt, St) -> {[],St}; expr({cons,_Anno,H,T}, Vt, St) -> expr_list([H,T], Vt, St); expr({lc,_Anno,E,Qs}, Vt, St) -> handle_comprehension(E, Qs, Vt, St); expr({bc,_Anno,E,Qs}, Vt, St) -> handle_comprehension(E, Qs, Vt, St); expr({mc,_Anno,E,Qs}, Vt, St) -> handle_comprehension(E, Qs, Vt, St); expr({tuple,_Anno,Es}, Vt, St) -> expr_list(Es, Vt, St); expr({map,_Anno,Es}, Vt, St) -> map_fields(Es, Vt, check_assoc_fields(Es, St), fun expr_list/3); expr({map,_Anno,Src,Es}, Vt, St) -> {Svt,St1} = expr(Src, Vt, St), {Fvt,St2} = map_fields(Es, Vt, St1, fun expr_list/3), {vtupdate(Svt, Fvt),St2}; expr({record_index,Anno,Name,Field}, _Vt, St) -> check_record(Anno, Name, St, fun (Dfs, St1) -> record_field(Field, Name, Dfs, St1) end); expr({record,Anno,Name,Inits}, Vt, St) -> check_record(Anno, Name, St, fun (Dfs, St1) -> init_fields(Inits, Anno, Name, Dfs, Vt, St1) end); expr({record_field,Anno,Rec,Name,Field}, Vt, St0) -> {Rvt,St1} = record_expr(Anno, Rec, Vt, St0), {Fvt,St2} = check_record(Anno, Name, St1, fun (Dfs, St) -> record_field(Field, Name, Dfs, St) end), {vtmerge(Rvt, Fvt),St2}; expr({record,Anno,Rec,Name,Upds}, Vt, St0) -> {Rvt,St1} = record_expr(Anno, Rec, Vt, St0), {Usvt,St2} = check_record(Anno, Name, St1, fun (Dfs, St) -> update_fields(Upds, Name, Dfs, Vt, St) end ), case has_wildcard_field(Upds) of no -> {vtmerge(Rvt, Usvt),St2}; WildAnno -> {[],add_error(WildAnno, {wildcard_in_update,Name}, St2)} end; expr({bin,_Anno,Fs}, Vt, St) -> expr_bin(Fs, Vt, St, fun expr/3); expr({block,_Anno,Es}, Vt, St) -> %% Unfold block into a sequence. exprs(Es, Vt, St); expr({'if',Anno,Cs}, Vt, St) -> icrt_clauses(Cs, {'if',Anno}, Vt, St); expr({'case',Anno,E,Cs}, Vt, St0) -> {Evt,St1} = expr(E, Vt, St0), {Cvt,St2} = icrt_clauses(Cs, {'case',Anno}, vtupdate(Evt, Vt), St1), {vtmerge(Evt, Cvt),St2}; expr({'receive',Anno,Cs}, Vt, St) -> icrt_clauses(Cs, {'receive',Anno}, Vt, St); expr({'receive',Anno,Cs,To,ToEs}, Vt, St0) -> %% Are variables from the timeout expression visible in the clauses? NO! {Tvt,St1} = expr(To, Vt, St0), {Tevt,St2} = exprs(ToEs, Vt, St1), {Cvt,St3} = icrt_clauses(Cs, Vt, St2), %% Csvts = [vtnew(Tevt, Vt)|Cvt], %This is just NEW variables! Csvts = [Tevt|Cvt], Rvt = icrt_export(Csvts, Vt, {'receive',Anno}, St3), {vtmerge([Tvt,Tevt,Rvt]),St3}; expr({'fun',Anno,Body}, Vt, St) -> %%No one can think funs export! case Body of {clauses,Cs} -> fun_clauses(Cs, Vt, St); {function,record_info,2} -> %% It is illegal to call record_info/2 with unknown arguments. {[],add_error(Anno, illegal_record_info, St)}; {function,F,A} -> %% BifClash - Fun expression %% N.B. Only allows BIFs here as well, NO IMPORTS!! case ((not is_local_function(St#lint.locals,{F,A})) andalso (erl_internal:bif(F, A) andalso (not is_autoimport_suppressed(St#lint.no_auto,{F,A})))) of true -> {[],St}; false -> {[],call_function(Anno, F, A, St)} end; {function,M,F,A} -> expr_list([M,F,A], Vt, St) end; expr({named_fun,_,'_',Cs}, Vt, St) -> fun_clauses(Cs, Vt, St); expr({named_fun,Anno,Name,Cs}, Vt, St0) -> Nvt0 = [{Name,{bound,unused,[Anno]}}], St1 = shadow_vars(Nvt0, Vt, 'named fun', St0), Nvt1 = vtupdate(vtsubtract(Vt, Nvt0), Nvt0), {Csvt,St2} = fun_clauses(Cs, Nvt1, St1), {_,St3} = check_unused_vars(vtupdate(Csvt, Nvt0), [], St2), {vtold(Csvt, Vt),St3}; expr({call,_Anno,{atom,_Ar,is_record},[E,{atom,An,Name}]}, Vt, St0) -> {Rvt,St1} = expr(E, Vt, St0), {Rvt,exist_record(An, Name, St1)}; expr({call,Anno,{remote,_Ar,{atom,_Am,erlang},{atom,Af,is_record}},[E,A]}, Vt, St0) -> expr({call,Anno,{atom,Af,is_record},[E,A]}, Vt, St0); expr({call,A,{tuple,At,[{atom,Am,erlang},{atom,Af,is_record}]},As}, Vt, St) -> expr({call,A,{remote,At,{atom,Am,erlang},{atom,Af,is_record}},As}, Vt, St); expr({call,Anno,{remote,_Ar,{atom,_Am,M},{atom,Af,F}},As}, Vt, St0) -> St1 = keyword_warning(Af, F, St0), St2 = check_remote_function(Anno, M, F, As, St1), St3 = check_module_name(M, Anno, St2), expr_list(As, Vt, St3); expr({call,Anno,{remote,_Ar,M,F},As}, Vt, St0) -> St1 = keyword_warning(Anno, M, St0), St2 = keyword_warning(Anno, F, St1), St3 = case M of {atom,Am,Mod} -> check_module_name(Mod, Am, St2); _ -> St2 end, expr_list([M,F|As], Vt, St3); expr({call,Anno,{atom,Aa,F},As}, Vt, St0) -> St1 = keyword_warning(Aa, F, St0), {Asvt,St2} = expr_list(As, Vt, St1), A = length(As), IsLocal = is_local_function(St2#lint.locals,{F,A}), IsAutoBif = erl_internal:bif(F, A), AutoSuppressed = is_autoimport_suppressed(St2#lint.no_auto,{F,A}), Warn = is_warn_enabled(bif_clash, St2) and (not bif_clash_specifically_disabled(St2,{F,A})), Imported = imported(F, A, St2), case ((not IsLocal) andalso (Imported =:= no) andalso IsAutoBif andalso (not AutoSuppressed)) of true -> St3 = deprecated_function(Anno, erlang, F, As, St2), {Asvt,St3}; false -> {Asvt,case Imported of {yes,M} -> St3 = check_remote_function(Anno, M, F, As, St2), U0 = St3#lint.usage, Imp = ordsets:add_element({{F,A},M},U0#usage.imported), St3#lint{usage=U0#usage{imported = Imp}}; no -> case {F,A} of {record_info,2} -> check_record_info_call(Anno,Aa,As,St2); N -> %% BifClash - function call %% Issue these warnings/errors even if it's a recursive call St3 = if (not AutoSuppressed) andalso IsAutoBif andalso Warn -> case erl_internal:old_bif(F,A) of true -> add_error (Anno, {call_to_redefined_old_bif, {F,A}}, St2); false -> add_warning (Anno, {call_to_redefined_bif, {F,A}}, St2) end; true -> St2 end, %% ...but don't lint recursive calls if N =:= St3#lint.func -> St3; true -> call_function(Anno, F, A, St3) end end end} end; expr({call,Anno,F,As}, Vt, St0) -> St = warn_invalid_call(Anno,F,St0), expr_list([F|As], Vt, St); %They see the same variables expr({'try',Anno,Es,Scs,Ccs,As}, Vt, St0) -> %% The only exports we allow are from the try expressions to the %% success clauses. {Evt0,St1} = exprs(Es, Vt, St0), TryAnno = {'try',Anno}, Uvt = vtunsafe(TryAnno, Evt0, Vt), {Sccs,St2} = try_clauses(Scs, Ccs, TryAnno, vtupdate(Evt0, Vt), Uvt, St1), Evt1 = vtupdate(Uvt, Evt0), Rvt0 = Sccs, Rvt1 = vtupdate(vtunsafe(TryAnno, Rvt0, Vt), Rvt0), Evt2 = vtmerge(Evt1, Rvt1), {Avt0,St} = exprs(As, vtupdate(Evt2, Vt), St2), Avt1 = vtupdate(vtunsafe(TryAnno, Avt0, Vt), Avt0), Avt = vtmerge(Evt2, Avt1), {Avt,St}; expr({'catch',Anno,E}, Vt, St0) -> %% No new variables added, flag new variables as unsafe. {Evt,St} = expr(E, Vt, St0), {vtupdate(vtunsafe({'catch',Anno}, Evt, Vt), Evt),St}; expr({match,_Anno,P,E}, Vt, St0) -> {Evt,St1} = expr(E, Vt, St0), {Pvt,Pnew,St} = pattern(P, vtupdate(Evt, Vt), St1), {vtupdate(Pnew, vtmerge(Evt, Pvt)),St}; expr({maybe_match,Anno,P,E}, Vt, St0) -> expr({match,Anno,P,E}, Vt, St0); expr({'maybe',Anno,Es}, Vt, St) -> %% No variables are exported. {Evt0, St1} = exprs(Es, Vt, St), Evt1 = vtupdate(vtunsafe({'maybe',Anno}, Evt0, Vt), Vt), Evt2 = vtmerge(Evt0, Evt1), {Evt2,St1}; expr({'maybe',MaybeAnno,Es,{'else',ElseAnno,Cs}}, Vt, St) -> %% No variables are exported. {Evt0, St1} = exprs(Es, Vt, St), Evt1 = vtupdate(vtunsafe({'maybe',MaybeAnno}, Evt0, Vt), Vt), {Cvt0, St2} = icrt_clauses(Cs, {'else',ElseAnno}, Evt1, St1), Cvt1 = vtupdate(vtunsafe({'else',ElseAnno}, Cvt0, Vt), Vt), Evt2 = vtmerge(Evt0, Evt1), Cvt2 = vtmerge(Cvt0, Cvt1), {vtmerge(Evt2, Cvt2),St2}; %% No comparison or boolean operators yet. expr({op,_Anno,_Op,A}, Vt, St) -> expr(A, Vt, St); expr({op,Anno,Op,L,R}, Vt, St0) when Op =:= 'orelse'; Op =:= 'andalso' -> {Evt1,St1} = expr(L, Vt, St0), Vt1 = vtupdate(Evt1, Vt), {Evt2,St2} = expr(R, Vt1, St1), Evt3 = vtupdate(vtunsafe({Op,Anno}, Evt2, Vt1), Evt2), {vtmerge(Evt1, Evt3),St2}; expr({op,_Anno,_Op,L,R}, Vt, St) -> expr_list([L,R], Vt, St); %They see the same variables %% The following are not allowed to occur anywhere! expr({remote,_Anno,M,_F}, _Vt, St) -> {[],add_error(erl_parse:first_anno(M), illegal_expr, St)}; expr({ssa_check_when,_Anno,_WantedResult,_Args,_Tag,_Exprs}, _Vt, St) -> {[], St}. %% expr_list(Expressions, Variables, State) -> %% {UsedVarTable,State} expr_list(Es, Vt, St0) -> foldl(fun (E, {Esvt, St1}) -> {Evt, St2} = expr(E, Vt, St1), vtmerge_pat(Evt, Esvt, St2) end, {[], St0}, Es). record_expr(Anno, Rec, Vt, St0) -> St1 = warn_invalid_record(Anno, Rec, St0), expr(Rec, Vt, St1). check_assoc_fields([{map_field_exact,Anno,_,_}|Fs], St) -> check_assoc_fields(Fs, add_error(Anno, illegal_map_construction, St)); check_assoc_fields([{map_field_assoc,_,_,_}|Fs], St) -> check_assoc_fields(Fs, St); check_assoc_fields([], St) -> St. map_fields([{Tag,_,K,V}|Fs], Vt, St, F) when Tag =:= map_field_assoc; Tag =:= map_field_exact -> {Pvt,St2} = F([K,V], Vt, St), {Vts,St3} = map_fields(Fs, Vt, St2, F), {vtupdate(Pvt, Vts),St3}; map_fields([], _, St, _) -> {[],St}. %% warn_invalid_record(Anno, Record, State0) -> State %% Adds warning if the record is invalid. warn_invalid_record(Anno, R, St) -> case is_valid_record(R) of true -> St; false -> add_warning(Anno, invalid_record, St) end. %% is_valid_record(Record) -> boolean(). is_valid_record(Rec) -> case Rec of {char, _, _} -> false; {integer, _, _} -> false; {float, _, _} -> false; {atom, _, _} -> false; {string, _, _} -> false; {cons, _, _, _} -> false; {nil, _} -> false; {lc, _, _, _} -> false; {record_index, _, _, _} -> false; {'fun', _, _} -> false; {named_fun, _, _, _} -> false; _ -> true end. %% warn_invalid_call(Anno, Call, State0) -> State %% Adds warning if the call is invalid. warn_invalid_call(Anno, F, St) -> case is_valid_call(F) of true -> St; false -> add_warning(Anno, invalid_call, St) end. %% is_valid_call(Call) -> boolean(). is_valid_call(Call) -> case Call of {char, _, _} -> false; {integer, _, _} -> false; {float, _, _} -> false; {string, _, _} -> false; {cons, _, _, _} -> false; {nil, _} -> false; {lc, _, _, _} -> false; {record_index, _, _, _} -> false; {tuple, _, Exprs} when length(Exprs) =/= 2 -> false; _ -> true end. %% record_def(Anno, RecordName, [RecField], State) -> State. %% Add a record definition if it does not already exist. Normalise %% so that all fields have explicit initial value. record_def(Anno, Name, Fs0, St0) -> case is_map_key(Name, St0#lint.records) of true -> add_error(Anno, {redefine_record,Name}, St0); false -> {Fs1,St1} = def_fields(normalise_fields(Fs0), Name, St0), St2 = St1#lint{records=maps:put(Name, {Anno,Fs1}, St1#lint.records)}, Types = [T || {typed_record_field, _, T} <- Fs0], St3 = St2#lint{type_id = {record, Name}}, check_type({type, nowarn(), product, Types}, St3) end. %% def_fields([RecDef], RecordName, State) -> {[DefField],State}. %% Check (normalised) fields for duplicates. Return unduplicated %% record and set State. def_fields(Fs0, Name, St0) -> foldl(fun ({record_field,Af,{atom,Aa,F},V}, {Fs,St}) -> case exist_field(F, Fs) of true -> {Fs,add_error(Af, {redefine_field,Name,F}, St)}; false -> St1 = St#lint{recdef_top = true}, {_,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, 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 true -> V; false -> {atom,Aa,undefined} end, {[{record_field,Af,{atom,Aa,F},NV}|Fs],St3} end end, {[],St0}, Fs0). %% normalise_fields([RecDef]) -> [Field]. %% Normalise the field definitions to always have a default value. If %% none has been given then use 'undefined'. %% Also, strip type information from typed record fields. normalise_fields(Fs) -> map(fun ({record_field,Af,Field}) -> {record_field,Af,Field,{atom,Af,undefined}}; ({typed_record_field,{record_field,Af,Field},_Type}) -> {record_field,Af,Field,{atom,Af,undefined}}; ({typed_record_field,Field,_Type}) -> Field; (F) -> F end, Fs). %% exist_record(Anno, RecordName, State) -> State. %% Check if a record exists. Set State. exist_record(Anno, Name, St) -> case is_map_key(Name, St#lint.records) of true -> used_record(Name, St); false -> add_error(Anno, {undefined_record,Name}, St) end. %% check_record(Anno, RecordName, State, CheckFun) -> %% {UpdVarTable, State}. %% The generic record checking function, first checks that the record %% exists then calls the specific check function. N.B. the check %% function can safely assume that the record exists. %% %% The check function is called: %% CheckFun(RecordDefFields, State) %% and must return %% {UpdatedVarTable,State} check_record(Anno, Name, St, CheckFun) -> case maps:find(Name, St#lint.records) of {ok,{_Anno,Fields}} -> CheckFun(Fields, used_record(Name, St)); error -> {[],add_error(Anno, {undefined_record,Name}, St)} end. used_record(Name, #lint{usage=Usage}=St) -> UsedRecs = gb_sets:add_element(Name, Usage#usage.used_records), St#lint{usage = Usage#usage{used_records=UsedRecs}}. %%% Record check functions. %% check_fields([ChkField], RecordName, [RecDefField], VarTable, State, CheckFun) -> %% {UpdVarTable,State}. check_fields(Fs, Name, Fields, Vt0, St0, CheckFun) -> {_SeenFields,Uvt,St1} = foldl(fun (Field, {Sfsa,Vta,Sta}) -> {Sfsb,{Vtb,Stb}} = check_field(Field, Name, Fields, Vt0, Sta, Sfsa, CheckFun), {Vt1, St1} = vtmerge_pat(Vta, Vtb, Stb), {Sfsb, Vt1, St1} end, {[],[], St0}, Fs), {Uvt,St1}. check_field({record_field,Af,{atom,Aa,F},Val}, Name, Fields, Vt, St, Sfs, CheckFun) -> case member(F, Sfs) of true -> {Sfs,{[],add_error(Af, {redefine_field,Name,F}, St)}}; false -> {[F|Sfs], case find_field(F, Fields) of {ok,_I} -> CheckFun(Val, Vt, St); error -> {[],add_error(Aa, {undefined_field,Name,F}, St)} end} end; check_field({record_field,_Af,{var,Aa,'_'=F},Val}, _Name, _Fields, Vt, St, Sfs, CheckFun) -> case member(F, Sfs) of true -> {Sfs,{[],add_error(Aa, bad_multi_field_init, St)}}; false -> {[F|Sfs],CheckFun(Val, Vt, St)} end; check_field({record_field,_Af,{var,Aa,V},_Val}, Name, _Fields, Vt, St, Sfs, _CheckFun) -> {Sfs,{Vt,add_error(Aa, {field_name_is_variable,Name,V}, St)}}. %% pattern_field(Field, RecordName, [RecDefField], State) -> %% {UpdVarTable,State}. %% Test if record RecordName has field Field. Set State. pattern_field({atom,Aa,F}, Name, Fields, St) -> case find_field(F, Fields) of {ok,_I} -> {[],St}; error -> {[],add_error(Aa, {undefined_field,Name,F}, St)} end. %% pattern_fields([PatField],RecordName,[RecDefField], %% VarTable,Old,State) -> %% {UpdVarTable,NewVars,State}. pattern_fields(Fs, Name, Fields, Vt0, Old, St0) -> CheckFun = fun (Val, Vt, St) -> pattern(Val, Vt, Old, St) end, {_SeenFields,Uvt,Unew,St1} = foldl(fun (Field, {Sfsa,Vta,Newa,Sta}) -> case check_field(Field, Name, Fields, Vt0, Sta, Sfsa, CheckFun) of {Sfsb,{Vtb,Stb}} -> {Vt, St1} = vtmerge_pat(Vta, Vtb, Stb), {Sfsb, Vt, [], St1}; {Sfsb,{Vtb,Newb,Stb}} -> {Vt, Mst0} = vtmerge_pat(Vta, Vtb, Stb), {New, Mst} = vtmerge_pat(Newa, Newb, Mst0), {Sfsb, Vt, New, Mst} end end, {[],[],[],St0}, Fs), {Uvt,Unew,St1}. %% record_field(Field, RecordName, [RecDefField], State) -> %% {UpdVarTable,State}. %% Test if record RecordName has field Field. Set State. record_field({atom,Aa,F}, Name, Fields, St) -> case find_field(F, Fields) of {ok,_I} -> {[],St}; error -> {[],add_error(Aa, {undefined_field,Name,F}, St)} end. %% init_fields([InitField], InitAnno, RecordName, [DefField], VarTable, State) -> %% {UpdVarTable,State}. %% ginit_fields([InitField], InitAnno, RecordName, [DefField], VarTable, State) -> %% {UpdVarTable,State}. %% Check record initialisation. Explicit initialisations are checked %% as is, while default values are checked only if there are no %% explicit inititialisations of the fields. Be careful not to %% duplicate warnings (and possibly errors, but def_fields %% substitutes 'undefined' for bogus inititialisations) from when the %% record definitions were checked. Usage of records, imports, and %% functions is collected. init_fields(Ifs, Anno, Name, Dfs, Vt0, St0) -> {Vt1,St1} = check_fields(Ifs, Name, Dfs, Vt0, St0, fun expr/3), Defs = init_fields(Ifs, Anno, Dfs), {_,St2} = check_fields(Defs, Name, Dfs, Vt1, St1, fun expr/3), {Vt1,St1#lint{usage = St2#lint.usage}}. 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}, {Vt1,St4}. %% Default initializations to be carried out init_fields(Ifs, Anno, Dfs) -> [ {record_field,Af,{atom,Aa,F},copy_expr(Di, Anno)} || {record_field,Af,{atom,Aa,F},Di} <- Dfs, not exist_field(F, Ifs) ]. %% update_fields(UpdFields, RecordName, RecDefFields, VarTable, State) -> %% {UpdVarTable,State} update_fields(Ufs, Name, Dfs, Vt, St) -> check_fields(Ufs, Name, Dfs, Vt, St, fun expr/3). %% exist_field(FieldName, [Field]) -> boolean(). %% Find a record field in a field list. exist_field(F, [{record_field,_Af,{atom,_Aa,F},_Val}|_Fs]) -> true; exist_field(F, [_|Fs]) -> exist_field(F, Fs); exist_field(_F, []) -> false. %% find_field(FieldName, [Field]) -> {ok,Val} | error. %% Find a record field in a field list. find_field(F, [{record_field,_Af,{atom,_Aa,F},Val}|_Fs]) -> {ok,Val}; find_field(F, [_|Fs]) -> find_field(F, Fs); find_field(_F, []) -> error. %% type_def(Attr, Anno, TypeName, PatField, Args, State) -> State. %% Attr :: 'type' | 'opaque' %% Checks that a type definition is valid. -dialyzer({no_match, type_def/6}). type_def(Attr, Anno, TypeName, ProtoType, Args, St0) -> TypeDefs = St0#lint.types, Arity = length(Args), TypePair = {TypeName, Arity}, Info = #typeinfo{attr = Attr, anno = Anno}, StoreType = fun(St) -> NewDefs = maps:put(TypePair, Info, TypeDefs), CheckType = {type, nowarn(), product, [ProtoType|Args]}, St1 = St#lint{types=NewDefs, type_id={type, TypePair}}, check_type(CheckType, St1) end, case is_default_type(TypePair) andalso not member(no_auto_import_types, St0#lint.compile) of true -> case is_obsolete_builtin_type(TypePair) of true -> StoreType(St0); false -> %% Starting from OTP 26, redefining built-in types %% is allowed. St1 = StoreType(St0), warn_redefined_builtin_type(Anno, TypePair, St1) end; false -> case is_map_key(TypePair, TypeDefs) of true -> add_error(Anno, {redefine_type, TypePair}, St0); false -> StoreType(St0) end end. warn_redefined_builtin_type(Anno, TypePair, #lint{compile=Opts}=St) -> case is_warn_enabled(redefined_builtin_type, St) of true -> NoWarn = [Type || {nowarn_redefined_builtin_type, Type0} <- Opts, Type <- lists:flatten([Type0])], case lists:member(TypePair, NoWarn) of true -> St; false -> Warn = {redefine_builtin_type, TypePair}, add_warning(Anno, Warn, St) end; false -> St end. check_type(Types, St) -> {SeenVars, St1} = check_type_1(Types, maps:new(), St), maps:fold(fun(Var, {seen_once, Anno}, AccSt) -> case atom_to_list(Var) of "_"++_ -> AccSt; _ -> add_error(Anno, {singleton_typevar, Var}, AccSt) end; (Var, {seen_once_union, Anno}, AccSt) -> case is_warn_enabled(singleton_typevar, AccSt) of true -> case atom_to_list(Var) of "_"++_ -> AccSt; _ -> add_warning(Anno, {singleton_typevar, Var}, AccSt) end; false -> AccSt end; (_Var, seen_multiple, AccSt) -> AccSt end, St1, SeenVars). check_type_1({type, Anno, TypeName, Args}=Type, SeenVars, #lint{types=Types}=St) -> TypePair = {TypeName, if is_list(Args) -> length(Args); true -> 0 end}, case is_map_key(TypePair, Types) of true -> check_type_2(Type, SeenVars, used_type(TypePair, Anno, St)); false -> check_type_2(Type, SeenVars, St) end; check_type_1(Types, SeenVars, St) -> check_type_2(Types, SeenVars, St). check_type_2({ann_type, _A, [_Var, Type]}, SeenVars, St) -> check_type_1(Type, SeenVars, St); check_type_2({remote_type, A, [{atom, _, Mod}, {atom, _, Name}, Args]}, SeenVars, St00) -> St0 = check_module_name(Mod, A, St00), St = deprecated_type(A, Mod, Name, Args, St0), CurrentMod = St#lint.module, case Mod =:= CurrentMod of true -> check_type_2({user_type, A, Name, Args}, SeenVars, St); false -> lists:foldl(fun(T, {AccSeenVars, AccSt}) -> check_type_1(T, AccSeenVars, AccSt) end, {SeenVars, St}, Args) end; check_type_2({integer, _A, _}, SeenVars, St) -> {SeenVars, St}; check_type_2({atom, _A, _}, SeenVars, St) -> {SeenVars, St}; check_type_2({var, _A, '_'}, SeenVars, St) -> {SeenVars, St}; check_type_2({var, A, Name}, SeenVars, St) -> NewSeenVars = case maps:find(Name, SeenVars) of {ok, {seen_once, _}} -> maps:put(Name, seen_multiple, SeenVars); {ok, {seen_once_union, _}} -> maps:put(Name, seen_multiple, SeenVars); {ok, seen_multiple} -> SeenVars; error -> maps:put(Name, {seen_once, A}, SeenVars) end, {NewSeenVars, St}; check_type_2({type, A, bool, []}, SeenVars, St) -> {SeenVars, add_warning(A, {renamed_type, bool, boolean}, St)}; check_type_2({type, A, 'fun', [Dom, Range]}, SeenVars, St) -> St1 = case Dom of {type, _, product, _} -> St; {type, _, any} -> St; _ -> add_error(A, {type_syntax, 'fun'}, St) end, check_type_2({type, nowarn(), product, [Dom, Range]}, SeenVars, St1); check_type_2({type, A, range, [From, To]}, SeenVars, St) -> St1 = case {erl_eval:partial_eval(From), erl_eval:partial_eval(To)} of {{integer, _, X}, {integer, _, Y}} when X < Y -> St; _ -> add_error(A, {type_syntax, range}, St) end, {SeenVars, St1}; check_type_2({type, _A, map, any}, SeenVars, St) -> {SeenVars, St}; check_type_2({type, _A, map, Pairs}, SeenVars, St) -> lists:foldl(fun(Pair, {AccSeenVars, AccSt}) -> check_type_2(Pair, AccSeenVars, AccSt) end, {SeenVars, St}, Pairs); check_type_2({type, _A, map_field_assoc, [Dom, Range]}, SeenVars, St) -> check_type_2({type, nowarn(), product, [Dom, Range]}, SeenVars, St); check_type_2({type, _A, tuple, any}, SeenVars, St) -> {SeenVars, St}; check_type_2({type, _A, any}, SeenVars, St) -> {SeenVars, St}; check_type_2({type, A, binary, [Base, Unit]}, SeenVars, St) -> St1 = case {erl_eval:partial_eval(Base), erl_eval:partial_eval(Unit)} of {{integer, _, BaseVal}, {integer, _, UnitVal}} when BaseVal >= 0, UnitVal >= 0 -> St; _ -> add_error(A, {type_syntax, binary}, St) end, {SeenVars, St1}; check_type_2({type, A, record, [Name|Fields]}, SeenVars, St) -> case Name of {atom, _, Atom} -> St1 = used_record(Atom, St), check_record_types(A, Atom, Fields, SeenVars, St1); _ -> {SeenVars, add_error(A, {type_syntax, record}, St)} end; check_type_2({type, _A, Tag, Args}=_F, SeenVars, St) when Tag =:= product; Tag =:= tuple -> lists:foldl(fun(T, {AccSeenVars, AccSt}) -> check_type_1(T, AccSeenVars, AccSt) end, {SeenVars, St}, Args); check_type_2({type, _A, union, Args}=_F, SeenVars0, St) -> lists:foldl(fun(T, {AccSeenVars0, AccSt}) -> {SeenVars1, St0} = check_type_1(T, SeenVars0, AccSt), AccSeenVars = maps:merge_with( fun (K, {seen_once, Anno}, {seen_once, _}) -> case SeenVars0 of #{K := _} -> %% Unused outside of this union. {seen_once, Anno}; #{} -> {seen_once_union, Anno} end; (_K, {seen_once, Anno}, {seen_once_union, _}) -> {seen_once_union, Anno}; (_K, {seen_once_union, _}=R, {seen_once, _}) -> R; (_K, {seen_once_union, _}=R, {seen_once_union, _}) -> R; (_K, {seen_once_union, _}, Else) -> Else; (_K, {seen_once, _}, Else) -> Else; (_K, Else, {seen_once_union, _}) -> Else; (_K, Else, {seen_once, _}) -> Else; (_K, Else1, _Else2) -> Else1 end, AccSeenVars0, SeenVars1), {AccSeenVars, St0} end, {SeenVars0, St}, Args); check_type_2({type, Anno, TypeName, Args}, SeenVars, St) -> #lint{module = Module, types=Types} = St, Arity = length(Args), TypePair = {TypeName, Arity}, Obsolete = (is_warn_enabled(deprecated_type, St) andalso obsolete_builtin_type(TypePair)), St1 = case Obsolete of {deprecated, Repl, _} when element(1, Repl) =/= Module -> case maps:find(TypePair, Types) of {ok, _} -> used_type(TypePair, Anno, St); error -> {deprecated, Replacement, Rel} = Obsolete, Tag = deprecated_builtin_type, W = {Tag, TypePair, Replacement, Rel}, add_warning(Anno, W, St) end; _ -> case is_default_type(TypePair) of true -> used_type(TypePair, Anno, St); false -> St end end, check_type_2({type, nowarn(), product, Args}, SeenVars, St1); check_type_2({user_type, A, TypeName, Args}, SeenVars, St) -> Arity = length(Args), TypePair = {TypeName, Arity}, St1 = used_type(TypePair, A, St), lists:foldl(fun(T, {AccSeenVars, AccSt}) -> check_type_1(T, AccSeenVars, AccSt) end, {SeenVars, St1}, Args); check_type_2([{typed_record_field,Field,_T}|_], SeenVars, St) -> {SeenVars, add_error(element(2, Field), old_abstract_code, St)}; check_type_2(I, SeenVars, St) -> case erl_eval:partial_eval(I) of {integer,_A,_Integer} -> {SeenVars, St}; _Other -> {SeenVars, add_error(element(2, I), {type_syntax, integer}, St)} end. check_record_types(Anno, Name, Fields, SeenVars, St) -> case maps:find(Name, St#lint.records) of {ok,{_A,DefFields}} -> case lists:all(fun({type, _, field_type, _}) -> true; (_) -> false end, Fields) of true -> check_record_types(Fields, Name, DefFields, SeenVars, St, []); false -> {SeenVars, add_error(Anno, {type_syntax, record}, St)} end; error -> {SeenVars, add_error(Anno, {undefined_record, Name}, St)} end. check_record_types([{type, _, field_type, [{atom, Anno, FName}, Type]}|Left], Name, DefFields, SeenVars, St, SeenFields) -> %% Check that the field name is valid St1 = case exist_field(FName, DefFields) of true -> St; false -> add_error(Anno, {undefined_field, Name, FName}, St) end, %% Check for duplicates St2 = case ordsets:is_element(FName, SeenFields) of true -> add_error(Anno, {redefine_field, Name, FName}, St1); false -> St1 end, %% Check Type {NewSeenVars, St3} = check_type_2(Type, SeenVars, St2), NewSeenFields = ordsets:add_element(FName, SeenFields), check_record_types(Left, Name, DefFields, NewSeenVars, St3, NewSeenFields); check_record_types([], _Name, _DefFields, SeenVars, St, _SeenFields) -> {SeenVars, St}. used_type(TypePair, Anno, #lint{usage = Usage, file = File} = St) -> Used = Usage#usage.used_types, UsedType = #used_type{anno = erl_anno:set_file(File, Anno), at = St#lint.type_id}, NewUsed = maps_prepend(TypePair, UsedType, Used), St#lint{usage=Usage#usage{used_types=NewUsed}}. is_default_type({Name, NumberOfTypeVariables}) -> erl_internal:is_type(Name, NumberOfTypeVariables). is_obsolete_builtin_type(TypePair) -> obsolete_builtin_type(TypePair) =/= no. %% To keep Dialyzer silent... obsolete_builtin_type({1, 255}) -> {deprecated, {2, 255}, ""}; obsolete_builtin_type({Name, A}) when is_atom(Name), is_integer(A) -> no. %% spec_decl(Anno, Fun, Types, State) -> State. spec_decl(Anno, MFA0, TypeSpecs, St00 = #lint{specs = Specs, module = Mod}) -> MFA = case MFA0 of {F, Arity} -> {Mod, F, Arity}; {_M, _F, Arity} -> MFA0 end, St0 = check_module_name(element(1, MFA), Anno, St00), St1 = St0#lint{specs = maps:put(MFA, Anno, Specs)}, case is_map_key(MFA, Specs) of true -> add_error(Anno, {redefine_spec, MFA0}, St1); false -> St2 = case MFA of {Mod, _, _} -> St1; _ -> add_error(Anno, {bad_module, MFA}, St1) end, St3 = St2#lint{type_id = {spec, MFA}}, check_specs(TypeSpecs, spec_wrong_arity, Arity, St3) end. %% callback_decl(Anno, Fun, Types, State) -> State. callback_decl(Anno, MFA0, TypeSpecs, St0 = #lint{callbacks = Callbacks, module = Mod}) -> case MFA0 of {M, _F, _A} -> St1 = check_module_name(M, Anno, St0), add_error(Anno, {bad_callback, MFA0}, St1); {F, Arity} -> MFA = {Mod, F, Arity}, St1 = St0#lint{callbacks = maps:put(MFA, Anno, Callbacks)}, case is_map_key(MFA, Callbacks) of true -> add_error(Anno, {redefine_callback, MFA0}, St1); false -> St2 = St1#lint{type_id = {spec, MFA}}, check_specs(TypeSpecs, callback_wrong_arity, Arity, St2) end end. %% optional_callbacks(Anno, FAs, State) -> State. optional_callbacks(Anno, Term, St0) -> try true = is_fa_list(Term), Term of FAs -> optional_cbs(Anno, FAs, St0) catch _:_ -> St0 % ignore others end. optional_cbs(_Anno, [], St) -> St; optional_cbs(Anno, [{F,A}|FAs], St0) -> #lint{optional_callbacks = OptionalCbs, module = Mod} = St0, MFA = {Mod, F, A}, St1 = St0#lint{optional_callbacks = maps:put(MFA, Anno, OptionalCbs)}, St2 = case is_map_key(MFA, OptionalCbs) of true -> add_error(Anno, {redefine_optional_callback, {F,A}}, St1); false -> St1 end, optional_cbs(Anno, FAs, St2). is_fa_list([E|L]) -> is_fa(E) andalso is_fa_list(L); is_fa_list([]) -> true; is_fa_list(_) -> false. is_fa({FuncName, Arity}) when is_atom(FuncName), is_integer(Arity), Arity >= 0 -> true; is_fa(_) -> false. check_module_name(M, Anno, St0) -> AllChars = atom_to_list(M), VisibleChars = remove_non_visible(AllChars), case {AllChars, VisibleChars} of {[], []} -> add_error(Anno, empty_module_name, St0); {[_|_], []} -> add_error(Anno, blank_module_name, St0); {Cs,[_|_]} -> St1 = case io_lib:latin1_char_list(Cs) of true -> St0; false -> add_error(Anno, non_latin1_module_unsupported, St0) end, case any_control_characters(Cs) of true -> add_error(Anno, ctrl_chars_in_module_name, St1); false -> St1 end end. remove_non_visible(Cs) -> SP = $\s, %Plain space. NBSP = 16#A0, %Non-breaking space. SHY = 16#AD, %Soft hyphen. [C || C <- Cs, C =/= SP, C =/= NBSP, C =/= SHY]. any_control_characters(Cs) -> any(fun(C) when is_integer(C), 0 =< C, C < 16#20; is_integer(C), 16#7F =< C, C < 16#A0 -> true; (_) -> false end, Cs). check_specs([FunType|Left], ETag, Arity, St0) -> {FunType1, CTypes} = case FunType of {type, _, bounded_fun, [FT = {type, _, 'fun', _}, Cs]} -> Types0 = [T || {type, _, constraint, [_, T]} <- Cs], {FT, lists:append(Types0)}; {type, _, 'fun', _} = FT -> {FT, []} end, {type, A, 'fun', [{type, _, product, D}, _]} = FunType1, SpecArity = length(D), St1 = case Arity =:= SpecArity of true -> St0; false -> %% Cannot happen if called from the compiler. add_error(A, ETag, St0) end, St2 = check_type({type, nowarn(), product, [FunType1|CTypes]}, St1), check_specs(Left, ETag, Arity, St2); check_specs([], _ETag, _Arity, St) -> St. nowarn() -> A0 = erl_anno:new(0), A1 = erl_anno:set_generated(true, A0), erl_anno:set_file("", A1). check_specs_without_function(#lint{module=Mod,defined=Funcs,specs=Specs}=St) -> Fun = fun({M, F, A}, Anno, AccSt) when M =:= Mod -> FA = {F, A}, case gb_sets:is_element(FA, Funcs) of true -> AccSt; false -> add_error(Anno, {spec_fun_undefined, FA}, AccSt) end; ({_M, _F, _A}, _Anno, AccSt) -> AccSt end, maps:fold(Fun, St, Specs). %% This generates warnings for functions without specs; if the user has %% specified both options, we do not generate the same warnings twice. check_functions_without_spec(Forms, St0) -> case is_warn_enabled(missing_spec_all, St0) of true -> add_missing_spec_warnings(Forms, St0, all); false -> case is_warn_enabled(missing_spec, St0) of true -> add_missing_spec_warnings(Forms, St0, exported); false -> St0 end end. 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 -> [{FA,Anno} || {function,Anno,F,A,_} <- Forms, not lists:member(FA = {F,A}, Specs)]; exported -> Exps0 = gb_sets:to_list(exports(St0)) -- pseudolocals(), Exps = Exps0 -- Specs, [{FA,Anno} || {function,Anno,F,A,_} <- Forms, member(FA = {F,A}, Exps)] end, foldl(fun ({FA,Anno}, St) -> add_warning(Anno, {missing_spec,FA}, St) end, St0, Warns). check_unused_types(Forms, St) -> case is_warn_enabled(unused_type, St) of true -> check_unused_types_1(Forms, St); false -> St end. check_unused_types_1(Forms, #lint{types=Ts}=St) -> case [File || {attribute,_A,file,{File,_Anno}} <- Forms] of [FirstFile|_] -> L = reached_types(St), UsedTypes = gb_sets:from_list(L), FoldFun = fun({{record, _}=_Type, 0}, _, AccSt) -> AccSt; % Before Erlang/OTP 19.0 (Type, #typeinfo{anno = Anno}, AccSt) -> case loc(Anno, AccSt) of {FirstFile, _} -> case gb_sets:is_member(Type, UsedTypes) of true -> AccSt; false -> Warn = {unused_type,Type}, add_warning(Anno, Warn, AccSt) end; _ -> %% No warns about unused types in include files AccSt end end, maps:fold(FoldFun, St, Ts); [] -> St end. reached_types(#lint{usage = Usage}) -> Es = [{From, {type, To}} || To := UsedTs <- Usage#usage.used_types, #used_type{at = From} <- UsedTs], Initial = initially_reached_types(Es), G = sofs:family_to_digraph(sofs:rel2fam(sofs:relation(Es))), R = digraph_utils:reachable(Initial, G), true = digraph:delete(G), [T || {type, T} <- R]. initially_reached_types(Es) -> [FromTypeId || {{T, _}=FromTypeId, _} <- Es, T =/= type]. check_local_opaque_types(St) -> #lint{types=Ts, exp_types=ExpTs} = St, FoldFun = fun(_Type, #typeinfo{attr = type}, AccSt) -> AccSt; (Type, #typeinfo{attr = opaque, anno = Anno}, AccSt) -> case gb_sets:is_element(Type, ExpTs) of true -> AccSt; false -> Warn = {not_exported_opaque,Type}, add_warning(Anno, Warn, AccSt) end end, maps:fold(FoldFun, St, Ts). check_dialyzer_attribute(Forms, St0) -> Vals = [{Anno,V} || {attribute,Anno,dialyzer,Val} <- Forms, V0 <- lists:flatten([Val]), V <- case V0 of {O,F} -> [{A,B} || A <- lists:flatten([O]), B <- lists:flatten([F])]; T -> [T] end], {Wellformed, Bad} = lists:partition(fun ({_,{Option,FA}}) when is_atom(Option) -> is_fa(FA); ({_,Option}) when is_atom(Option) -> true; (_) -> false end, Vals), St1 = foldl(fun ({Anno,Term}, St) -> add_error(Anno, {bad_dialyzer_attribute,Term}, St) end, St0, Bad), DefFunctions = (gb_sets:to_list(St0#lint.defined) -- pseudolocals()), Fun = fun ({Anno,{Option,FA}}, St) -> case is_function_dialyzer_option(Option) of true -> case lists:member(FA, DefFunctions) of true -> St; false -> add_error(Anno, {undefined_function,FA}, St) end; false -> add_error(Anno, {bad_dialyzer_option,Option}, St) end; ({Anno,Option}, St) -> case is_module_dialyzer_option(Option) of true -> St; false -> add_error(Anno, {bad_dialyzer_option,Option}, St) end end, foldl(Fun, St1, Wellformed). is_function_dialyzer_option(nowarn_function) -> true; is_function_dialyzer_option(Option) -> is_module_dialyzer_option(Option). is_module_dialyzer_option(Option) -> lists:member(Option, [no_return,no_unused,no_improper_lists,no_fun_app, no_match,no_opaque,no_fail_call,no_contracts,no_unknown, no_behaviours,no_undefined_callbacks,unmatched_returns, error_handling,race_conditions,no_missing_calls, specdiffs,overspecs,underspecs,unknown, no_underspecs,extra_return,no_extra_return, missing_return,no_missing_return,overlapping_contract ]). %% try_catch_clauses(Scs, Ccs, In, ImportVarTable, State) -> %% {UpdVt,State}. try_clauses(Scs, Ccs, In, Vt, Uvt, St0) -> {Csvt0,St1} = icrt_clauses(Scs, Vt, St0), {Csvt1,St2} = catch_clauses(Ccs, vtupdate(Uvt, Vt), St1), Csvt = Csvt0 ++ Csvt1, UpdVt = icrt_export(Csvt, Vt, In, St2), {UpdVt,St2}. %% icrt_clauses(Clauses, In, ImportVarTable, State) -> %% {UpdVt,State}. icrt_clauses(Cs, In, Vt, St0) -> {Csvt,St1} = icrt_clauses(Cs, Vt, St0), UpdVt = icrt_export(Csvt, Vt, In, St1), {UpdVt,St1}. %% icrt_clauses(Clauses, ImportVarTable, State) -> %% {NewVts,State}. icrt_clauses(Cs, Vt, St) -> mapfoldl(fun (C, St0) -> icrt_clause(C, Vt, St0) end, St, Cs). icrt_clause({clause,_Anno,H,G,B}, Vt0, St0) -> {Hvt,Hnew,St1} = head(H, Vt0, St0), Vt1 = vtupdate(Hvt, Hnew), {Gvt,St2} = guard(G, vtupdate(Vt1, Vt0), St1), Vt2 = vtupdate(Gvt, Vt1), {Bvt,St3} = exprs(B, vtupdate(Vt2, Vt0), St2), {vtupdate(Bvt, Vt2),St3}. catch_clauses(Cs, Vt, St) -> mapfoldl(fun(C, St0) -> catch_clause(C, Vt, St0) end, St, Cs). catch_clause({clause,_Anno,H,G,B}, Vt0, St0) -> [{tuple,_,[_,_,Stack]}] = H, {Hvt,Hnew,St1} = head(H, Vt0, St0), Vt1 = vtupdate(Hvt, Hnew), %% check and mark the stack trace variable before checking the guard {GuardVt,St2} = taint_stack_var(Stack, vtupdate(Vt1, Vt0), St1), {Gvt,St3} = guard(G, GuardVt, St2), Vt2 = vtupdate(Gvt, Vt1), {Bvt,St4} = exprs(B, vtupdate(Vt2, Vt0), St3), {vtupdate(Bvt, Vt2),St4}. taint_stack_var({var,Anno,V}, Vt, St) when V =/= '_' -> St1 = case orddict:find(V, Vt) of {ok,{_,used,_}} -> %% the stack var must be unused after processing the pattern; %% it can be used either if bound/unsafe before the try, or %% if it occurs in the class or term part of the pattern add_error(Anno, {stacktrace_bound,V}, St); _ -> St end, {vtupdate([{V,{stacktrace,unused,[Anno]}}], Vt), St1}; taint_stack_var(_, Vt, St) -> {Vt, St}. icrt_export(Vts, Vt, {Tag,Attrs}, St) -> {_File,Loc} = loc(Attrs, St), icrt_export(lists:merge(Vts), Vt, {Tag,Loc}, length(Vts), []). icrt_export([{V,{{export,_},_,_}}|Vs0], [{V,{{export,_}=S0,_,As}}|Vt], In, I, Acc) -> %% V was an exported variable and has been used in an expression in at least %% one clause. Its state needs to be merged from all clauses to silence any %% exported var warning already emitted. {VVs,Vs} = lists:partition(fun ({K,_}) -> K =:= V end, Vs0), S = foldl(fun ({_,{S1,_,_}}, AccS) -> merge_state(AccS, S1) end, S0, VVs), icrt_export(Vs, Vt, In, I, [{V,{S,used,As}}|Acc]); icrt_export([{V,_}|Vs0], [{V,{_,_,As}}|Vt], In, I, Acc) -> %% V was either unsafe or bound and has now been reused. It may also have %% been an export but as it was not matched by the previous clause, it means %% it has been changed to 'bound' in at least one clause because it was used %% in a pattern. Vs = lists:dropwhile(fun ({K,_}) -> K =:= V end, Vs0), icrt_export(Vs, Vt, In, I, [{V,{bound,used,As}}|Acc]); icrt_export([{V1,_}|_]=Vs, [{V2,_}|Vt], In, I, Acc) when V1 > V2 -> %% V2 was already in scope and has not been reused in any clause. icrt_export(Vs, Vt, In, I, Acc); icrt_export([{V,_}|_]=Vs0, Vt, In, I, Acc) -> %% V is a new variable. {VVs,Vs} = lists:partition(fun ({K,_}) -> K =:= V end, Vs0), F = fun ({_,{S,U,As}}, {AccI,AccS0,AccAs0}) -> AccS = case {S,AccS0} of {{unsafe,_},{unsafe,_}} -> %% V was found unsafe in a previous clause, mark %% it as unsafe for the whole parent expression. {unsafe,In}; {{unsafe,_},_} -> %% V was unsafe in a clause, keep that state and %% generalize it to the whole expression if it %% is found unsafe in another one. S; _ -> %% V is either bound or exported, keep original %% state. AccS0 end, AccAs = case U of used -> AccAs0; unused -> merge_annos(AccAs0, As) end, {AccI + 1,AccS,AccAs} end, %% Initial state is exported from the current expression. {Count,S1,As} = foldl(F, {0,{export,In},[]}, VVs), S = case Count of I -> %% V was found in all clauses, keep computed state. S1; _ -> %% V was not bound in some clauses, mark as unsafe. {unsafe,In} end, U = case As of [] -> used; _ -> unused end, icrt_export(Vs, Vt, In, I, [{V,{S,U,As}}|Acc]); icrt_export([], _, _, _, Acc) -> reverse(Acc). handle_comprehension(E, Qs, Vt0, St0) -> {Vt1, Uvt, St1} = lc_quals(Qs, Vt0, St0), {Evt,St2} = comprehension_expr(E, Vt1, St1), Vt2 = vtupdate(Evt, Vt1), %% Shadowed global variables. {_,St3} = check_old_unused_vars(Vt2, Uvt, St2), %% There may be local variables in Uvt that are not global. {_,St4} = check_unused_vars(Uvt, Vt0, St3), %% Local variables that have not been shadowed. {_,St} = check_unused_vars(Vt2, Vt0, St4), Vt3 = vtmerge(vtsubtract(Vt2, Uvt), Uvt), %% Don't export local variables. Vt4 = vtold(Vt3, Vt0), %% Forget about old variables which were not used as well as unsafe %% variables, preventing them from being marked as used and bound by %% icrt_export/4. Vt = vt_no_unsafe(vt_no_unused(Vt4)), {Vt, St}. comprehension_expr({map_field_assoc,_,K,V}, Vt0, St0) -> expr_list([K,V], Vt0, St0); comprehension_expr(E, Vt, St) -> expr(E, Vt, St). %% lc_quals(Qualifiers, ImportVarTable, State) -> %% {VarTable,ShadowedVarTable,State} %% Test list comprehension qualifiers, return all variables. Allow %% filters to be both guard tests and general expressions, but the errors %% will be for expressions. Return the complete updated vartable including %% local variables and all updates. ShadowVarTable contains the state of %% each shadowed variable. All variable states of variables in ImportVarTable %% that have been shadowed are included in ShadowVarTable. In addition, all %% shadowed variables that are not included in ImportVarTable are included %% in ShadowVarTable (these are local variables that are not global variables). lc_quals(Qs, Vt0, St0) -> OldRecDef = St0#lint.recdef_top, {Vt,Uvt,St} = lc_quals(Qs, Vt0, [], St0#lint{recdef_top = false}), {Vt,Uvt,St#lint{recdef_top = OldRecDef}}. lc_quals([{generate,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St0), lc_quals(Qs, Vt, Uvt, St); lc_quals([{b_generate,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> St1 = handle_bitstring_gen_pat(P,St0), {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St1), lc_quals(Qs, Vt, Uvt, St); lc_quals([{m_generate,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St0), lc_quals(Qs, Vt, Uvt, St); lc_quals([F|Qs], Vt, Uvt, St0) -> Info = is_guard_test2_info(St0), {Fvt,St1} = case is_guard_test2(F, Info) of true -> guard_test(F, Vt, St0); false -> expr(F, Vt, St0) end, lc_quals(Qs, vtupdate(Fvt, Vt), Uvt, St1); lc_quals([], Vt, Uvt, St) -> {Vt, Uvt, St}. is_guard_test2_info(#lint{records=RDs,locals=Locals,imports=Imports}) -> {RDs,fun(FA) -> is_local_function(Locals, FA) orelse is_imported_function(Imports, FA) end}. handle_generator(P,E,Vt,Uvt,St0) -> {Evt,St1} = expr(E, Vt, St0), %% Forget variables local to E immediately. Vt1 = vtupdate(vtold(Evt, Vt), Vt), {_, St2} = check_unused_vars(Evt, Vt, St1), {Pvt,Pnew,St3} = comprehension_pattern(P, Vt1, St2), %% Have to keep fresh variables separated from used variables somehow %% in order to handle for example X = foo(), [X || <> <- bar()]. %% 1 2 2 1 Vt2 = vtupdate(Pvt, Vt1), St4 = shadow_vars(Pnew, Vt1, generate, St3), Svt = vtold(Vt2, Pnew), {_, St5} = check_old_unused_vars(Svt, Uvt, St4), NUvt = vtupdate(vtnew(Svt, Uvt), Uvt), Vt3 = vtupdate(vtsubtract(Vt2, Pnew), Pnew), {Vt3,NUvt,St5}. comprehension_pattern({map_field_exact,_,K,V}, Vt, St) -> pattern_list([K,V], Vt, [], St); comprehension_pattern(P, Vt, St) -> pattern(P, Vt, [], St). handle_bitstring_gen_pat({bin,_,Segments=[_|_]},St) -> case lists:last(Segments) of {bin_element,Anno,_,default,Flags} when is_list(Flags) -> case member(binary, Flags) orelse member(bytes, Flags) orelse member(bits, Flags) orelse member(bitstring, Flags) of true -> add_error(Anno, unsized_binary_in_bin_gen_pattern, St); false -> St end; _ -> St end; handle_bitstring_gen_pat(_,St) -> St. %% fun_clauses(Clauses, ImportVarTable, State) -> %% {UsedVars, State}. %% Fun's cannot export any variables. %% It is an error if variable is bound inside a record definition %% unless it was introduced in a fun or an lc. Only if pat_var finds %% such variables can the correct line number be given. %% If fun_used_vars is set, we want to compute the tree of used %% vars across functions. This is by erl_eval to compute used vars %% without having to traverse the tree multiple times. fun_clauses(Cs, Vt, #lint{fun_used_vars=(#{}=FUV)} = St) -> {Uvt, St0} = fun_clauses1(Cs, Vt, St#lint{fun_used_vars=maps:new()}), #lint{fun_used_vars=InnerFUV} = St0, UsedVars = [V || {V, {_, used, _}} <- Uvt], OuterFUV = maps:put(Cs, {UsedVars, InnerFUV}, FUV), {Uvt, St0#lint{fun_used_vars=OuterFUV}}; fun_clauses(Cs, Vt, St) -> fun_clauses1(Cs, Vt, St). fun_clauses1(Cs, Vt, St) -> OldRecDef = St#lint.recdef_top, {Bvt,St2} = foldl(fun (C, {Bvt0, St0}) -> {Cvt,St1} = fun_clause(C, Vt, St0), {vtmerge(Cvt, Bvt0),St1} end, {[],St#lint{recdef_top = false}}, Cs), Uvt = vt_no_unsafe(vt_no_unused(vtold(Bvt, Vt))), {Uvt,St2#lint{recdef_top = OldRecDef}}. fun_clause({clause,_Anno,H,G,B}, Vt0, St0) -> {Hvt,Hnew,St1} = head(H, Vt0, [], St0), % No imported pattern variables Vt1 = vtupdate(Hvt, Vt0), St2 = shadow_vars(Hnew, Vt0, 'fun', St1), Vt2 = vtupdate(vtsubtract(Vt1, Hnew), Hnew), {Gvt,St3} = guard(G, Vt2, St2), Vt3 = vtupdate(Gvt, Vt2), {Bvt,St4} = exprs(B, Vt3, St3), Cvt = vtupdate(Bvt, Vt3), %% Check new local variables. {_, St5} = check_unused_vars(Cvt, Vt0, St4), %% Check all shadowing variables. Svt = vtold(Vt1, Hnew), {_, St6} = check_old_unused_vars(Cvt, Svt, St5), Vt4 = vtmerge(Svt, vtsubtract(Cvt, Svt)), {vtold(Vt4, Vt0),St6}. %% In the variable table we store information about variables. The %% information is a tuple {State,Usage,Annos}, the variables state and %% usage. A variable can be in the following states: %% %% bound everything is normal %% {export,From} variable has been exported %% {unsafe,In} variable is unsafe %% %% The usage information has the following form: %% %% used variable has been used %% unused variable has been bound but not used %% %% Annos is a list of annotations including the location where the %% variable was bound. %% %% Report variable errors/warnings as soon as possible and then change %% the state to ok. This simplifies the code and reports errors only %% once. Having the usage information like this makes it easy too when %% merging states. %% For keeping track of which variables are bound, ordsets are used. %% In order to be able to give warnings about unused variables, a %% possible value is {bound, unused, [Anno]}. The usual value when a %% variable is used is {bound, used, [Anno]}. An exception occurs for %% variables in the size position in a bin element in a pattern. %% Currently, such a variable is never matched out, always used, and %% therefore it makes no sense to warn for "variable imported in %% match". %% For storing the variable table we use the orddict module. %% We know an empty set is []. %% pat_var(Variable, Anno, VarTable, NewVars, State) -> %% {UpdVarTable,UpdNewVars,State} %% A pattern variable has been found. Handle errors and warnings. Return %% all used variables as bound so errors and warnings are only reported %% once. New shadows Vt here, which is necessary in order to separate %% uses of shadowed and shadowing variables. See also pat_binsize_var. pat_var(V, Anno, Vt, New, St0) -> case orddict:find(V, New) of {ok, {bound,_Usage,As}} -> %% variable already in NewVars, mark as used St = warn_underscore_match(V, Anno, St0), {[],[{V,{bound,used,As}}],St}; error -> case orddict:find(V, Vt) of {ok,{bound,_Usage,Ls}} -> St = warn_underscore_match(V, Anno, St0), {[{V,{bound,used,Ls}}],[],St}; {ok,{{unsafe,In},_Usage,Ls}} -> {[{V,{bound,used,Ls}}],[], 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)}; error when St0#lint.recdef_top -> {[],[{V,{bound,unused,[Anno]}}], add_error(Anno, {variable_in_record_def,V}, St0)}; error -> %% add variable to NewVars, not yet used {[],[{V,{bound,unused,[Anno]}}],St0} end end. %% Underscore-prefixed variables merely suppress unused variable warnings, but %% are often misunderstood and thought to behave like '_' which can introduce %% subtle errors. Warn about this since matching them on purpose is rare. warn_underscore_match(V, Anno, St) -> case {is_warn_enabled(underscore_match, St), atom_to_list(V)} of {true, [$_ | _]} -> add_warning(Anno, {match_underscore_var,V}, St); {_, _} -> St end. warn_underscore_match_pat(V, Annos, St) -> case {is_warn_enabled(underscore_match, St), atom_to_list(V)} of {true, [$_ | _]} -> warn_underscore_match_pat_1(Annos, V, St); {_, _} -> St end. warn_underscore_match_pat_1([Anno | Annos], V, St0) -> St = add_warning(Anno, {match_underscore_var_pat,V}, St0), warn_underscore_match_pat_1(Annos, V, St); warn_underscore_match_pat_1([], _V, St) -> St. %% pat_binsize_var(Variable, LineNo, VarTable, NewVars, State) -> %% {UpdVarTable,UpdNewVars,State'} %% Special case of pat_var/expr_var for variables in binary size expressions %% (never adds variables to NewVars, only marks uses). pat_binsize_var(V, Anno, Vt, New, St) -> case orddict:find(V, New) of {ok,{bound,_Used,As}} -> {[],[{V,{bound,used,As}}],St}; error -> case orddict:find(V, Vt) of {ok,{bound,_Used,As}} -> {[{V,{bound,used,As}}],[],St}; {ok,{{unsafe,In},_Used,As}} -> {[{V,{bound,used,As}}],[], 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 %% probably safe. exported_var(Anno, V, From, St)}; error -> {[{V,{bound,used,[Anno]}}],[], add_error(Anno, {unbound_var,V}, St)} end end. %% expr_var(Variable, Anno, VarTable, State) -> %% {UpdVarTable,State} %% Check if a variable is defined, or if there is an error or warning %% connected to its usage. Return all variables as bound so errors %% and warnings are only reported once. As this is not matching %% exported vars are probably safe, warn only if warn_export_vars is %% set. expr_var(V, Anno, Vt, #lint{bvt=none}=St) -> do_expr_var(V, Anno, Vt, St); expr_var(V, Anno, Vt0, #lint{bvt=Bvt0}=St0) when is_list(Bvt0) -> %% handles variables in a binary segment size expression {Vt,Bvt,St} = pat_binsize_var(V, Anno, Vt0, Bvt0, St0), {Vt,St#lint{bvt=vtmerge(Bvt0, Bvt)}}. 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}} -> {[{V,{bound,used,As}}], add_error(Anno, {unsafe_var,V,In}, St)}; {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)}; false -> {[{V,{{export,From},used,As}}],St} end; {ok,{stacktrace,_Usage,As}} -> {[{V,{bound,used,As}}], add_error(Anno, {stacktrace_guard,V}, St)}; error -> {[{V,{bound,used,[Anno]}}], add_error(Anno, {unbound_var,V}, St)} end. exported_var(Anno, V, From, St) -> case is_warn_enabled(export_vars, St) of true -> add_warning(Anno, {exported_var,V,From}, St); false -> St end. 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); (_, St) -> St end, St0, vtold(Vt, vt_no_unsafe(Vt0))); false -> St0 end. check_unused_vars(Vt, Vt0, St0) -> U = unused_vars(Vt, Vt0, St0), warn_unused_vars(U, Vt, St0). check_old_unused_vars(Vt, Vt0, St0) -> U = unused_vars(vtold(Vt, Vt0), [], St0), warn_unused_vars(U, Vt, St0). unused_vars(Vt, Vt0, _St0) -> U0 = orddict:filter(fun (V, {_State,unused,_As}) -> case atom_to_list(V) of "_"++_ -> false; _ -> true end; (_V, _How) -> false end, Vt), vtnew(U0, Vt0). % Only new variables. warn_unused_vars([], Vt, St0) -> {Vt,St0}; warn_unused_vars(U, Vt, St0) -> St1 = case is_warn_enabled(unused_vars, St0) of false -> St0; true -> foldl(fun ({V,{_,unused,As}}, St) -> foldl(fun (A, St2) -> add_warning(A, {unused_var,V}, St2) end, St, As) end, St0, U) end, %% Return all variables as bound so warnings are only reported once. UVt = map(fun ({V,{State,_,As}}) -> {V,{State,used,As}} end, U), {vtmerge(Vt, UVt), St1}. %% vtupdate(UpdVarTable, VarTable) -> VarTable. %% Add the variables in the updated vartable to VarTable. The variables %% will be updated with their property in UpdVarTable. The state of %% the variables in UpdVarTable will be returned. vtupdate(Uvt, Vt0) -> orddict:merge(fun (_V, {S,U1,A1}, {_S,U2,A2}) -> {S, merge_used(U1, U2), merge_annos(A1, A2)} end, Uvt, Vt0). %% vtunsafe(From, UpdVarTable, VarTable) -> UnsafeVarTable. %% Return all new variables in UpdVarTable as unsafe. vtunsafe({Tag,Anno}, Uvt, Vt) -> Location = erl_anno:location(Anno), [{V,{{unsafe,{Tag,Location}},U,As}} || {V,{_,U,As}} <- vtnew(Uvt, Vt)]. %% vtmerge(VarTable, VarTable) -> VarTable. %% Merge two variables tables generating a new vartable. Give priority to %% errors then warnings. vtmerge(Vt1, Vt2) -> orddict:merge(fun (_V, {S1,U1,A1}, {S2,U2,A2}) -> {merge_state(S1, S2), merge_used(U1, U2), merge_annos(A1, A2)} end, Vt1, Vt2). vtmerge(Vts) -> foldl(fun (Vt, Mvts) -> vtmerge(Vt, Mvts) end, [], Vts). %% This is similar to vtmerge/2 but marks repeated variables as 'used' because %% they're used in the pattern itself (matching). It also returns warnings for %% iffy variable uses when appropriate, such as matching on underscore-prefixed %% variables: %% %% f(_Same, _Same) -> ok. %% Should raise a warning vtmerge_pat(VtA, VtB, St0) -> Vt0 = orddict:merge(fun(_V, {S1, Usage1, Annos1}, {S2, Usage2, Annos2}) -> Annos = merge_annos(Annos1, Annos2), Usage = case {Usage1, Usage2} of {unused, unused} -> {matched, Annos}; {unused, _} -> {matched, Annos1}; {_, unused} -> {matched, Annos2}; {_, _} -> used end, {merge_state(S1, S2), Usage, Annos} end, VtA, VtB), lists:mapfoldl(fun({Name, {State, {matched, MatchAs}, As}}, St1) -> St = warn_underscore_match_pat(Name, MatchAs, St1), {{Name, {State, used, As}}, St}; (Var, St) -> {Var, St} end, St0, Vt0). merge_annos(As1, As2) -> ordsets:union(As1,As2). merge_state({unsafe,_F1}=S1, _S2) -> S1; %Take the error case merge_state(_S1, {unsafe,_F2}=S2) -> S2; merge_state(bound, S2) -> S2; %Take the warning merge_state(S1, bound) -> S1; merge_state({export,F1},{export,_F2}) -> %Sanity check %% We want to report the outermost construct {export,F1}. merge_used(used, _Usage2) -> used; merge_used(_Usage1, used) -> used; merge_used(unused, unused) -> unused. %% vtnew(NewVarTable, OldVarTable) -> NewVarTable. %% Return all the truly new variables in NewVarTable. vtnew(New, Old) -> orddict:filter(fun (V, _How) -> not orddict:is_key(V, Old) end, New). %% vtsubtract(VarTable1, VarTable2) -> NewVarTable. %% Return all the variables in VarTable1 which don't occur in VarTable2. %% Same thing as vtnew, but a more intuitive name for some uses. vtsubtract(New, Old) -> vtnew(New, Old). %% vtold(NewVarTable, OldVarTable) -> OldVarTable. %% Return all the truly old variables in NewVarTable. vtold(New, Old) -> orddict:filter(fun (V, _How) -> orddict:is_key(V, Old) end, New). vt_no_unsafe(Vt) -> [V || {_,{S,_U,_A}}=V <- Vt, case S of {unsafe,_} -> false; _ -> true end]. vt_no_unused(Vt) -> [V || {_,{_,U,_A}}=V <- Vt, U =/= unused]. %% copy_expr(Expr, Anno) -> Expr. %% Make a copy of Expr converting all annotations numbers to Anno. copy_expr(Expr, Anno) -> erl_parse:map_anno(fun(_A) -> Anno end, Expr). %% Check a record_info call. We have already checked that it is not %% shadowed by an import. check_record_info_call(_Anno,Aa,[{atom,Ai,Info},{atom,_An,Name}],St) -> case member(Info, [fields,size]) of true -> exist_record(Aa, Name, St); false -> add_error(Ai, illegal_record_info, St) end; check_record_info_call(Anno,_Aa,_As,St) -> add_error(Anno, illegal_record_info, St). has_wildcard_field([{record_field,_Af,{var,Aa,'_'},_Val}|_Fs]) -> Aa; has_wildcard_field([_|Fs]) -> has_wildcard_field(Fs); has_wildcard_field([]) -> no. %% check_remote_function(Anno, ModuleName, FuncName, [Arg], State) -> State. %% Perform checks on known remote calls. check_remote_function(Anno, M, F, As, St0) -> St1 = deprecated_function(Anno, M, F, As, St0), St2 = check_qlc_hrl(Anno, M, F, As, St1), St3 = check_load_nif(Anno, M, F, As, St2), format_function(Anno, M, F, As, St3). %% check_load_nif(Anno, ModName, FuncName, [Arg], State) -> State %% Add warning if erlang:load_nif/2 is called when any kind of inlining has %% been enabled. check_load_nif(Anno, erlang, load_nif, [_, _], St0) -> St = St0#lint{load_nif = true}, case is_warn_enabled(nif_inline, St) of true -> check_nif_inline(Anno, St); false -> St end; check_load_nif(_Anno, _ModName, _FuncName, _Args, St) -> St. check_nif_inline(Anno, St) -> case any(fun is_inline_opt/1, St#lint.compile) of true -> add_warning(Anno, nif_inline, St); false -> St end. is_inline_opt({inline, [_|_]=_FAs}) -> true; is_inline_opt(inline) -> true; is_inline_opt(_) -> false. %% check_qlc_hrl(Anno, ModName, FuncName, [Arg], State) -> State %% Add warning if qlc:q/1,2 has been called but qlc.hrl has not %% been included. check_qlc_hrl(Anno, M, F, As, St) -> Arity = length(As), case As of [{lc,_A,_E,_Qs}|_] when M =:= qlc, F =:= q, Arity < 3, not St#lint.xqlc -> add_warning(Anno, {missing_qlc_hrl, Arity}, St); _ -> St end. %% deprecated_function(Anno, ModName, FuncName, [Arg], State) -> State. %% Add warning for calls to deprecated functions. -dialyzer({no_match, deprecated_function/5}). deprecated_function(Anno, M, F, As, St) -> Arity = length(As), MFA = {M, F, Arity}, case otp_internal:obsolete(M, F, Arity) of {deprecated, String} when is_list(String) -> case not is_warn_enabled(deprecated_function, St) orelse ordsets:is_element(MFA, St#lint.not_deprecated) of true -> St; false -> add_warning(Anno, {deprecated, MFA, String}, St) end; {deprecated, Replacement, Rel} -> case not is_warn_enabled(deprecated_function, St) orelse ordsets:is_element(MFA, St#lint.not_deprecated) of true -> St; false -> add_warning(Anno, {deprecated, MFA, Replacement, Rel}, St) end; {removed, String} when is_list(String) -> add_removed_warning(Anno, MFA, {removed, MFA, String}, St); {removed, Replacement, Rel} -> add_removed_warning(Anno, MFA, {removed, MFA, Replacement, Rel}, St); no -> St end. add_removed_warning(Anno, {M, _, _}=MFA, Warning, #lint{not_removed=NotRemoved}=St) -> case is_warn_enabled(removed, St) andalso not gb_sets:is_element(M, NotRemoved) andalso not gb_sets:is_element(MFA, NotRemoved) of true -> add_warning(Anno, Warning, St); false -> St end. -dialyzer({no_match, deprecated_type/5}). 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) -> case is_warn_enabled(deprecated_type, St) of true -> add_warning(Anno, {deprecated_type, {M,N,NAs}, String}, St); false -> St end; {removed, String} -> add_warning(Anno, {removed_type, {M,N,NAs}, String}, St); no -> St end. obsolete_guard({call,Anno,{atom,Ar,F},As}, St0) -> Arity = length(As), case erl_internal:old_type_test(F, Arity) of false -> deprecated_function(Anno, erlang, F, As, St0); true -> St = case is_warn_enabled(obsolete_guard, St0) of true -> add_warning(Ar, {obsolete_guard, {F, Arity}}, St0); false -> St0 end, test_overriden_by_local(Ar, F, Arity, St) end; obsolete_guard(_G, St) -> St. test_overriden_by_local(Anno, OldTest, Arity, St) -> ModernTest = list_to_atom("is_"++atom_to_list(OldTest)), case is_local_function(St#lint.locals, {ModernTest, Arity}) of true -> add_error(Anno, {obsolete_guard_overridden,OldTest}, St); false -> St end. feature_keywords() -> Features = erl_features:configurable(), G = fun(Ftr, Map) -> Keywords = erl_features:keywords(Ftr), Add = fun(Keyword, M) -> maps:put(Keyword, Ftr, M) end, lists:foldl(Add, Map, Keywords) end, lists:foldl(G, #{}, Features). %% keyword_warning(Anno, Atom, State) -> State. %% Add warning for atoms that will be reserved keywords in the future. %% (Currently, no such keywords to warn for.) keyword_warning(Anno, Atom, St) -> case is_warn_enabled(keyword_warning, St) of true -> case erl_anno:text(Anno) of [$'| _] -> %% Don't warn for quoted atoms St; _ -> Keywords = St#lint.feature_keywords, case maps:find(Atom, Keywords) of error -> St; {ok, Ftr} -> add_warning(Anno, {future_feature, Ftr, Atom}, St) end end; false -> St end. %% format_function(Anno, ModName, FuncName, [Arg], State) -> State. %% Add warning for bad calls to io:fwrite/format functions. format_function(DefAnno, M, F, As, St) -> maybe true ?= is_format_function(M, F), Lev = St#lint.warn_format, true ?= Lev > 0, case check_format_1(As) of {warn,Level,Fmt,Fas} when Level =< Lev -> add_warning(DefAnno, {format_error,{Fmt,Fas}}, St); {warn,Level,Anno,Fmt,Fas} when Level =< Lev -> add_warning(Anno, {format_error,{Fmt,Fas}}, St); _ -> St end else false -> St end. is_format_function(io, fwrite) -> true; is_format_function(io, format) -> true; is_format_function(io_lib, fwrite) -> true; is_format_function(io_lib, format) -> true; is_format_function(M, F) when is_atom(M), is_atom(F) -> false. %% check_format_1([Arg]) -> ok | {warn,Level,Format,[Arg]}. check_format_1([Fmt]) -> check_format_1([Fmt,no_argument_list]); check_format_1([Fmt,As]) -> check_format_2(Fmt, canonicalize_string(As)); check_format_1([_Dev,Fmt,As]) -> check_format_1([Fmt,As]); check_format_1(_As) -> {warn,1,"format call with wrong number of arguments",[]}. canonicalize_string({string,Anno,Cs}) -> foldr(fun (C, T) -> {cons,Anno,{integer,Anno,C},T} end, {nil,Anno}, Cs); canonicalize_string(Term) -> Term. check_format_2(Fmt, As) -> case Fmt of {string,A,S} -> check_format_2a(S, A, As); {atom,A,Atom} -> check_format_2a(atom_to_list(Atom), A, As); _ -> Anno = erl_parse:first_anno(Fmt), {warn,2,Anno,"format string not a textual constant",[]} end. check_format_2a(Fmt, FmtAnno, no_argument_list=As) -> check_format_3(Fmt, FmtAnno, As); check_format_2a(Fmt, FmtAnno, As) -> case args_list(As) of true -> check_format_3(Fmt, FmtAnno, As); false -> Anno = element(2, As), {warn,1,Anno,"format arguments not a list",[]}; 'maybe' -> Anno = erl_parse:first_anno(As), {warn,2,Anno,"format arguments perhaps not a list",[]} end. check_format_3(Fmt, FmtAnno, As) -> case check_format_string(Fmt) of {ok,Need} -> check_format_4(Need, FmtAnno, As); {error,S} -> {warn,1,FmtAnno,"format string invalid (~ts)",[S]} end. check_format_4([], _FmtAnno, no_argument_list) -> ok; check_format_4(Need, FmtAnno, no_argument_list) -> Msg = "the format string requires an argument list with ~s, but no argument list is given", {warn,1,FmtAnno,Msg,[arguments(length(Need))]}; check_format_4(Need, _FmtAnno, As) -> Anno = element(2, As), Prefix = "the format string requires an argument list with ~s, but the argument list ", case {args_length(As),length(Need)} of {Same,Same} -> ok; {Actual,0} -> Msg = "the format string requires an empty argument list, but the argument list contains ~s", {warn,1,Anno,Msg,[arguments(Actual)]}; {0,Needed} -> Msg = Prefix ++ "is empty", {warn,1,Anno,Msg,[arguments(Needed)]}; {Actual,Needed} when Actual < Needed -> Msg = Prefix ++ "contains only ~s", {warn,1,Anno,Msg,[arguments(Needed),arguments(Actual)]}; {Actual,Needed} when Actual > Needed -> Msg = Prefix ++ "contains ~s", {warn,1,Anno,Msg,[arguments(Needed),arguments(Actual)]} end. arguments(1) -> "1 argument"; arguments(N) -> [integer_to_list(N), " arguments"]. args_list({cons,_A,_H,T}) -> args_list(T); %% Strange case: user has written something like [a | "bcd"]; pretend %% we don't know: args_list({string,_A,_Cs}) -> 'maybe'; args_list({nil,_A}) -> true; args_list({atom,_,_}) -> false; args_list({integer,_,_}) -> false; args_list({float,_,_}) -> false; args_list(_Other) -> 'maybe'. args_length({cons,_A,_H,T}) -> 1 + args_length(T); args_length({nil,_A}) -> 0. check_format_string(Fmt) when is_atom(Fmt) -> check_format_string(atom_to_list(Fmt)); check_format_string(Fmt) when is_binary(Fmt) -> check_format_string(binary_to_list(Fmt)); check_format_string(Fmt) -> extract_sequences(Fmt, []). extract_sequences(Fmt, Need0) -> case string:find(Fmt, [$~]) of nomatch -> {ok,lists:reverse(Need0)}; %That's it [$~|Fmt1] -> case extract_sequence(1, Fmt1, Need0) of {ok,Need1,Rest} -> extract_sequences(Rest, Need1); Error -> Error end end. extract_sequence(1, [$-,C|Fmt], Need) when is_integer(C), C >= $0, C =< $9 -> extract_sequence_digits(1, Fmt, Need); extract_sequence(1, [C|Fmt], Need) when is_integer(C), C >= $0, C =< $9 -> extract_sequence_digits(1, Fmt, Need); extract_sequence(1, [$-,$*|Fmt], Need) -> extract_sequence(2, Fmt, [int|Need]); extract_sequence(1, [$*|Fmt], Need) -> extract_sequence(2, Fmt, [int|Need]); extract_sequence(1, Fmt, Need) -> extract_sequence(2, Fmt, Need); extract_sequence(2, [$.,C|Fmt], Need) when is_integer(C), C >= $0, C =< $9 -> extract_sequence_digits(2, Fmt, Need); extract_sequence(2, [$.,$*|Fmt], Need) -> extract_sequence(3, Fmt, [int|Need]); extract_sequence(2, [$.|Fmt], Need) -> extract_sequence(3, Fmt, Need); extract_sequence(2, Fmt, Need) -> extract_sequence(4, Fmt, Need); extract_sequence(3, [$.,$*|Fmt], Need) -> extract_sequence(4, Fmt, [int|Need]); extract_sequence(3, [$.,_|Fmt], Need) -> extract_sequence(4, Fmt, Need); extract_sequence(3, Fmt, Need) -> extract_sequence(4, Fmt, Need); extract_sequence(4, Fmt0, Need) -> case extract_modifiers(Fmt0, []) of {error, _} = Error -> Error; {[C|Fmt], Modifiers} -> maybe ok ?= check_modifiers(C, Modifiers), case ordsets:is_element($K, Modifiers) of true -> extract_sequence(5, [C|Fmt], ['fun'|Need]); false -> extract_sequence(5, [C|Fmt], Need) end end; {[], _} -> extract_sequence(5, [], Need) end; extract_sequence(5, [C|Fmt], Need0) -> case control_type(C, Need0) of error -> {error,"invalid control ~" ++ [C]}; Need1 -> {ok,Need1,Fmt} end; extract_sequence(_, [], _Need) -> {error,"truncated"}. extract_sequence_digits(Fld, [C|Fmt], Need) when is_integer(C), C >= $0, C =< $9 -> extract_sequence_digits(Fld, Fmt, Need); extract_sequence_digits(Fld, Fmt, Need) -> extract_sequence(Fld+1, Fmt, Need). extract_modifiers([C|Fmt], Modifiers0) -> case is_modifier(C) of true -> case ordsets:add_element(C, Modifiers0) of Modifiers0 -> {error, "repeated modifier " ++ [C]}; Modifiers -> extract_modifiers(Fmt, Modifiers) end; false -> {[C|Fmt], Modifiers0} end; extract_modifiers([], Modifiers) -> {[], Modifiers}. check_modifiers(C, Modifiers) -> maybe ok ?= check_modifiers_1("l", Modifiers, C, "Pp"), ok ?= check_modifiers_1("lt", Modifiers, C, "cPpsWw"), ok ?= check_modifiers_1("Kk", Modifiers, C, "PpWw") end. check_modifiers_1(M, Modifiers, C, Cs) -> case ordsets:intersection(ordsets:from_list(M), Modifiers) of [_]=Mod -> case lists:member(C, Cs) of true -> ok; false -> {error, "invalid modifier/control combination ~" ++ Mod ++ [C]} end; [] -> ok; [_,_]=M -> {error, "conflicting modifiers ~" ++ M ++ [C]} end. is_modifier($k) -> true; is_modifier($K) -> true; is_modifier($l) -> true; is_modifier($t) -> true; is_modifier(_) -> false. control_type($~, Need) -> Need; control_type($c, Need) -> [int|Need]; control_type($f, Need) -> [float|Need]; control_type($e, Need) -> [float|Need]; control_type($g, Need) -> [float|Need]; control_type($s, Need) -> [string|Need]; control_type($w, Need) -> [term|Need]; control_type($p, Need) -> [term|Need]; control_type($W, Need) -> [int,term|Need]; %% Note: reversed control_type($P, Need) -> [int,term|Need]; %% Note: reversed control_type($b, Need) -> [int|Need]; control_type($B, Need) -> [int|Need]; control_type($x, Need) -> [string,int|Need]; %% Note: reversed control_type($X, Need) -> [string,int|Need]; %% Note: reversed control_type($+, Need) -> [term|Need]; control_type($#, Need) -> [term|Need]; control_type($n, Need) -> Need; control_type($i, Need) -> [term|Need]; control_type(_C, _Need) -> error. %% Prebuild set of local functions (to override auto-import) local_functions(Forms) -> gb_sets:from_list([ {Func,Arity} || {function,_,Func,Arity,_} <- Forms ]). %% Predicate to find out if the function is locally defined is_local_function(LocalSet,{Func,Arity}) -> gb_sets:is_element({Func,Arity},LocalSet). %% Predicate to see if a function is explicitly imported is_imported_function(ImportSet,{Func,Arity}) -> case orddict:find({Func,Arity}, ImportSet) of {ok,_Mod} -> true; error -> false end. %% Predicate to see if a function is explicitly imported from the erlang module is_imported_from_erlang(ImportSet,{Func,Arity}) -> case orddict:find({Func,Arity}, ImportSet) of {ok,erlang} -> true; _ -> false end. %% Build set of functions where auto-import is explicitly suppressed auto_import_suppressed(CompileFlags) -> case lists:member(no_auto_import, CompileFlags) of true -> all; false -> L0 = [ X || {no_auto_import,X} <- CompileFlags ], L1 = [ {Y,Z} || {Y,Z} <- lists:flatten(L0), is_atom(Y), is_integer(Z) ], gb_sets:from_list(L1) end. %% Predicate to find out if autoimport is explicitly suppressed for a function is_autoimport_suppressed(all,{_Func,_Arity}) -> true; is_autoimport_suppressed(NoAutoSet,{Func,Arity}) -> gb_sets:is_element({Func,Arity},NoAutoSet). %% Predicate to find out if a function specific bif-clash suppression (old deprecated) is present bif_clash_specifically_disabled(St,{F,A}) -> lists:member({F,A},St#lint.nowarn_bif_clash). %% Predicate to find out if an autoimported guard_bif is not overriden in some way %% Guard Bif without module name is disallowed if %% * It is overridden by local function %% * It is overridden by -import and that import is not of itself (i.e. from module erlang) %% * The autoimport is suppressed or it's not reimported by -import directive %% Otherwise it's OK (given that it's actually a guard bif and actually is autoimported) no_guard_bif_clash(St,{F,A}) -> ( (not is_local_function(St#lint.locals,{F,A})) andalso ( (not is_imported_function(St#lint.imports,{F,A})) orelse is_imported_from_erlang(St#lint.imports,{F,A}) ) andalso ( (not is_autoimport_suppressed(St#lint.no_auto, {F,A})) orelse is_imported_from_erlang(St#lint.imports,{F,A}) ) ). %% maps_prepend(Key, Value, Map) -> Map. maps_prepend(Key, Value, Map) -> case maps:find(Key, Map) of {ok, Values} -> maps:put(Key, [Value|Values], Map); error -> maps:put(Key, [Value], Map) end.