spec_with_struct.ex (1619B)
1 defmodule Credo.Check.Warning.SpecWithStruct do 2 use Credo.Check, 3 base_priority: :normal, 4 category: :warning, 5 explanations: [ 6 check: """ 7 Structs create compile-time dependencies between modules. Using a struct in a spec 8 will cause the module to be recompiled whenever the struct's module changes. 9 10 It is preferable to define and use `MyModule.t()` instead of `%MyModule{}` in specs. 11 12 Example: 13 14 # preferred 15 @spec a_function(MyModule.t()) :: any 16 17 # NOT preferred 18 @spec a_function(%MyModule{}) :: any 19 """ 20 ] 21 22 alias Credo.Code.Name 23 24 @doc false 25 @impl true 26 def run(%SourceFile{} = source_file, params) do 27 issue_meta = IssueMeta.for(source_file, params) 28 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 29 end 30 31 defp traverse({:@, meta, [{:spec, _, args}]}, issues, issue_meta) do 32 case Macro.prewalk(args, [], &find_structs/2) do 33 {ast, []} -> 34 {ast, issues} 35 36 {ast, structs} -> 37 issues = 38 Enum.reduce(structs, issues, fn curr, acc -> 39 options = [ 40 message: "Struct %#{curr}{} found in @spec", 41 trigger: "%#{curr}{}", 42 line_no: meta[:line] 43 ] 44 45 [format_issue(issue_meta, options) | acc] 46 end) 47 48 {ast, issues} 49 end 50 end 51 52 defp traverse(ast, issues, _issue_meta) do 53 {ast, issues} 54 end 55 56 defp find_structs({:%, _, [{:__aliases__, _, _} = aliases | _]} = ast, acc) do 57 {ast, [Name.full(aliases) | acc]} 58 end 59 60 defp find_structs(ast, acc) do 61 {ast, acc} 62 end 63 end