specs.ex (3652B)
1 defmodule Credo.Check.Readability.Specs do 2 use Credo.Check, 3 tags: [:controversial], 4 param_defaults: [ 5 include_defp: false 6 ], 7 explanations: [ 8 check: """ 9 Functions, callbacks and macros need typespecs. 10 11 Adding typespecs gives tools like Dialyzer more information when performing 12 checks for type errors in function calls and definitions. 13 14 @spec add(integer, integer) :: integer 15 def add(a, b), do: a + b 16 17 Functions with multiple arities need to have a spec defined for each arity: 18 19 @spec foo(integer) :: boolean 20 @spec foo(integer, integer) :: boolean 21 def foo(a), do: a > 0 22 def foo(a, b), do: a > b 23 24 The check only considers whether the specification is present, it doesn't 25 perform any actual type checking. 26 27 Like all `Readability` issues, this one is not a technical concern. 28 But you can improve the odds of others reading and liking your code by making 29 it easier to follow. 30 """, 31 params: [ 32 include_defp: "Include private functions." 33 ] 34 ] 35 36 @doc false 37 @impl true 38 def run(%SourceFile{} = source_file, params) do 39 issue_meta = IssueMeta.for(source_file, params) 40 specs = Credo.Code.prewalk(source_file, &find_specs(&1, &2)) 41 42 Credo.Code.prewalk(source_file, &traverse(&1, &2, specs, issue_meta)) 43 end 44 45 defp find_specs( 46 {:spec, _, [{:when, _, [{:"::", _, [{name, _, args}, _]}, _]} | _]} = ast, 47 specs 48 ) do 49 {ast, [{name, length(args)} | specs]} 50 end 51 52 defp find_specs({:spec, _, [{_, _, [{name, _, args} | _]}]} = ast, specs) 53 when is_list(args) or is_nil(args) do 54 args = with nil <- args, do: [] 55 {ast, [{name, length(args)} | specs]} 56 end 57 58 defp find_specs({:impl, _, [impl]} = ast, specs) when impl != false do 59 {ast, [:impl | specs]} 60 end 61 62 defp find_specs({keyword, meta, [{:when, _, def_ast} | _]}, [:impl | specs]) 63 when keyword in [:def, :defp] do 64 find_specs({keyword, meta, def_ast}, [:impl | specs]) 65 end 66 67 defp find_specs({keyword, _, [{name, _, nil}, _]} = ast, [:impl | specs]) 68 when keyword in [:def, :defp] do 69 {ast, [{name, 0} | specs]} 70 end 71 72 defp find_specs({keyword, _, [{name, _, args}, _]} = ast, [:impl | specs]) 73 when keyword in [:def, :defp] do 74 {ast, [{name, length(args)} | specs]} 75 end 76 77 defp find_specs(ast, issues) do 78 {ast, issues} 79 end 80 81 # TODO: consider for experimental check front-loader (ast) 82 defp traverse( 83 {keyword, meta, [{:when, _, def_ast} | _]}, 84 issues, 85 specs, 86 issue_meta 87 ) 88 when keyword in [:def, :defp] do 89 traverse({keyword, meta, def_ast}, issues, specs, issue_meta) 90 end 91 92 defp traverse( 93 {keyword, meta, [{name, _, args} | _]} = ast, 94 issues, 95 specs, 96 issue_meta 97 ) 98 when is_list(args) or is_nil(args) do 99 args = with nil <- args, do: [] 100 101 if keyword not in enabled_keywords(issue_meta) or {name, length(args)} in specs do 102 {ast, issues} 103 else 104 {ast, [issue_for(issue_meta, meta[:line], name) | issues]} 105 end 106 end 107 108 defp traverse(ast, issues, _specs, _issue_meta) do 109 {ast, issues} 110 end 111 112 defp issue_for(issue_meta, line_no, trigger) do 113 format_issue( 114 issue_meta, 115 message: "Functions should have a @spec type specification.", 116 trigger: trigger, 117 line_no: line_no 118 ) 119 end 120 121 defp enabled_keywords(issue_meta) do 122 issue_meta 123 |> IssueMeta.params() 124 |> Params.get(:include_defp, __MODULE__) 125 |> case do 126 true -> [:def, :defp] 127 _ -> [:def] 128 end 129 end 130 end