diff options
| author | Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com> | 2016-05-22 19:49:17 +0200 |
|---|---|---|
| committer | Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com> | 2016-05-23 12:09:41 +0200 |
| commit | 110d0cde0e1d10a72706d29eacc06d466b85dcba (patch) | |
| tree | e4d880efb58c8e746f271fdcaa6a3b111d77e526 | |
| parent | 97fb96af71ece23e748511b2b4442a3b07f14860 (diff) | |
| download | elixir-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.ex | 29 | ||||
| -rw-r--r-- | lib/elixir/lib/system.ex | 2 | ||||
| -rw-r--r-- | lib/elixir/pages/Typespecs.md | 44 | ||||
| -rw-r--r-- | lib/elixir/test/elixir/kernel/typespec_test.exs | 25 | ||||
| -rw-r--r-- | lib/mix/lib/mix/project.ex | 2 |
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] |
