summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@plataformatec.com.br>2017-12-11 18:21:57 +0100
committerJosé Valim <jose.valim@plataformatec.com.br>2017-12-11 18:22:08 +0100
commit4d3cbcdaaa92b25d40ceeec50782ecdb8b5e2934 (patch)
treef30a4273fbb49b1060cb2af8254f475a85928fa5
parent29e8385409245a6bb566ba34f5d5f516902fc6c9 (diff)
downloadelixir-4d3cbcdaaa92b25d40ceeec50782ecdb8b5e2934.tar.gz
Introduce @since as non-warning definition annotation
Partially implements #7099.
-rw-r--r--lib/elixir/lib/kernel/typespec.ex6
-rw-r--r--lib/elixir/lib/module.ex207
-rw-r--r--lib/elixir/src/elixir_module.erl4
-rw-r--r--lib/elixir/test/elixir/kernel/docs_test.exs12
-rw-r--r--lib/elixir/test/elixir/task/supervisor_test.exs6
5 files changed, 119 insertions, 116 deletions
diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex
index 7fd28f5bd..816f9f442 100644
--- a/lib/elixir/lib/kernel/typespec.ex
+++ b/lib/elixir/lib/kernel/typespec.ex
@@ -412,6 +412,7 @@ defmodule Kernel.Typespec do
defp store_callbackdoc(line, _file, module, kind, name, arity) do
table = :elixir_module.data_table(module)
{line, doc} = get_doc_info(table, :doc, line)
+ _ = get_since_info(table)
:ets.insert(table, {{:callbackdoc, {name, arity}}, line, kind, doc})
end
@@ -422,6 +423,10 @@ defmodule Kernel.Typespec do
end
end
+ defp get_since_info(table) do
+ :ets.take(table, :since)
+ end
+
@doc false
def deftype(kind, expr, line, file, module, pos) do
case type_to_signature(expr) do
@@ -435,6 +440,7 @@ defmodule Kernel.Typespec do
defp store_typedoc(line, file, module, kind, name, arity) do
table = :elixir_module.data_table(module)
{line, doc} = get_doc_info(table, :typedoc, line)
+ _ = get_since_info(table)
if kind == :typep && doc do
warning =
diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex
index 71f47958e..83cfaf989 100644
--- a/lib/elixir/lib/module.ex
+++ b/lib/elixir/lib/module.ex
@@ -104,7 +104,7 @@ defmodule Module do
Multiple uses of `@compile` will accumulate instead of overriding
previous ones. See the "Compile options" section below.
- ### `@doc`
+ ### `@doc` (and `@since`)
Provides documentation for the function or macro that follows the
attribute.
@@ -115,6 +115,7 @@ defmodule Module do
defmodule MyModule do
@doc "Hello world"
+ @since "1.1.0"
def hello do
"world"
end
@@ -127,6 +128,9 @@ defmodule Module do
end
end
+ `@since` is an optional attribute that annotates which version the
+ function was introduced.
+
### `@dialyzer`
Defines warnings to request or suppress when using a version of
@@ -395,7 +399,6 @@ defmodule Module do
@typep definition :: {atom, arity}
@typep def_kind :: :def | :defp | :defmacro | :defmacrop
- @typep type_kind :: :type | :typep | :opaque
@doc """
Provides runtime information about functions and macros defined by the
@@ -623,80 +626,42 @@ defmodule Module do
:elixir_aliases.safe_concat([left, right])
end
- @doc """
- Attaches documentation to a given function or type.
-
- It expects the module the function/type belongs to, the line (a non-negative integer),
- the kind (`:def`, `:defmacro`, `:type`, `:opaque`), a tuple `{<function name>, <arity>}`,
- the function signature (the signature should be omitted for types) and the documentation,
- which should be either a binary or a boolean.
-
- It returns `:ok` or `{:error, :private_doc}`.
-
- ## Examples
-
- defmodule MyModule do
- Module.add_doc(__MODULE__, __ENV__.line + 1, :def, {:version, 0}, [], "Manually added docs")
- def version, do: 1
- end
+ # Build signatures to be stored in docs
- """
- @spec add_doc(
- module,
- non_neg_integer,
- def_kind | type_kind,
- definition,
- list,
- String.t() | boolean | nil
- ) :: :ok | {:error, :private_doc}
- def add_doc(module, line, kind, function_tuple, signature \\ [], doc)
-
- def add_doc(_module, _line, kind, _function_tuple, _signature, doc)
- when kind in [:defp, :defmacrop, :typep] do
- if doc, do: {:error, :private_doc}, else: :ok
- end
-
- def add_doc(module, line, kind, function_tuple, signature, doc)
- when kind in [:def, :defmacro, :type, :opaque] and
- (is_binary(doc) or is_boolean(doc) or doc == nil) do
- assert_not_compiled!(:add_doc, module)
- table = data_table_for(module)
- signature = simplify_signature(signature)
+ defp build_signature(args, env) do
+ {reverse_args, counters} = simplify_args(args, %{}, [], env)
+ expand_vars(reverse_args, counters, [])
+ end
- case :ets.lookup(table, {:doc, function_tuple}) do
- [] ->
- :ets.insert(table, {{:doc, function_tuple}, line, kind, signature, doc})
- :ok
+ defp simplify_args([arg | args], counters, acc, env) do
+ {arg, counters} = simplify_arg(arg, counters, env)
+ simplify_args(args, counters, [arg | acc], env)
+ end
- [{doc_tuple, line, _current_kind, current_sign, current_doc}] ->
- signature = merge_signatures(current_sign, signature, 1)
- doc = if(is_nil(doc), do: current_doc, else: doc)
- :ets.insert(table, {doc_tuple, line, kind, signature, doc})
- :ok
- end
+ defp simplify_args([], counters, reverse_args, _env) do
+ {reverse_args, counters}
end
- # Simplify signatures to be stored in docs
+ defp simplify_arg({:\\, _, [left, right]}, acc, env) do
+ {left, acc} = simplify_arg(left, acc, env)
- defp simplify_signature(signature) do
- {signature, acc} = :lists.mapfoldl(&simplify_signature/2, [], signature)
- {signature, _} = :lists.mapfoldl(&expand_signature/2, {acc, acc}, signature)
- signature
- end
+ right =
+ Macro.prewalk(right, fn
+ {:@, _, _} = attr -> Macro.expand_once(attr, env)
+ other -> other
+ end)
- defp simplify_signature({:\\, _, [left, right]}, acc) do
- {left, acc} = simplify_signature(left, acc)
{{:\\, [], [left, right]}, acc}
end
# If the variable is being used explicitly for naming,
# we always give it a higher priority (nil) even if it
# starts with underscore.
- defp simplify_signature({:=, _, [{var, _, atom}, _]}, acc) when is_atom(atom) do
+ defp simplify_arg({:=, _, [{var, _, atom}, _]}, acc, _env) when is_atom(atom) do
{simplify_var(var, nil), acc}
end
- defp simplify_signature({:=, _, [_, {var, _, atom}]}, acc) when is_atom(atom) do
+ defp simplify_arg({:=, _, [_, {var, _, atom}]}, acc, _env) when is_atom(atom) do
{simplify_var(var, nil), acc}
end
@@ -704,26 +669,32 @@ defmodule Module do
# higher priority. However, if the variable starts with an
# underscore, we give it a secondary context (Elixir) with
# lower priority.
- defp simplify_signature({var, _, atom}, acc) when is_atom(atom) do
+ defp simplify_arg({var, _, atom}, acc, _env) when is_atom(atom) do
{simplify_var(var, Elixir), acc}
end
- defp simplify_signature({:%, _, [left, _]}, acc) when is_atom(left) do
- module_name = simplify_module_name(left)
- autogenerated(acc, module_name)
+ defp simplify_arg({:%, _, [left, _]}, acc, env) do
+ case Macro.expand_once(left, env) do
+ module when is_atom(module) -> autogenerated(acc, simplify_module_name(module))
+ _ -> autogenerated(acc, :struct)
+ end
end
- defp simplify_signature({:%{}, _, _}, acc) do
+ defp simplify_arg({:%{}, _, _}, acc, _env) do
autogenerated(acc, :map)
end
- defp simplify_signature(other, acc) when is_integer(other), do: autogenerated(acc, :int)
- defp simplify_signature(other, acc) when is_boolean(other), do: autogenerated(acc, :bool)
- defp simplify_signature(other, acc) when is_atom(other), do: autogenerated(acc, :atom)
- defp simplify_signature(other, acc) when is_list(other), do: autogenerated(acc, :list)
- defp simplify_signature(other, acc) when is_float(other), do: autogenerated(acc, :float)
- defp simplify_signature(other, acc) when is_binary(other), do: autogenerated(acc, :binary)
- defp simplify_signature(_, acc), do: autogenerated(acc, :arg)
+ defp simplify_arg({:@, _, _} = attr, acc, env) do
+ simplify_arg(Macro.expand_once(attr, env), acc, env)
+ end
+
+ defp simplify_arg(other, acc, _env) when is_integer(other), do: autogenerated(acc, :int)
+ defp simplify_arg(other, acc, _env) when is_boolean(other), do: autogenerated(acc, :bool)
+ defp simplify_arg(other, acc, _env) when is_atom(other), do: autogenerated(acc, :atom)
+ defp simplify_arg(other, acc, _env) when is_list(other), do: autogenerated(acc, :list)
+ defp simplify_arg(other, acc, _env) when is_float(other), do: autogenerated(acc, :float)
+ defp simplify_arg(other, acc, _env) when is_binary(other), do: autogenerated(acc, :binary)
+ defp simplify_arg(_, acc, _env), do: autogenerated(acc, :arg)
defp simplify_var(var, guess_priority) do
case Atom.to_string(var) do
@@ -744,33 +715,30 @@ defmodule Module do
end
defp autogenerated(acc, key) do
- {key, [key | acc]}
- end
-
- defp expand_signature(key, {all_keys, acc}) when is_atom(key) do
- case previous_values(key, all_keys, acc) do
- {i, acc} -> {{:"#{key}#{i}", [], Elixir}, {all_keys, acc}}
- :none -> {{key, [], Elixir}, {all_keys, acc}}
+ case acc do
+ %{^key => :once} -> {key, Map.put(acc, key, 2)}
+ %{^key => value} -> {key, Map.put(acc, key, value + 1)}
+ %{} -> {key, Map.put(acc, key, :once)}
end
end
- defp expand_signature(term, {_, _} = acc) do
- {term, acc}
- end
-
- defp previous_values(key, all_keys, acc) do
- total_occurrences = occurrences(key, all_keys)
+ defp expand_vars([key | keys], counters, acc) when is_atom(key) do
+ case counters do
+ %{^key => count} when is_integer(count) and count >= 1 ->
+ counters = Map.put(counters, key, count - 1)
+ expand_vars(keys, counters, [{:"#{key}#{count}", [], Elixir} | acc])
- if total_occurrences == 1 do
- :none
- else
- index = total_occurrences - occurrences(key, acc) + 1
- {index, :lists.delete(key, acc)}
+ _ ->
+ expand_vars(keys, counters, [{key, [], Elixir} | acc])
end
end
- defp occurrences(key, list) do
- length(:lists.filter(fn el -> el == key end, list))
+ defp expand_vars([arg | args], counters, acc) do
+ expand_vars(args, counters, [arg | acc])
+ end
+
+ defp expand_vars([], _counters, acc) do
+ acc
end
# Merge
@@ -1217,33 +1185,36 @@ defmodule Module do
{line, doc} = get_doc_info(table, env)
- # Arguments are not expanded for the docs, but we make an exception for
- # module attributes and for structs (aliases to be precise).
- args =
- Macro.prewalk(args, fn
- {:@, _, _} = attr ->
- Macro.expand_once(attr, env)
+ # TODO: Store @since alongside the docs
+ _ = get_since_info(table)
- {:%, meta, [aliases, fields]} ->
- {:%, meta, [Macro.expand_once(aliases, env), fields]}
+ add_doc(table, line, kind, pair, args, doc, env)
+ :ok
+ end
- x ->
- x
- end)
+ defp add_doc(_module, line, kind, {name, arity}, _args, doc, env)
+ when kind in [:defp, :defmacrop] do
+ if doc do
+ error_message =
+ "#{kind} #{name}/#{arity} is private, " <>
+ "@doc attribute is always discarded for private functions/macros/types"
- case add_doc(module, line, kind, pair, args, doc) do
- :ok ->
- :ok
+ :elixir_errors.warn(line, env.file, error_message)
+ end
+ end
- {:error, :private_doc} ->
- error_message =
- "#{kind} #{name}/#{arity} is private, " <>
- "@doc attribute is always discarded for private functions/macros/types"
+ defp add_doc(table, line, kind, pair, args, doc, env) do
+ signature = build_signature(args, env)
- :elixir_errors.warn(line, env.file, error_message)
- end
+ case :ets.lookup(table, {:doc, pair}) do
+ [] ->
+ :ets.insert(table, {{:doc, pair}, line, kind, signature, doc})
- :ok
+ [{doc_tuple, line, _current_kind, current_sign, current_doc}] ->
+ signature = merge_signatures(current_sign, signature, 1)
+ doc = if is_nil(doc), do: current_doc, else: doc
+ :ets.insert(table, {doc_tuple, line, kind, signature, doc})
+ end
end
@doc false
@@ -1709,6 +1680,12 @@ defmodule Module do
"must be set directly via the @ notation"
end
+ defp preprocess_attribute(:since, value) when not is_binary(value) do
+ raise ArgumentError,
+ "@since is used for documentation purposes and expects a string representing " <>
+ "the version a function, macro, type or callback was added, got: #{inspect(value)}"
+ end
+
defp preprocess_attribute(_key, value) do
value
end
@@ -1726,6 +1703,10 @@ defmodule Module do
end
end
+ defp get_since_info(table) do
+ :ets.take(table, :since)
+ end
+
defp data_table_for(module) do
:elixir_module.data_table(module)
end
diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl
index d7e500729..78bf7ba52 100644
--- a/lib/elixir/src/elixir_module.erl
+++ b/lib/elixir/src/elixir_module.erl
@@ -315,7 +315,9 @@ beam_location(#{lexical_tracker := Pid, module := Module}) ->
%% Handle unused attributes warnings and special cases.
warn_unused_attributes(File, Data, PersistedAttrs) ->
- ReservedAttrs = [after_compile, before_compile, moduledoc, on_definition | PersistedAttrs],
+ %% TODO: Remove since from this list once it is read by the docs tool
+ ReservedAttrs = [after_compile, before_compile, moduledoc,
+ on_definition, optional_callbacks, since | PersistedAttrs],
Keys = ets:select(Data, [{{'$1', '_', '_', '$2'}, [{is_atom, '$1'}, {is_integer, '$2'}], [['$1', '$2']]}]),
[elixir_errors:form_warn([{line, Line}], File, ?MODULE, {unused_attribute, Key}) ||
[Key, Line] <- Keys, not lists:member(Key, ReservedAttrs)].
diff --git a/lib/elixir/test/elixir/kernel/docs_test.exs b/lib/elixir/test/elixir/kernel/docs_test.exs
index 64992dc18..bb2e97b85 100644
--- a/lib/elixir/test/elixir/kernel/docs_test.exs
+++ b/lib/elixir/test/elixir/kernel/docs_test.exs
@@ -59,6 +59,15 @@ defmodule Kernel.DocsTest do
assert Code.get_docs(InMemoryDocs, :callback_docs) == nil
end
+ test "raises on invalid @since" do
+ assert_raise ArgumentError, ~r"@since is used for documentation purposes", fn ->
+ defmodule InvalidSince do
+ @since 1.2
+ def foo, do: :bar
+ end
+ end
+ end
+
describe "compiled with docs" do
test "infers signatures" do
write_beam(
@@ -132,12 +141,14 @@ defmodule Kernel.DocsTest do
@moduledoc "Module doc"
@typedoc "Type doc"
+ @since "1.2.3"
@type foo(any) :: any
@typedoc "Opaque type doc"
@opaque bar(any) :: any
@doc "Callback doc"
+ @since "1.2.3"
@callback foo(any) :: any
@doc false
@@ -148,6 +159,7 @@ defmodule Kernel.DocsTest do
@macrocallback qux(any) :: any
@doc "Function doc"
+ @since "1.2.3"
def foo(arg) do
arg + 1
end
diff --git a/lib/elixir/test/elixir/task/supervisor_test.exs b/lib/elixir/test/elixir/task/supervisor_test.exs
index 8106c56fb..805e004ca 100644
--- a/lib/elixir/test/elixir/task/supervisor_test.exs
+++ b/lib/elixir/test/elixir/task/supervisor_test.exs
@@ -43,10 +43,12 @@ defmodule Task.SupervisorTest do
test "counts and returns children", config do
assert Task.Supervisor.children(config[:supervisor]) == []
+
assert Supervisor.count_children(config[:supervisor]) ==
- %{active: 0, specs: 0, supervisors: 0, workers: 0}
+ %{active: 0, specs: 0, supervisors: 0, workers: 0}
+
assert DynamicSupervisor.count_children(config[:supervisor]) ==
- %{active: 0, specs: 0, supervisors: 0, workers: 0}
+ %{active: 0, specs: 0, supervisors: 0, workers: 0}
end
test "async/1", config do