module_doc.ex (3838B)
1 defmodule Credo.Check.Readability.ModuleDoc do 2 use Credo.Check, 3 param_defaults: [ 4 ignore_names: [ 5 ~r/(\.\w+Controller|\.Endpoint|\.\w+Live(\.\w+)?|\.Repo|\.Router|\.\w+Socket|\.\w+View)$/ 6 ] 7 ], 8 explanations: [ 9 check: """ 10 Every module should contain comprehensive documentation. 11 12 # preferred 13 14 defmodule MyApp.Web.Search do 15 @moduledoc \"\"\" 16 This module provides a public API for all search queries originating 17 in the web layer. 18 \"\"\" 19 end 20 21 # also okay: explicitly say there is no documentation 22 23 defmodule MyApp.Web.Search do 24 @moduledoc false 25 end 26 27 Many times a sentence or two in plain english, explaining why the module 28 exists, will suffice. Documenting your train of thought this way will help 29 both your co-workers and your future-self. 30 31 Other times you will want to elaborate even further and show some 32 examples of how the module's functions can and should be used. 33 34 In some cases however, you might not want to document things about a module, 35 e.g. it is part of a private API inside your project. Since Elixir prefers 36 explicitness over implicit behaviour, you should "tag" these modules with 37 38 @moduledoc false 39 40 to make it clear that there is no intention in documenting it. 41 """, 42 params: [ 43 ignore_names: "All modules matching this regex (or list of regexes) will be ignored." 44 ] 45 ] 46 47 alias Credo.Code.Module 48 49 @doc false 50 def run(%SourceFile{filename: filename} = source_file, params \\ []) do 51 if Path.extname(filename) == ".exs" do 52 [] 53 else 54 issue_meta = IssueMeta.for(source_file, params) 55 ignore_names = Params.get(params, :ignore_names, __MODULE__) 56 57 {_continue, issues} = 58 Credo.Code.prewalk( 59 source_file, 60 &traverse(&1, &2, issue_meta, ignore_names), 61 {true, []} 62 ) 63 64 issues 65 end 66 end 67 68 defp traverse( 69 {:defmodule, meta, _arguments} = ast, 70 {true, issues}, 71 issue_meta, 72 ignore_names 73 ) do 74 mod_name = Module.name(ast) 75 76 if matches_any?(mod_name, ignore_names) do 77 {ast, {false, issues}} 78 else 79 exception? = Module.exception?(ast) 80 81 case Module.attribute(ast, :moduledoc) do 82 {:error, _} when not exception? -> 83 { 84 ast, 85 {true, 86 [ 87 issue_for( 88 "Modules should have a @moduledoc tag.", 89 issue_meta, 90 meta[:line], 91 mod_name 92 ) 93 ] ++ issues} 94 } 95 96 string when is_binary(string) -> 97 if String.trim(string) == "" do 98 { 99 ast, 100 {true, 101 [ 102 issue_for( 103 "Use `@moduledoc false` if a module will not be documented.", 104 issue_meta, 105 meta[:line], 106 mod_name 107 ) 108 ] ++ issues} 109 } 110 else 111 {ast, {true, issues}} 112 end 113 114 _ -> 115 {ast, {true, issues}} 116 end 117 end 118 end 119 120 defp traverse(ast, {continue, issues}, _issue_meta, _ignore_names) do 121 {ast, {continue, issues}} 122 end 123 124 defp matches_any?(name, list) when is_list(list) do 125 Enum.any?(list, &matches_any?(name, &1)) 126 end 127 128 defp matches_any?(name, string) when is_binary(string) do 129 String.contains?(name, string) 130 end 131 132 defp matches_any?(name, regex) do 133 String.match?(name, regex) 134 end 135 136 defp issue_for(message, issue_meta, line_no, trigger) do 137 format_issue( 138 issue_meta, 139 message: message, 140 trigger: trigger, 141 line_no: line_no 142 ) 143 end 144 end