%%%---------------------------------------------------------------------------- %%% File : seqvar.erl %%% Author : Anders Dahlin %%% Purpose : Check to see if variables is handled in sequence %%% Created : 28 Mar 2008 by Anders Dahlin %%%---------------------------------------------------------------------------- -module(seqvar). -author('Anders Dahlin'). -vsn("1.1"). -export([check/0, check/1, check/2]). -include_lib("kernel/include/file.hrl"). -record(opts, {incl = nil, excl = nil, incldir = nil, excldir = nil, inclvar = nil, exclvar = nil, recurse = false, verbose = false }). -define(default_dirs, []). %%------------------------------------------------------------------------------ %% Func : check/0, 1, 2 %% %% Purpose : Main function. Initiates what should be checked %% %% Args : Arg : DoFs or Options, see below %% DoFs : Path() %% Path to current file (might be header file) %% Options : List() %% Option list, will be handled by fix_opts %% %% Returns : void() %%------------------------------------------------------------------------------ check() -> check([]). check(Arg) -> case Arg of [X| _Rest] when atom(X); tuple(X) -> % Arg = options {ok, Cwd} = file:get_cwd(), check(Cwd, Arg); _Other -> check(Arg, []) % Arg = dir|file end. check(DoFs, Options)-> Opts = fix_opts(Options), Files = get_files(DoFs, Opts), OrgPath = code:get_path(), OtpSrcPath = ebin_to_src(OrgPath), Includes = ?default_dirs ++ OtpSrcPath, check_files(Files, Opts, Includes). %%------------------------------------------------------------------------------ %% Func : check_files/3 %% %% Purpose : Main function. Initiates what should be checked %% %% Args : Files : [Path()] %% List of files to be checked %% Opts : #opts{} %% Includes : Files to include in the check %% %% Returns : void() %%------------------------------------------------------------------------------ check_files([File| Rest], Opts, Includes) -> io:format("Checking file ~p\n", [File]), case epp:open(File, [filename:dirname(File)| Includes], []) of {ok, Epp} -> parse_file(Epp, File, Opts), epp:close(Epp); Error -> io:format("Unable to open ~s due to ~p", [File, Error]) end, io:nl(), check_files(Rest, Opts, Includes); check_files([], _Opts, _Includes) -> done. %%------------------------------------------------------------------------------ %% Func : parse_file/2 %% %% Purpose : Parse the module abstract form by abstract form %% %% Args : Epp : File descriptor of a file opened for parsing %% CF : Path() %% Path to current file (might be header file) %% Opts : #opts{} %% %% Returns : void() %%------------------------------------------------------------------------------ parse_file(Epp, CF, Opts) -> case epp:parse_erl_form(Epp) of {ok, AbsForm} -> NewCF = parse_abs_form(AbsForm, CF, Opts), parse_file(Epp, NewCF, Opts); {eof, _} -> ok; {error, Error} -> io:format("Unable to parse ~s due to ~p", [CF, Error]), parse_file(Epp, CF, Opts) % correct? end. %%------------------------------------------------------------------------------ %% Func : parse_abs_form/2 %% %% Purpose : Parse an absolute erlang form %% %% Args : AbsForm : Absolute form returned by epp:parse_erl_form %% CF : see parse_file/2 %% Opts : see parse_file/2 %% %% Returns : Path() %%------------------------------------------------------------------------------ parse_abs_form({attribute, _L, Attribute, Value}, CF, _Opts) -> attribute(Attribute, Value, CF); parse_abs_form({function, _L, F, A, Clauses}, CF, Opts) -> func(F, A, Clauses, Opts), CF; parse_abs_form(_AbsForm, CF, _Opts) -> % ignoring others CF. %%------------------------------------------------------------------------------ %% Func : attribute/3 %% %% Purpose : Parse an attribute. Only file attributes are handled. %% All others are disregarded. %% Args : Attribute : Atom() %% file | export | import | compile | Atom() %% Value : Term() %% CF : see parse_file/2 %% %% Returns : Path() %%------------------------------------------------------------------------------ %% Handle the file attribute attribute(file, {NewCF, _L}, _CF) -> filename:absname(NewCF); %% Disregard all other attributes attribute(_Attribute, _Value, CF) -> CF. %%------------------------------------------------------------------------------ %% Func : func/4 %% %% Purpose : Parse a function %% %% Args : F : Atom() - function %% A : Int() - arity %% Clauses : List() %% Opts : see parse_file/2 %% %% Returns : void %%------------------------------------------------------------------------------ func(_F, _A, Clauses, Opts) -> clauses(Clauses, Opts). %%------------------------------------------------------------------------------ %% Func : clauses/2 %% %% Purpose : Parse a list of function clauses. %% %% Args : Clauses : List() %% [{clause, Line, Parameters, Guards, Body},...] %% Opts : see parse_file/2 %% %% Returns : void() %%------------------------------------------------------------------------------ %% function clauses clauses([{clause, _L, _P, _G, B} | Clauses], Opts) -> %% Not needed to check the head or the guards? %% Vars = sequence(P, [], Opts), % check parameters (head) sequence(B, [], Opts), % check body clauses(Clauses, Opts); clauses([], _Opts) -> void. %%------------------------------------------------------------------------------ %% Func : sequence/3 %% %% Purpose : Parse a sequence of expressions %% %% Args : Sequence : List() %% [Expression, ...] %% Vars : List() - [{Var1, Seq1}, ..., {VarN, SeqN}] %% Opts : see parse_file/2 %% %% Returns : Vars %%------------------------------------------------------------------------------ sequence([Expressions | Rest], Vars, Opts) when list(Expressions) -> NewVars = sequence(Expressions, Vars, Opts), sequence(Rest, NewVars, Opts); sequence([Expression | Rest], Vars, Opts) -> NewVars = expression(Expression, Vars, Opts), sequence(Rest, NewVars, Opts); sequence([], Vars, _Opts) -> Vars. %%------------------------------------------------------------------------------ %% Func : fric_clauses/3 %% %% Purpose : Parse a list of fun/receive/if/case/try clauses. %% %% Args : Clauses : List() %% [{clause, Line, Parameters, Guards, Body},...] %% Vars : see sequence/3 %% Opts : see parse_file/2 %% %% Returns : Vars %%------------------------------------------------------------------------------ fric_clauses(Clauses, Vars, Opts) -> fric_clauses(Clauses, Vars, Opts, []). fric_clauses([{clause, _L, P, G, B} | Clauses], Vars, Opts, AllVars) -> NewVars1 = sequence(P, Vars, Opts), % check parameters guards(G, NewVars1, Opts), % check guards NewVars2 = sequence(B, NewVars1, Opts), % check body fric_clauses(Clauses, Vars, Opts, [NewVars2| AllVars]); fric_clauses([], _Vars, _Opts, AllVars) -> comm(AllVars). % get vars common to all clauses %%------------------------------------------------------------------------------ %% Func : guards/3 %% %% Purpose : Parse a list of guard expressions %% %% Args : Guards : List() %% [Expression, ...] %% Vars : see sequence/3 %% Opts : see parse_file/2 %% %% Returns : Vars %%------------------------------------------------------------------------------ guards([Expressions | Rest], Vars, Opts) when list(Expressions) -> guards(Expressions, Vars, Opts), guards(Rest, Vars, Opts); guards([Expression | Rest], Vars, Opts) -> expression(Expression, Vars, Opts), guards(Rest, Vars, Opts); guards([], Vars, _Opts) -> Vars. %%------------------------------------------------------------------------------ %% Func : expression/3 %% %% Purpose : Parse an expression %% %% Args : Expression : Tuple() %% Vars : see sequence/3 %% Opts : see parse_file/2 %% %% Returns : Vars %%------------------------------------------------------------------------------ expression({call, _Line, Call, Parameters}, Vars, Opts) -> NewVars = expression(Call, Vars, Opts), sequence(Parameters, NewVars, Opts); expression({cons, _Line, Head, Tail}, Vars, Opts) -> NewVars = expression(Head, Vars, Opts), expression(Tail, NewVars, Opts); expression({tuple, _Line, Expressions}, Vars, Opts) -> sequence(Expressions, Vars, Opts); expression({'if', _Line, Clauses}, Vars, Opts) -> fric_clauses(Clauses, Vars, Opts); expression({'case', _Line, Expression, Clauses}, Vars, Opts) -> NewVars = expression(Expression, Vars, Opts), fric_clauses(Clauses, NewVars, Opts); expression({'receive', _Line, Clauses}, Vars, Opts) -> fric_clauses(Clauses, Vars, Opts); expression({'receive', _Line, Clauses, T1, T2}, Vars, Opts) -> Vars1 = expression(T1, Vars, Opts), Vars2 = sequence(T2, Vars1, Opts), fric_clauses(Clauses, Vars2, Opts); expression({arith, _, _Operator, Left, Right}, Vars, Opts) -> Vars1 = expression(Left, Vars, Opts), expression(Right, Vars1, Opts); expression({arith, _, _Operator, Expression}, Vars, Opts) -> expression(Expression, Vars, Opts); expression({match, _, Left, Right}, Vars, Opts) -> Vars1 = expression(Right, Vars, Opts), expression(Left, Vars1, Opts); % will this work? expression({block, _, Expression}, Vars, Opts) -> expression(Expression, Vars, Opts); expression({'catch', _, Expression}, Vars, Opts) -> expression(Expression, Vars, Opts); expression({send, _, Receiver, Message}, Vars, Opts) -> Vars1 = expression(Receiver, Vars, Opts), expression(Message, Vars1, Opts); %% This is where the actual checking starts! expression({var, L, Var}, Vars, Opts) -> parameter(Var, L, Vars, Opts); expression({record, _, _, Record}, Vars, Opts) when list(Record) -> sequence(Record, Vars, Opts); expression({record, _, Expression, _, Record}, Vars, Opts) when list(Record) -> Vars1 = expression(Expression, Vars, Opts), sequence(Record, Vars1, Opts); expression({record_field, _L, _, Expression}, Vars, Opts) -> expression(Expression, Vars, Opts); expression({record_field, _L, Var, _F, Expression}, Vars, Opts) -> Vars1 = expression(Var, Vars, Opts), expression(Expression, Vars1, Opts); expression({record_index, _L, _, _}, Vars, _Opts) -> Vars; expression({op, _L, _Op, Expr1, Expr2}, Vars, Opts) -> Vars1 = expression(Expr1, Vars, Opts), expression(Expr2, Vars1, Opts); expression({op, _L, _Op, Expr}, Vars, Opts) -> expression(Expr, Vars, Opts); expression({lc, _, Expr1, Expr2}, Vars, Opts) -> Vars1 = expression(Expr1, Vars, Opts), sequence(Expr2, Vars1, Opts); expression({generate, _, Expr1, Expr2}, Vars, Opts) -> Vars1 = expression(Expr1, Vars, Opts), expression(Expr2, Vars1, Opts); expression({remote, _, Expr1, Expr2}, Vars, Opts) -> Vars1 = expression(Expr1, Vars, Opts), expression(Expr2, Vars1, Opts); expression({'fun', _, {clauses, Clauses}}, Vars, Opts) -> fric_clauses(Clauses, Vars, Opts); expression({'fun', _L, _}, Vars, _Opts) -> Vars; expression({'query', _, Expr}, Vars, Opts) -> expression(Expr, Vars, Opts); expression({atom, _L, _Atom}, Vars, _Opts) -> Vars; expression({integer, _L, _Int}, Vars, _Opts) -> Vars; expression({float, _L, _Float}, Vars, _Opts) -> Vars; expression({string, _L, _String}, Vars, _Opts) -> Vars; expression({char, _L, _Char}, Vars, _Opts) -> Vars; expression({bin, _L, _Bin}, Vars, _Opts) -> Vars; expression({nil, _L}, Vars, _Opts) -> Vars; expression({'try', _L, Call, Clauses, Exceptions, After}, Vars, Opts) -> Vars1 = sequence(Call, Vars, Opts), Vars2 = fric_clauses(Clauses, Vars1, Opts), Vars3 = fric_clauses(Exceptions, Vars1, Opts), Vars4 = comm([Vars2, Vars3]), sequence(After, Vars4, Opts), Vars4; expression(BeginEnd, Vars, Opts) when is_list(BeginEnd) -> sequence(BeginEnd, Vars, Opts); expression(Expr, Vars, Opts) -> case Opts#opts.verbose of true when is_tuple(Expr) -> io:format("Line ~w: unhandled expression ~p~n", [element(2, Expr), Expr]); true -> io:format("unhandled expression ~p~n", [Expr]); _ -> ok end, Vars. %%------------------------------------------------------------------------------ %% Func : parameter/4 %% %% Purpose : Check a variable and report if it's used out of sequence %% %% Args : Var : Atom() %% The variable %% L : Integer() %% Line number %% Vars : see sequence/3 %% Opts : see parse_file/2 %% %% Returns : Vars %%------------------------------------------------------------------------------ parameter(Var, L, Vars, Opts) -> case include(Var, {Opts#opts.inclvar, Opts#opts.exclvar}) of true -> {V, NewS} = parse_var(Var), case lists:keysearch(V, 1, Vars) of {value, {_, NewS}} -> Vars; {value, {_, -1}} -> lists:keyreplace(V, 1, Vars, {V, NewS}); {value, {_, OldS}} when NewS > OldS -> lists:keyreplace(V, 1, Vars, {V, NewS}); {value, {_, OldS}} -> % using old var io:format("Line ~p: Usage of ~p, expected ~p~n", [L, frmt_var({V, NewS}), frmt_var({V, OldS})]), Vars; false -> [{V, NewS}| Vars] end; false -> Vars end. parse_var(VarIn) -> parse_var(lists:reverse(atom_to_list(VarIn)), []). parse_var([H| T], Seq) when H >= 48, H =< 57 -> parse_var(T, [H| Seq]); parse_var(RevVar, []) -> {lists:reverse(RevVar), -1}; parse_var([], Seq) -> {Seq, -1}; parse_var(RevVar, Seq) -> {lists:reverse(RevVar), list_to_integer(Seq)}. frmt_var({Var, -1}) -> list_to_atom(Var); frmt_var({Var, Seq}) -> list_to_atom(lists:concat([Var, Seq])). %%------------------------------------------------------------------------------ %% Utility functions %%------------------------------------------------------------------------------ %% Convert paths which ends in ebin to src ebin_to_src(PathList) -> F = fun(P) -> case filename:basename(P) of "ebin" -> filename:join(filename:dirname(P), "src"); _ -> P end end, lists:map(F, PathList). %%% get_files(DoFs, Opts) where DoFs is a list of directories and files %%% or a directory or file. get_files will list all files in the specified %%% directories and also traverse sub directories if the recurse option is set. %%% Include only .erl files for which Incl(File) returns true %%% and Excl(File) returns false. If the recurse option is set %%% get_files will traverse all sub directories. get_files([DoF| Rest], Opts) when list(DoF); atom(DoF) -> get_files(DoF, Opts) ++ get_files(Rest, Opts); get_files(InDoF, Opts) -> DoF = coerce2list(InDoF), #opts{incl = Incl, excl = Excl, incldir = InclDir, excldir = ExclDir, recurse = Recurse} = Opts, FileFuns = {Incl, Excl}, DirFuns = {InclDir, ExclDir}, case type(DoF) of file -> case include(DoF, FileFuns) of true -> [DoF]; false -> [] end; dir -> case include(DoF, DirFuns) of true -> list_dir(DoF, {FileFuns, DirFuns}, Recurse, []); false -> [] end; _Other -> [] end. list_dir(Dir, IEFuns, Recurse, AllFiles) -> {Dirs, Files} = list_dir(Dir, IEFuns), case Recurse of true -> list_dirs(Dirs, IEFuns, Recurse, AllFiles ++ Files); false -> AllFiles ++ Files end. list_dirs([Dir | Dirs], IEFuns, Recurse, AllFiles) -> NewAllFiles = list_dir(Dir, IEFuns, Recurse, AllFiles), list_dirs(Dirs, IEFuns, Recurse, NewAllFiles); list_dirs([], _IEFuns, _Recurse, AllFiles) -> AllFiles. list_dir(Dir, IEFuns) -> {ok, Content} = file:list_dir(Dir), extract(Content, Dir, IEFuns, [], []). extract([DoF | Content], Dir, Funs, Files, Dirs) -> case include(DoF, Dir, Funs) of {dir, PathDoF} -> extract(Content, Dir, Funs, Files, [PathDoF| Dirs]); {file, PathDoF} -> extract(Content, Dir, Funs, [PathDoF| Files], Dirs); false -> extract(Content, Dir, Funs, Files, Dirs) end; extract([], _Dir, _Funs, Files, Dirs) -> {lists:reverse(Dirs), lists:reverse(Files)}. include(DoF, Dir, {FileFuns, DirFuns}) -> PathDoF = filename:join(Dir, DoF), case type(PathDoF) of file -> case filename:extension(DoF) of ".erl" -> case include(PathDoF, FileFuns) of true -> {file, PathDoF}; false -> false end; _SomethingElse -> false end; dir -> case include(PathDoF, DirFuns) of true -> {dir, PathDoF}; false -> false end; _Other -> false end. type(#file_info{type=directory}) -> dir; type(#file_info{type=regular}) -> file; type(#file_info{type=Type}) -> Type; type(DoF) when list(DoF); atom(DoF) -> case file:read_file_info(DoF) of {ok, FileInfo} -> type(FileInfo); Error -> Error end; type(Error) -> {error, Error}. coerce2list(X) when atom(X) -> atom_to_list(X); coerce2list(X) -> X. %%% fix_opts will scan a list of options and set the appropriate %%% fields in the opts record which will be used later in the %%% code. Options appearing before others in the list has precedence fix_opts(Options) when record(Options, opts) -> Options; fix_opts(Options) -> fix_opts(lists:reverse(Options), #opts{}). fix_opts([Option| Options], Opts) -> NewOpts = case Option of rec -> Opts#opts{recurse = true}; recurse -> Opts#opts{recurse = true}; recursive -> Opts#opts{recurse = true}; {incl, Fun} -> Opts#opts{incl = Fun}; {excl, Fun} -> Opts#opts{excl = Fun}; {incldir, Fun} -> Opts#opts{incldir = Fun}; {excldir, Fun} -> Opts#opts{excldir = Fun}; {inclvar, Fun} -> Opts#opts{inclvar = Fun}; {exclvar, Fun} -> Opts#opts{exclvar = Fun}; verbose -> Opts#opts{verbose = true}; Option -> Opts end, fix_opts(Options, NewOpts); fix_opts([], Opts) -> Opts. %% default excludes %% exclude(AbsDir) -> %% Dir = filename:basename(AbsDir), %% case Dir of %% "lost+found" -> %% true; %% Dir -> %% case string:to_upper(lists:reverse(Dir)) of %% "KAB." ++ _ -> % ends with .BAK %% true; %% "DLO." ++ _ -> % ends with .OLD %% true; %% _Other -> %% false %% end %% end. %% include or exclude something include(_Cand, {nil, nil}) -> true; include(Cand, {Incl, nil}) -> Incl(Cand); include(Cand, {nil, Excl}) -> not(Excl(Cand)); include(Cand, {Incl, Excl}) -> Incl(Cand) and not(Excl(Cand)). %% comm returns the intersection of a list of lists comm([L1, L2| Lists]) -> Comm = common(lists:sort(L1), lists:sort(L2)), comm([lists:sort(L) || L <- Lists], Comm); comm(Lists) -> Lists. comm([L1| Lists], Comm) -> comm(Lists, common(L1, Comm)); comm([], Comm) -> Comm. %% assumes sorted lists common(L1, L2) -> common(L1, L2, []). common([E| L1], [E| L2], Res) -> common(L1, L2, [E| Res]); common([E1| L1], [E2| L2], Res) when E1 < E2 -> common(L1, [E2| L2], Res); common([E1| L1], [_E2| L2], Res) -> common([E1| L1] , L2, Res); common(_, _, Res) -> lists:reverse(Res).