autolink.ex (5860B)
1 defmodule ExDoc.Autolink do 2 @moduledoc false 3 4 # * `:apps` - the apps that the docs are being generated for. When linking modules they are 5 # checked if they are part of the app and based on that the links are relative or absolute. 6 # 7 # * `:current_module` - the module that the docs are being generated for. Used to link local 8 # calls and see if remote calls are in the same app. 9 # 10 # * `:module_id` - id of the module being documented (e.g.: `"String"`) 11 # 12 # * `:file` - source file location 13 # 14 # * `:line` - line number of the beginning of the documentation 15 # 16 # * `:id` - a module/function/etc being documented (e.g.: `"String.upcase/2"`) 17 # 18 # * `:ext` - the extension (`".html"`, "`.xhtml"`, etc) 19 # 20 # * `:extras` - map of extras 21 # 22 # * `:skip_undefined_reference_warnings_on` - list of modules to skip the warning on 23 24 defstruct [ 25 :current_module, 26 :module_id, 27 :id, 28 :line, 29 file: "nofile", 30 apps: [], 31 extras: [], 32 deps: [], 33 ext: ".html", 34 siblings: [], 35 skip_undefined_reference_warnings_on: [] 36 ] 37 38 @hexdocs "https://hexdocs.pm/" 39 @otpdocs "https://www.erlang.org/doc/man/" 40 41 def app_module_url(:ex_doc, module, %{current_module: module} = config) do 42 path = module |> inspect() |> String.trim_leading(":") 43 ex_doc_app_url(module, config, path, config.ext, "#content") 44 end 45 46 def app_module_url(:ex_doc, module, config) do 47 path = module |> inspect() |> String.trim_leading(":") 48 ex_doc_app_url(module, config, path, config.ext, "") 49 end 50 51 def app_module_url(:otp, module, _config) do 52 @otpdocs <> "#{module}.html" 53 end 54 55 def app_module_url(:no_tool, _, _) do 56 nil 57 end 58 59 # TODO: make more generic 60 @doc false 61 def ex_doc_app_url(module, config, path, ext, suffix) do 62 if app = app(module) do 63 if app in config.apps do 64 path <> ext <> suffix 65 else 66 config.deps 67 |> Keyword.get_lazy(app, fn -> @hexdocs <> "#{app}" end) 68 |> String.trim_trailing("/") 69 |> Kernel.<>("/" <> path <> ".html" <> suffix) 70 end 71 else 72 path <> ext <> suffix 73 end 74 end 75 76 defp app(module) do 77 {_, app} = app_info(module) 78 app 79 end 80 81 @doc false 82 def tool(module, config) do 83 if match?("Elixir." <> _, Atom.to_string(module)) do 84 :ex_doc 85 else 86 {otp, app} = app_info(module) 87 apps = Enum.uniq(config.apps ++ Keyword.keys(config.deps)) 88 89 if otp == true and app not in apps do 90 :otp 91 else 92 :ex_doc 93 end 94 end 95 end 96 97 defp app_info(module) do 98 case :code.which(module) do 99 :preloaded -> 100 {true, :erts} 101 102 maybe_path -> 103 otp? = is_list(maybe_path) and List.starts_with?(maybe_path, :code.lib_dir()) 104 105 app = 106 case :application.get_application(module) do 107 {:ok, app} -> 108 app 109 110 _ -> 111 with true <- is_list(maybe_path), 112 [_, "ebin", app, "lib" | _] <- maybe_path |> Path.split() |> Enum.reverse() do 113 String.to_atom(app) 114 else 115 _ -> nil 116 end 117 end 118 119 {otp?, app} 120 end 121 end 122 123 def maybe_warn(ref, config, visibility, metadata) do 124 skipped = config.skip_undefined_reference_warnings_on 125 file = Path.relative_to(config.file, File.cwd!()) 126 line = config.line 127 128 unless Enum.any?([config.id, config.module_id, file], &(&1 in skipped)) do 129 warn(ref, {file, line}, config.id, visibility, metadata) 130 end 131 end 132 133 defp warn(message, {file, line}, id) do 134 warning = IO.ANSI.format([:yellow, "warning: ", :reset]) 135 136 stacktrace = 137 " #{file}" <> 138 if(line, do: ":#{line}", else: "") <> 139 if(id, do: ": #{id}", else: "") 140 141 IO.puts(:stderr, [warning, message, ?\n, stacktrace, ?\n]) 142 end 143 144 defp warn(ref, file_line, id, visibility, metadata) 145 146 defp warn( 147 {:module, _module}, 148 {file, line}, 149 id, 150 visibility, 151 %{mix_task: true, original_text: original_text} 152 ) do 153 message = 154 "documentation references \"#{original_text}\" but it is " <> 155 format_visibility(visibility, :module) 156 157 warn(message, {file, line}, id) 158 end 159 160 defp warn( 161 {:module, _module}, 162 {file, line}, 163 id, 164 visibility, 165 %{original_text: original_text} 166 ) do 167 message = 168 "documentation references module \"#{original_text}\" but it is " <> 169 format_visibility(visibility, :module) 170 171 warn(message, {file, line}, id) 172 end 173 174 defp warn( 175 nil, 176 {file, line}, 177 id, 178 _visibility, 179 %{file_path: _file_path, original_text: original_text} 180 ) do 181 message = "documentation references file \"#{original_text}\" but it does not exist" 182 183 warn(message, {file, line}, id) 184 end 185 186 defp warn( 187 {kind, _module, _name, _arity}, 188 {file, line}, 189 id, 190 visibility, 191 %{original_text: original_text} 192 ) do 193 message = 194 "documentation references #{kind} \"#{original_text}\" but it is " <> 195 format_visibility(visibility, kind) 196 197 warn(message, {file, line}, id) 198 end 199 200 defp warn(message, {file, line}, id, _, _) when is_binary(message) do 201 warn(message, {file, line}, id) 202 end 203 204 # there is not such a thing as private callback or private module 205 defp format_visibility(visibility, kind) when kind in [:module, :callback], do: "#{visibility}" 206 207 # typep is defined as :hidden, since there is no :private visibility value 208 # but type defined with @doc false also is the stored the same way. 209 defp format_visibility(:hidden, :type), do: "hidden or private" 210 211 # for the rest, it can either be undefined or private 212 defp format_visibility(:undefined, _kind), do: "undefined or private" 213 defp format_visibility(visibility, _kind), do: "#{visibility}" 214 end