summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>2016-05-22 19:49:17 +0200
committerEric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>2016-05-23 12:09:41 +0200
commit110d0cde0e1d10a72706d29eacc06d466b85dcba (patch)
treee4d880efb58c8e746f271fdcaa6a3b111d77e526
parent97fb96af71ece23e748511b2b4442a3b07f14860 (diff)
downloadelixir-emj-map-typespec.tar.gz
Add support for OTP 19 map typespecsemj-map-typespec
* %{required(foo) => bar} and %{optional(foo) => bar} forms are added * %{foo => bar} is deprecated in favor of the above forms * %{...} is added to mean any map * %{} now means empty map
-rw-r--r--lib/elixir/lib/kernel/typespec.ex29
-rw-r--r--lib/elixir/lib/system.ex2
-rw-r--r--lib/elixir/pages/Typespecs.md44
-rw-r--r--lib/elixir/test/elixir/kernel/typespec_test.exs25
-rw-r--r--lib/mix/lib/mix/project.ex2
5 files changed, 72 insertions, 30 deletions
diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex
index 008f5d6a8..852a27546 100644
--- a/lib/elixir/lib/kernel/typespec.ex
+++ b/lib/elixir/lib/kernel/typespec.ex
@@ -566,12 +566,14 @@ defmodule Kernel.Typespec do
defp typespec_to_ast({:type, line, :map, fields}) do
fields = Enum.map fields, fn
- # OTP 18
+ {:type, _, :map_field_assoc, :any} ->
+ {:..., [line: line], nil}
+ {:type, _, :map_field_exact, [{:atom, _, k}, v]} ->
+ {k, typespec_to_ast(v)}
+ {:type, _, :map_field_exact, [k, v]} ->
+ {{:required, [], [typespec_to_ast(k)]}, typespec_to_ast(v)}
{:type, _, :map_field_assoc, [k, v]} ->
- {typespec_to_ast(k), typespec_to_ast(v)}
- # OTP 17
- {:type, _, :map_field_assoc, k, v} ->
- {typespec_to_ast(k), typespec_to_ast(v)}
+ {{:optional, [], [typespec_to_ast(k)]}, typespec_to_ast(v)}
end
{struct, fields} = Keyword.pop(fields, :__struct__)
@@ -737,11 +739,24 @@ defmodule Kernel.Typespec do
defp typespec({:%{}, meta, fields} = map, vars, caller) do
fields =
:lists.map(fn
+ :... ->
+ {:type, line(meta), :map_field_assoc, :any}
+ {k, v} when is_atom(k) ->
+ {:type, line(meta), :map_field_exact, [typespec(k, vars, caller), typespec(v, vars, caller)]}
+ {{:required, meta2, [k]}, v} ->
+ {:type, line(meta2), :map_field_exact, [typespec(k, vars, caller), typespec(v, vars, caller)]}
+ {{:optional, meta2, [k]}, v} ->
+ {:type, line(meta2), :map_field_assoc, [typespec(k, vars, caller), typespec(v, vars, caller)]}
{k, v} ->
+ # :elixir_errors.warn(caller.line, caller.file,
+ # "invalid map specification. %{foo => bar} is deprecated in favor of " <>
+ # "%{required(foo) => bar} and %{optional(foo) => bar}. required/1 is an " <>
+ # "OTP 19 only feature, if you are targeting OTP 18 use optional/1.")
{:type, line(meta), :map_field_assoc, [typespec(k, vars, caller), typespec(v, vars, caller)]}
{:|, _, [_, _]} ->
- compile_error(caller, "invalid map specification. When using the | operator in the map key, " <>
- "make sure to wrap the key type in parentheses: #{Macro.to_string(map)}")
+ compile_error(caller,
+ "invalid map specification. When using the | operator in the map key, " <>
+ "make sure to wrap the key type in parentheses: #{Macro.to_string(map)}")
_ ->
compile_error(caller, "invalid map specification: #{Macro.to_string(map)}")
end, fields)
diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex
index 48f6be97a..1607c7703 100644
--- a/lib/elixir/lib/system.ex
+++ b/lib/elixir/lib/system.ex
@@ -340,7 +340,7 @@ defmodule System do
Returns a list of all environment variables. Each variable is given as a
`{name, value}` tuple where both `name` and `value` are strings.
"""
- @spec get_env() :: %{String.t => String.t}
+ @spec get_env() :: %{optional(String.t) => String.t}
def get_env do
Enum.into(:os.getenv, %{}, fn var ->
var = IO.chardata_to_string var
diff --git a/lib/elixir/pages/Typespecs.md b/lib/elixir/pages/Typespecs.md
index 72104cca4..0fe01e481 100644
--- a/lib/elixir/pages/Typespecs.md
+++ b/lib/elixir/pages/Typespecs.md
@@ -26,8 +26,8 @@ Integers and atom literals are allowed as types (ex. `1`, `:atom` or `false`). A
| pos_integer() # 1, 2, 3, ...
| neg_integer() # ..., -3, -2, -1
| float()
- | map()
- | struct()
+ | map() # any map
+ | struct() # any struct
| list(type)
| nonempty_list(type)
| improper_list(type1, type2)
@@ -45,29 +45,35 @@ The following literals are also supported in typespecs:
| 1..10 ## Integers from 1 to 10
| 1.0 ## Floats
- | <<>> ## Bitstrings
- | <<_::size>> # size is 0 or a positive integer
- | <<_::_ * unit>> # unit is an integer from 1 to 256
+ ## Bitstrings
+ | <<>> # empty bitstring
+ | <<_::size>> # size is 0 or a positive integer
+ | <<_::_ * unit>> # unit is an integer from 1 to 256
| <<_::size * unit>>
- | [type] ## Lists
+ ## Lists
+ | [type] # list with any number of type elements
| [] # empty list
| [...] # shorthand for nonempty_list(any())
| [type, ...] # shorthand for nonempty_list(type)
| [key: type] # keyword lists
- | (... -> type) ## Functions
+ ## Functions
| (... -> type) # any arity, returns type
| (() -> type) # 0-arity, returns type
| (type1, type2 -> type) # 2-arity, returns type
- | %{} ## Maps
- | %{key: type} # map with key :key with value of type
- | %{type1 => type2} # map with keys of type1 with values of type2
- | %SomeStruct{}
- | %SomeStruct{key: type}
-
- | {} ## Tuples
+ ## Maps
+ | %{} # empty map
+ | %{...} # any map
+ | %{key: type} # map with required key :key with value of type
+ | %{required(type1) => type2} # map with required keys of type1 with values of type2
+ | %{optional(type1) => type2} # map with optional keys of type1 with values of type2
+ | %SomeStruct{} # struct with all fields of any type
+ | %SomeStruct{key: type} # struct with :key field of type
+
+ ## Tuples
+ | {} # empty tuple
| {:ok, type} # two element tuple with an atom and any type
### Built-in types
@@ -106,6 +112,14 @@ Built-in type | Defined as
Any module is also able to define its own type and the modules in Elixir are no exception. For example, a string is `String.t`, a range is `Range.t`, any enumerable can be `Enum.t` and so on.
+### Maps
+
+The key types in maps are allowed to overlap, and if they do, the leftmost key takes precedence. A map value does not belong to this type if it contains a key that is not in the maps allowed keys.
+
+Because it is common to end a map type with `optional(any) => any` to denote that keys that do not belong to any other key in the map type are allowed, and may map to any value, the shorthand notation `...` is allowed as the last element of a map type.
+
+Notice that the syntactic representation of `map()` is `%{...}` (or `%{optional(any) => any}`), not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
+
## Defining a type
@type type_name :: type
@@ -151,5 +165,3 @@ Specifications can be overloaded just like ordinary functions.
Elixir discourages the use of type `string` as it might be confused with binaries which are referred to as "strings" in Elixir (as opposed to character lists). In order to use the type that is called `string` in Erlang, one has to use the `charlist` type which is a synonym for `string`. If you use `string`, you'll get a warning from the compiler.
If you want to refer to the "string" type (the one operated on by functions in the `String` module), use `String.t` type instead.
-
-In map and struct type declarations such as `%{key: value}` or `%Struct{key: value}`, the key-value pair type information is not used by the current version of dialyzer.
diff --git a/lib/elixir/test/elixir/kernel/typespec_test.exs b/lib/elixir/test/elixir/kernel/typespec_test.exs
index 376619561..b872e5025 100644
--- a/lib/elixir/test/elixir/kernel/typespec_test.exs
+++ b/lib/elixir/test/elixir/kernel/typespec_test.exs
@@ -169,14 +169,27 @@ defmodule Kernel.TypespecTest do
types(module)
end
- test "@type with a map" do
+ test "@type with a keyword map" do
module = test_module do
@type mytype :: %{hello: :world}
end
assert [type: {:mytype,
{:type, _, :map, [
- {:type, _, :map_field_assoc, [{:atom, _, :hello}, {:atom, _, :world}]}
+ {:type, _, :map_field_exact, [{:atom, _, :hello}, {:atom, _, :world}]}
+ ]},
+ []}] = types(module)
+ end
+
+ test "@type with a map" do
+ module = test_module do
+ @type mytype :: %{required(:a) => :b, optional(:c) => :d}
+ end
+
+ assert [type: {:mytype,
+ {:type, _, :map, [
+ {:type, _, :map_field_exact, [{:atom, _, :a}, {:atom, _, :b}]},
+ {:type, _, :map_field_assoc, [{:atom, _, :c}, {:atom, _, :d}]}
]},
[]}] = types(module)
end
@@ -189,9 +202,9 @@ defmodule Kernel.TypespecTest do
assert [type: {:mytype,
{:type, _, :map, [
- {:type, _, :map_field_assoc, [{:atom, _, :__struct__}, {:atom, _, TestTypespec}]},
- {:type, _, :map_field_assoc, [{:atom, _, :hello}, {:atom, _, :world}]},
- {:type, _, :map_field_assoc, [{:atom, _, :other}, {:type, _, :term, []}]}
+ {:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, TestTypespec}]},
+ {:type, _, :map_field_exact, [{:atom, _, :hello}, {:atom, _, :world}]},
+ {:type, _, :map_field_exact, [{:atom, _, :other}, {:type, _, :term, []}]}
]},
[]}] = types(module)
end
@@ -562,6 +575,8 @@ defmodule Kernel.TypespecTest do
(quote do: @type a_map() :: map()),
(quote do: @type empty_map() :: %{}),
(quote do: @type my_map() :: %{hello: :world}),
+ (quote do: @type my_req_map() :: %{required(0) => :atom}),
+ (quote do: @type my_opt_map() :: %{optional(0) => :atom}),
(quote do: @type my_struct() :: %Kernel.TypespecTest{hello: :world}),
(quote do: @type list1() :: list()),
(quote do: @type list2() :: [0]),
diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex
index 1d8c73032..c9d959155 100644
--- a/lib/mix/lib/mix/project.ex
+++ b/lib/mix/lib/mix/project.ex
@@ -245,7 +245,7 @@ defmodule Mix.Project do
#=> %{foo: "deps/foo", bar: "custom/path/dep"}
"""
- @spec deps_paths() :: %{atom => Path.t}
+ @spec deps_paths() :: %{optional(atom) => Path.t}
def deps_paths do
Enum.reduce Mix.Dep.cached(), %{}, fn
%{app: app, opts: opts}, acc -> Map.put acc, app, opts[:dest]