elixir.ex (25001B)
1 defmodule ExDoc.Language.Elixir do 2 @moduledoc false 3 4 @behaviour ExDoc.Language 5 6 alias ExDoc.Autolink 7 alias ExDoc.Refs 8 alias ExDoc.Language.Erlang 9 10 @impl true 11 def module_data(module, docs_chunk, config) do 12 {type, skip} = module_type_and_skip(module) 13 14 if skip do 15 :skip 16 else 17 title = module_title(module, type) 18 abst_code = Erlang.get_abstract_code(module) 19 line = Erlang.find_module_line(module, abst_code) 20 optional_callbacks = type == :behaviour && module.behaviour_info(:optional_callbacks) 21 22 %{ 23 module: module, 24 docs: docs_chunk, 25 language: __MODULE__, 26 id: inspect(module), 27 title: title, 28 type: type, 29 line: line, 30 callback_types: [:callback, :macrocallback], 31 nesting_info: nesting_info(title, config.nest_modules_by_prefix), 32 private: %{ 33 abst_code: abst_code, 34 specs: Erlang.get_specs(module), 35 callbacks: Erlang.get_callbacks(module), 36 impls: get_impls(module), 37 optional_callbacks: optional_callbacks 38 } 39 } 40 end 41 end 42 43 @impl true 44 def function_data(entry, module_data) do 45 {{kind, name, arity}, _anno, _signature, doc_content, metadata} = entry 46 47 if doc?(entry, module_data.type) do 48 function_data(kind, name, arity, doc_content, metadata, module_data) 49 else 50 :skip 51 end 52 end 53 54 def function_data(kind, name, arity, _doc_content, metadata, module_data) do 55 extra_annotations = 56 case {kind, name, arity} do 57 {:macro, _, _} -> ["macro"] 58 {_, :__struct__, 0} -> ["struct"] 59 _ -> [] 60 end 61 62 actual_def = actual_def(name, arity, kind) 63 64 %{ 65 doc_fallback: fn -> 66 impl = Map.fetch(module_data.private.impls, actual_def) 67 68 callback_doc_ast(name, arity, impl) || 69 delegate_doc_ast(metadata[:delegate_to]) 70 end, 71 extra_annotations: extra_annotations, 72 line: find_function_line(module_data, actual_def), 73 specs: specs(kind, name, actual_def, module_data) 74 } 75 end 76 77 # We are only interested in functions and macros for now 78 defp doc?({{kind, _, _}, _, _, _, _}, _) when kind not in [:function, :macro] do 79 false 80 end 81 82 # Skip impl_for and impl_for! for protocols 83 defp doc?({{_, name, _}, _, _, _, _}, :protocol) when name in [:impl_for, :impl_for!] do 84 false 85 end 86 87 # If content is a map, then it is ok. 88 defp doc?({_, _, _, %{}, _}, _) do 89 true 90 end 91 92 # If it is none, then we need to look at underscore. 93 # TODO: We can remove this on Elixir v1.13 as all underscored are hidden. 94 defp doc?({{_, name, _}, _, _, :none, _}, _type) do 95 hd(Atom.to_charlist(name)) != ?_ 96 end 97 98 # Everything else is hidden. 99 defp doc?({_, _, _, _, _}, _) do 100 false 101 end 102 103 @impl true 104 def callback_data(entry, module_data) do 105 {{kind, name, arity}, anno, _signature, _doc, _metadata} = entry 106 actual_def = actual_def(name, arity, kind) 107 108 extra_annotations = 109 if actual_def in module_data.private.optional_callbacks, do: ["optional"], else: [] 110 111 specs = 112 case module_data.private.callbacks do 113 %{^actual_def => specs} when kind == :macrocallback -> 114 Enum.map(specs, &remove_callback_term/1) 115 116 %{^actual_def => specs} -> 117 specs 118 119 %{} -> 120 [] 121 end 122 123 line = 124 if specs != [] do 125 {:type, anno, _, _} = hd(specs) 126 anno_line(anno) 127 else 128 anno_line(anno) 129 end 130 131 quoted = Enum.map(specs, &Code.Typespec.spec_to_quoted(name, &1)) 132 signature = [get_typespec_signature(hd(quoted), arity)] 133 134 %{ 135 line: line, 136 signature: signature, 137 specs: quoted, 138 extra_annotations: extra_annotations 139 } 140 end 141 142 defp remove_callback_term({:type, num, :bounded_fun, [lhs, rhs]}) do 143 {:type, num, :bounded_fun, [remove_callback_term(lhs), rhs]} 144 end 145 146 defp remove_callback_term({:type, num, :fun, [{:type, num, :product, [_ | rest_args]} | rest]}) do 147 {:type, num, :fun, [{:type, num, :product, rest_args} | rest]} 148 end 149 150 @impl true 151 def type_data(entry, module_data) do 152 {{_kind, name, arity}, _anno, _signature, _doc, _metadata} = entry 153 154 %{type: type, spec: spec, line: line} = type_from_module_data(module_data, name, arity) 155 quoted = spec |> Code.Typespec.type_to_quoted() |> process_type_ast(type) 156 signature = [get_typespec_signature(quoted, arity)] 157 158 %{ 159 type: type, 160 line: line, 161 spec: quoted, 162 signature: signature 163 } 164 end 165 166 @doc false 167 def type_from_module_data(module_data, name, arity) do 168 Enum.find_value(module_data.private.abst_code, fn 169 {:attribute, anno, type, {^name, _, args} = spec} -> 170 if type in [:opaque, :type] and length(args) == arity do 171 %{ 172 type: type, 173 spec: spec, 174 line: anno_line(anno) 175 } 176 end 177 178 _ -> 179 nil 180 end) 181 end 182 183 @impl true 184 def autolink_doc(ast, opts) do 185 config = struct!(Autolink, opts) 186 walk_doc(ast, config) 187 end 188 189 @impl true 190 def autolink_spec(ast, opts) do 191 config = struct!(Autolink, opts) 192 193 # TODO: re-use ExDoc.Language.Erlang.autolink_spec/2 194 195 string = 196 ast 197 |> Macro.to_string() 198 |> safe_format_string!() 199 |> ExDoc.Utils.h() 200 201 name = typespec_name(ast) 202 {name, rest} = split_name(string, name) 203 204 name <> do_typespec(rest, config) 205 end 206 207 @impl true 208 def highlight_info() do 209 %{ 210 language_name: "elixir", 211 lexer: Makeup.Lexers.ElixirLexer, 212 opts: [] 213 } 214 end 215 216 @impl true 217 def format_spec_attribute(%ExDoc.TypeNode{type: type}), do: "@#{type}" 218 def format_spec_attribute(%ExDoc.FunctionNode{type: :callback}), do: "@callback" 219 def format_spec_attribute(%ExDoc.FunctionNode{type: :macrocallback}), do: "@macrocallback" 220 def format_spec_attribute(%ExDoc.FunctionNode{}), do: "@spec" 221 222 ## Module Helpers 223 224 defp nesting_info(title, prefixes) do 225 prefixes 226 |> Enum.find(&String.starts_with?(title, &1 <> ".")) 227 |> case do 228 nil -> nil 229 prefix -> {"." <> String.trim_leading(title, prefix <> "."), prefix} 230 end 231 end 232 233 defp module_type_and_skip(module) do 234 cond do 235 function_exported?(module, :__struct__, 0) and 236 match?(%{__exception__: true}, module.__struct__) -> 237 {:exception, false} 238 239 function_exported?(module, :__protocol__, 1) -> 240 {:protocol, false} 241 242 function_exported?(module, :__impl__, 1) -> 243 {:impl, true} 244 245 match?("Elixir.Mix.Tasks." <> _, Atom.to_string(module)) -> 246 {:task, false} 247 248 function_exported?(module, :behaviour_info, 1) -> 249 {:behaviour, false} 250 251 true -> 252 {:module, false} 253 end 254 end 255 256 defp module_title(module, :task), do: "mix " <> task_name(module) 257 defp module_title(module, _), do: inspect(module) 258 259 defp task_name(module) do 260 "Elixir.Mix.Tasks." <> name = Atom.to_string(module) 261 name |> String.split(".") |> Enum.map_join(".", &Macro.underscore/1) 262 end 263 264 def get_impls(module) do 265 for behaviour <- behaviours_implemented_by(module), 266 {callback, _} <- Erlang.get_callbacks(behaviour), 267 do: {callback, behaviour}, 268 into: %{} 269 end 270 271 defp behaviours_implemented_by(module) do 272 for {:behaviour, list} <- module.module_info(:attributes), 273 behaviour <- list, 274 do: behaviour 275 end 276 277 ## Helpers 278 279 defp specs(kind, name, actual_def, module_data) do 280 specs = 281 module_data.private.specs 282 |> Map.get(actual_def, []) 283 |> Enum.map(&Code.Typespec.spec_to_quoted(name, &1)) 284 285 if kind == :macro do 286 Enum.map(specs, &remove_first_macro_arg/1) 287 else 288 specs 289 end 290 end 291 292 defp actual_def(name, arity, :macrocallback) do 293 {String.to_atom("MACRO-" <> to_string(name)), arity + 1} 294 end 295 296 defp actual_def(name, arity, :macro) do 297 {String.to_atom("MACRO-" <> to_string(name)), arity + 1} 298 end 299 300 defp actual_def(name, arity, _), do: {name, arity} 301 302 defp remove_first_macro_arg({:"::", info, [{name, info2, [_term_arg | rest_args]}, return]}) do 303 {:"::", info, [{name, info2, rest_args}, return]} 304 end 305 306 defp remove_first_macro_arg({:when, meta, [lhs, rhs]}) do 307 {:when, meta, [remove_first_macro_arg(lhs), rhs]} 308 end 309 310 defp delegate_doc_ast({m, f, a}) do 311 [ 312 {:p, [], ["See ", {:code, [class: "inline"], [Exception.format_mfa(m, f, a)], %{}}, "."], 313 %{}} 314 ] 315 end 316 317 defp delegate_doc_ast(nil) do 318 nil 319 end 320 321 defp callback_doc_ast(name, arity, {:ok, behaviour}) do 322 [ 323 {:p, [], 324 [ 325 "Callback implementation for ", 326 {:code, [class: "inline"], ["c:#{inspect(behaviour)}.#{name}/#{arity}"], %{}}, 327 "." 328 ], %{}} 329 ] 330 end 331 332 defp callback_doc_ast(_, _, _) do 333 nil 334 end 335 336 defp find_function_line(module_data, {name, arity}) do 337 Enum.find_value(module_data.private.abst_code, fn 338 {:function, anno, ^name, ^arity, _} -> anno_line(anno) 339 _ -> nil 340 end) 341 end 342 343 defp anno_line(line) when is_integer(line), do: abs(line) 344 defp anno_line(anno), do: anno |> :erl_anno.line() |> abs() 345 346 defp get_typespec_signature({:when, _, [{:"::", _, [{name, meta, args}, _]}, _]}, arity) do 347 Macro.to_string({name, meta, strip_types(args, arity)}) 348 end 349 350 defp get_typespec_signature({:"::", _, [{name, meta, args}, _]}, arity) do 351 Macro.to_string({name, meta, strip_types(args, arity)}) 352 end 353 354 defp get_typespec_signature({name, meta, args}, arity) do 355 Macro.to_string({name, meta, strip_types(args, arity)}) 356 end 357 358 defp strip_types(args, arity) do 359 args 360 |> Enum.take(-arity) 361 |> Enum.with_index(1) 362 |> Enum.map(fn 363 {{:"::", _, [left, _]}, position} -> to_var(left, position) 364 {{:|, _, _}, position} -> to_var({}, position) 365 {left, position} -> to_var(left, position) 366 end) 367 |> Macro.prewalk(fn node -> Macro.update_meta(node, &Keyword.delete(&1, :line)) end) 368 end 369 370 defp to_var({:%, meta, [name, _]}, _), do: {:%, meta, [name, {:%{}, meta, []}]} 371 defp to_var({:%{}, _, _}, _), do: {:map, [], nil} 372 defp to_var({name, meta, _}, _) when is_atom(name), do: {name, meta, nil} 373 374 defp to_var({{:., meta, [_module, name]}, _, _args}, _) when is_atom(name), 375 do: {name, meta, nil} 376 377 defp to_var([{:->, _, _} | _], _), do: {:function, [], nil} 378 defp to_var({:<<>>, _, _}, _), do: {:binary, [], nil} 379 defp to_var({:{}, _, _}, _), do: {:tuple, [], nil} 380 defp to_var({_, _}, _), do: {:tuple, [], nil} 381 defp to_var(integer, _) when is_integer(integer), do: {:integer, [], nil} 382 defp to_var(float, _) when is_integer(float), do: {:float, [], nil} 383 defp to_var(list, _) when is_list(list), do: {:list, [], nil} 384 defp to_var(atom, _) when is_atom(atom), do: {:atom, [], nil} 385 defp to_var(_, position), do: {:"arg#{position}", [], nil} 386 387 # Cut off the body of an opaque type while leaving it on a normal type. 388 defp process_type_ast({:"::", _, [d | _]}, :opaque), do: d 389 defp process_type_ast(ast, _), do: ast 390 391 ## Autolinking 392 393 @autoimported_modules [Kernel, Kernel.SpecialForms] 394 395 defp walk_doc(list, config) when is_list(list) do 396 Enum.map(list, &walk_doc(&1, config)) 397 end 398 399 defp walk_doc(binary, _) when is_binary(binary) do 400 binary 401 end 402 403 defp walk_doc({:pre, _, _, _} = ast, _config) do 404 ast 405 end 406 407 defp walk_doc({:a, attrs, inner, meta} = ast, config) do 408 case custom_link(attrs, config) do 409 :remove_link -> 410 remove_link(ast) 411 412 nil -> 413 ast 414 415 url -> 416 {:a, Keyword.put(attrs, :href, url), inner, meta} 417 end 418 end 419 420 defp walk_doc({:code, attrs, [code], meta} = ast, config) do 421 if url = url(code, :regular_link, config) do 422 code = remove_prefix(code) 423 {:a, [href: url], [{:code, attrs, [code], meta}], %{}} 424 else 425 ast 426 end 427 end 428 429 defp walk_doc({tag, attrs, ast, meta}, config) do 430 {tag, attrs, walk_doc(ast, config), meta} 431 end 432 433 defp remove_link({:a, _attrs, inner, _meta}), 434 do: inner 435 436 @ref_regex ~r/^`(.+)`$/ 437 438 defp custom_link(attrs, config) do 439 case Keyword.fetch(attrs, :href) do 440 {:ok, href} -> 441 case Regex.scan(@ref_regex, href) do 442 [[_, custom_link]] -> url(custom_link, :custom_link, config) 443 [] -> build_extra_link(href, config) 444 end 445 446 _ -> 447 nil 448 end 449 end 450 451 defp build_extra_link(link, config) do 452 with %{scheme: nil, host: nil, path: path} = uri <- URI.parse(link), 453 true <- is_binary(path) and path != "" and not (path =~ @ref_regex), 454 true <- Path.extname(path) in [".livemd", ".md", ".txt", ""] do 455 if file = config.extras[Path.basename(path)] do 456 fragment = (uri.fragment && "#" <> uri.fragment) || "" 457 file <> config.ext <> fragment 458 else 459 Autolink.maybe_warn(nil, config, nil, %{file_path: path, original_text: link}) 460 nil 461 end 462 else 463 _ -> nil 464 end 465 end 466 467 @basic_types [ 468 any: 0, 469 none: 0, 470 atom: 0, 471 map: 0, 472 pid: 0, 473 port: 0, 474 reference: 0, 475 struct: 0, 476 tuple: 0, 477 float: 0, 478 integer: 0, 479 neg_integer: 0, 480 non_neg_integer: 0, 481 pos_integer: 0, 482 list: 1, 483 nonempty_list: 1, 484 maybe_improper_list: 2, 485 nonempty_improper_list: 2, 486 nonempty_maybe_improper_list: 2 487 ] 488 489 @built_in_types [ 490 term: 0, 491 arity: 0, 492 as_boolean: 1, 493 binary: 0, 494 bitstring: 0, 495 boolean: 0, 496 byte: 0, 497 char: 0, 498 charlist: 0, 499 nonempty_charlist: 0, 500 fun: 0, 501 function: 0, 502 identifier: 0, 503 iodata: 0, 504 iolist: 0, 505 keyword: 0, 506 keyword: 1, 507 list: 0, 508 nonempty_list: 0, 509 maybe_improper_list: 0, 510 nonempty_maybe_improper_list: 0, 511 mfa: 0, 512 module: 0, 513 no_return: 0, 514 node: 0, 515 number: 0, 516 struct: 0, 517 timeout: 0 518 ] 519 520 defp url(string = "mix help " <> name, mode, config), do: mix_task(name, string, mode, config) 521 defp url(string = "mix " <> name, mode, config), do: mix_task(name, string, mode, config) 522 523 defp url(string, mode, config) do 524 case Regex.run(~r{^(.+)/(\d+)$}, string) do 525 [_, left, right] -> 526 with {:ok, arity} <- parse_arity(right) do 527 {kind, rest} = kind(left) 528 529 case parse_module_function(rest) do 530 {:local, function} -> 531 local_url(kind, function, arity, config, string, mode: mode) 532 533 {:remote, module, function} -> 534 remote_url({kind, module, function, arity}, config, string, mode: mode) 535 536 :error -> 537 nil 538 end 539 else 540 _ -> 541 nil 542 end 543 544 nil -> 545 case parse_module(string, mode) do 546 {:module, module} -> 547 module_url(module, mode, config, string) 548 549 :error -> 550 nil 551 end 552 553 _ -> 554 nil 555 end 556 end 557 558 defp kind("c:" <> rest), do: {:callback, rest} 559 defp kind("t:" <> rest), do: {:type, rest} 560 defp kind(rest), do: {:function, rest} 561 562 defp remove_prefix("c:" <> rest), do: rest 563 defp remove_prefix("t:" <> rest), do: rest 564 defp remove_prefix(rest), do: rest 565 566 defp parse_arity(string) do 567 case Integer.parse(string) do 568 {arity, ""} -> {:ok, arity} 569 _ -> :error 570 end 571 end 572 573 defp parse_module_function(string) do 574 case string |> String.split(".") |> Enum.reverse() do 575 [string] -> 576 with {:function, function} <- parse_function(string) do 577 {:local, function} 578 end 579 580 ["", "", ""] -> 581 {:local, :..} 582 583 ["//", "", ""] -> 584 {:local, :"..//"} 585 586 ["", ""] -> 587 {:local, :.} 588 589 ["", "", "" | rest] -> 590 module_string = rest |> Enum.reverse() |> Enum.join(".") 591 592 with {:module, module} <- parse_module(module_string, :custom_link) do 593 {:remote, module, :..} 594 end 595 596 ["", "" | rest] -> 597 module_string = rest |> Enum.reverse() |> Enum.join(".") 598 599 with {:module, module} <- parse_module(module_string, :custom_link) do 600 {:remote, module, :.} 601 end 602 603 [function_string | rest] -> 604 module_string = rest |> Enum.reverse() |> Enum.join(".") 605 606 with {:module, module} <- parse_module(module_string, :custom_link), 607 {:function, function} <- parse_function(function_string) do 608 {:remote, module, function} 609 end 610 end 611 end 612 613 defp parse_module(<<first>> <> _ = string, _mode) when first in ?A..?Z do 614 do_parse_module(string) 615 end 616 617 defp parse_module(<<?:>> <> _ = string, :custom_link) do 618 do_parse_module(string) 619 end 620 621 defp parse_module(_, _) do 622 :error 623 end 624 625 defp do_parse_module(string) do 626 case Code.string_to_quoted(string, warn_on_unnecessary_quotes: false) do 627 {:ok, module} when is_atom(module) -> 628 {:module, module} 629 630 {:ok, {:__aliases__, _, parts}} -> 631 if Enum.all?(parts, &is_atom/1) do 632 {:module, Module.concat(parts)} 633 else 634 :error 635 end 636 637 _ -> 638 :error 639 end 640 end 641 642 # There are special forms that are forbidden by the tokenizer 643 defp parse_function("__aliases__"), do: {:function, :__aliases__} 644 defp parse_function("__block__"), do: {:function, :__block__} 645 defp parse_function("%"), do: {:function, :%} 646 647 defp parse_function(string) do 648 case Code.string_to_quoted("& #{string}/0") do 649 {:ok, {:&, _, [{:/, _, [{function, _, _}, 0]}]}} when is_atom(function) -> 650 {:function, function} 651 652 _ -> 653 :error 654 end 655 end 656 657 defp mix_task(name, string, mode, config) do 658 {module, url, visibility} = 659 if name =~ ~r/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/ do 660 parts = name |> String.split(".") |> Enum.map(&Macro.camelize/1) 661 module = Module.concat([Mix, Tasks | parts]) 662 663 {module, module_url(module, :regular_link, config, string), 664 Refs.get_visibility({:module, module})} 665 else 666 {nil, nil, :undefined} 667 end 668 669 if url in [nil, :remove_link] and mode == :custom_link do 670 Autolink.maybe_warn({:module, module}, config, visibility, %{ 671 mix_task: true, 672 original_text: string 673 }) 674 end 675 676 url 677 end 678 679 defp safe_format_string!(string) do 680 try do 681 string 682 |> Code.format_string!(line_length: 80) 683 |> IO.iodata_to_binary() 684 rescue 685 _ -> string 686 end 687 end 688 689 defp typespec_name({:"::", _, [{name, _, _}, _]}), do: Atom.to_string(name) 690 defp typespec_name({:when, _, [left, _]}), do: typespec_name(left) 691 defp typespec_name({name, _, _}) when is_atom(name), do: Atom.to_string(name) 692 693 # extract out function name so we don't process it. This is to avoid linking it when there's 694 # a type with the same name 695 defp split_name(string, name) do 696 if String.starts_with?(string, name) do 697 {name, binary_part(string, byte_size(name), byte_size(string) - byte_size(name))} 698 else 699 {"", string} 700 end 701 end 702 703 defp do_typespec(string, config) do 704 regex = ~r{ 705 ( # <call_string> 706 (?: 707 ( # <module_string> 708 (?: 709 \:[a-z][_a-zA-Z0-9]* # Erlang module 710 )| 711 (?: 712 [A-Z][_a-zA-Z0-9]* # Elixir module 713 (?:\.[A-Z][_a-zA-Z0-9]*)* # Elixir submodule 714 ) 715 ) # </module_string> 716 \. # Dot operator 717 )? 718 ([a-z_][_a-zA-Z0-9]*[\?\!]?) # Name <name_string /> 719 ) # </call_string> 720 (\(.*\)) # Arguments <rest /> 721 }x 722 723 Regex.replace(regex, string, fn _all, call_string, module_string, name_string, rest -> 724 module = string_to_module(module_string) 725 name = String.to_atom(name_string) 726 arity = count_args(rest, 0, 0) 727 original_text = call_string <> "()" 728 729 url = 730 if module do 731 remote_url({:type, module, name, arity}, config, original_text) 732 else 733 local_url(:type, name, arity, config, original_text) 734 end 735 736 if url do 737 ~s[<a href="#{url}">#{ExDoc.Utils.h(call_string)}</a>] 738 else 739 call_string 740 end <> do_typespec(rest, config) 741 end) 742 end 743 744 defp string_to_module(""), do: nil 745 746 defp string_to_module(string) do 747 if String.starts_with?(string, ":") do 748 string |> String.trim_leading(":") |> String.to_atom() 749 else 750 Module.concat([string]) 751 end 752 end 753 754 defp count_args("()" <> _, 0, 0), do: 0 755 defp count_args("(" <> rest, counter, acc), do: count_args(rest, counter + 1, acc) 756 defp count_args("[" <> rest, counter, acc), do: count_args(rest, counter + 1, acc) 757 defp count_args("{" <> rest, counter, acc), do: count_args(rest, counter + 1, acc) 758 defp count_args(")" <> _, 1, acc), do: acc + 1 759 defp count_args(")" <> rest, counter, acc), do: count_args(rest, counter - 1, acc) 760 defp count_args("]" <> rest, counter, acc), do: count_args(rest, counter - 1, acc) 761 defp count_args("}" <> rest, counter, acc), do: count_args(rest, counter - 1, acc) 762 defp count_args("," <> rest, 1, acc), do: count_args(rest, 1, acc + 1) 763 defp count_args(<<_>> <> rest, counter, acc), do: count_args(rest, counter, acc) 764 defp count_args("", _counter, acc), do: acc 765 766 ## Internals 767 768 defp module_url(module, mode, config, string) do 769 ref = {:module, module} 770 771 case {mode, Refs.get_visibility(ref)} do 772 {_link_type, visibility} when visibility in [:public, :limited] -> 773 Autolink.app_module_url(Autolink.tool(module, config), module, config) 774 775 {:regular_link, :undefined} -> 776 nil 777 778 {:custom_link, visibility} when visibility in [:hidden, :undefined] -> 779 Autolink.maybe_warn(ref, config, visibility, %{original_text: string}) 780 :remove_link 781 782 {_link_type, visibility} -> 783 Autolink.maybe_warn(ref, config, visibility, %{original_text: string}) 784 nil 785 end 786 end 787 788 defp local_url(kind, name, arity, config, original_text, options \\ []) 789 790 defp local_url(:type, name, arity, config, _original_text, _options) 791 when {name, arity} in @basic_types do 792 Autolink.ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#basic-types") 793 end 794 795 defp local_url(:type, name, arity, config, _original_text, _options) 796 when {name, arity} in @built_in_types do 797 Autolink.ex_doc_app_url(Kernel, config, "typespecs", config.ext, "#built-in-types") 798 end 799 800 defp local_url(kind, name, arity, config, original_text, options) do 801 module = config.current_module 802 ref = {kind, module, name, arity} 803 mode = Keyword.get(options, :mode, :regular_link) 804 visibility = Refs.get_visibility(ref) 805 806 case {kind, visibility} do 807 {_kind, :public} -> 808 fragment(Autolink.tool(module, config), kind, name, arity) 809 810 {:function, _visibility} -> 811 try_autoimported_function(name, arity, mode, config, original_text) 812 813 {:type, :hidden} -> 814 nil 815 816 {:type, _} -> 817 nil 818 819 _ -> 820 Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text}) 821 nil 822 end 823 end 824 825 defp try_autoimported_function(name, arity, mode, config, original_text) do 826 Enum.find_value(@autoimported_modules, fn module -> 827 remote_url({:function, module, name, arity}, config, original_text, 828 warn?: false, 829 mode: mode 830 ) 831 end) 832 end 833 834 defp remote_url({kind, module, name, arity} = ref, config, original_text, opts \\ []) do 835 warn? = Keyword.get(opts, :warn?, true) 836 mode = Keyword.get(opts, :mode, :regular_link) 837 same_module? = module == config.current_module 838 839 case {mode, Refs.get_visibility({:module, module}), Refs.get_visibility(ref)} do 840 {_mode, _module_visibility, :public} -> 841 tool = Autolink.tool(module, config) 842 843 if same_module? do 844 fragment(tool, kind, name, arity) 845 else 846 Autolink.app_module_url(tool, module, config) <> fragment(tool, kind, name, arity) 847 end 848 849 {:regular_link, module_visibility, :undefined} 850 when module_visibility == :public 851 when module_visibility == :limited and kind != :type -> 852 if warn?, 853 do: Autolink.maybe_warn(ref, config, :undefined, %{original_text: original_text}) 854 855 nil 856 857 {:regular_link, _module_visibility, :undefined} when not same_module? -> 858 nil 859 860 {_mode, _module_visibility, visibility} -> 861 if warn?, 862 do: Autolink.maybe_warn(ref, config, visibility, %{original_text: original_text}) 863 864 nil 865 end 866 end 867 868 defp prefix(kind) 869 defp prefix(:function), do: "" 870 defp prefix(:callback), do: "c:" 871 defp prefix(:type), do: "t:" 872 873 defp fragment(:ex_doc, kind, name, arity) do 874 "#" <> prefix(kind) <> "#{URI.encode(Atom.to_string(name))}/#{arity}" 875 end 876 877 defp fragment(_, kind, name, arity) do 878 case kind do 879 :function -> "##{name}-#{arity}" 880 :callback -> "#Module:#{name}-#{arity}" 881 :type -> "#type-#{name}" 882 end 883 end 884 end