Skip to content

Commit

Permalink
Merge pull request elixir-lang#4662 from elixir-lang/emj-map-typespec
Browse files Browse the repository at this point in the history
Add support for OTP 19 map typespecs
  • Loading branch information
ericmj committed May 24, 2016
2 parents bdee464 + db46243 commit dd29c4e
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 29 deletions.
29 changes: 22 additions & 7 deletions lib/elixir/lib/kernel/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir/lib/system.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 27 additions & 15 deletions lib/elixir/pages/Typespecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -45,29 +45,35 @@ The following literals are also supported in typespecs:
| 1..10 ## Integers from 1 to 10
| 1.0 ## Floats

| <<>> ## Bitstrings
## Bitstrings
| <<>> # empty bitstring
| <<_::size>> # size is 0 or a positive integer
| <<_::_*unit>> # unit is an integer from 1 to 256
| <<_::_ * 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
25 changes: 20 additions & 5 deletions lib/elixir/test/elixir/kernel/typespec_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,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
Expand All @@ -206,9 +219,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
Expand Down Expand Up @@ -579,6 +592,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]),
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/lib/mix/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit dd29c4e

Please sign in to comment.