diff --git a/CHANGELOG.md b/CHANGELOG.md index d4166b00085..235884acbdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,7 +108,9 @@ TODO. * [Enum] Allow slicing with steps in `Enum.slice/2` * [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero * [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol - * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, `Version`, and `Version.Requirement` + * [Inspect] Allow `:optional` when deriving the Inspect protocol + * [Inspect] Inspect struct fields in the order they are defined + * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement` * [Kernel] Allow any guard expression as the size of a bitstring in a pattern match * [Kernel] Allow composite types with pins as the map key in a pattern match * [Kernel] Print escaped version of control chars when they show up as unexpected tokens diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 6407ac6adbf..29923748c5a 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -8,16 +8,85 @@ defprotocol Inspect do The `Inspect` protocol converts an Elixir data structure into an algebra document. + This is typically done when you want to customize how your own + structs are inspected in logs and the terminal. + This documentation refers to implementing the `Inspect` protocol for your own data structures. To learn more about using inspect, see `Kernel.inspect/2` and `IO.inspect/2`. - The `inspect/2` function receives the entity to be inspected - followed by the inspecting options, represented by the struct - `Inspect.Opts`. Building of the algebra document is done with - `Inspect.Algebra`. + ## Inspect representation + + There are typically three choices of inspect representation. In order + to understand them, let's imagine we have the following `User` struct: + + defmodule User do + defstruct [:id, :name, :address] + end + + Our choices are: + + 1. Print the struct using Elixir's struct syntax, for example: + `%User{address: "Earth", id: 13, name: "Jane"}`. This is the + default representation and best choice if all struct fields + are public. + + 2. Print using the `#User<...>` notation, for example: `#User`. + This notation does not emit valid Elixir code and is typically + used when the struct has private fields (for example, you may want + to hide the field `:address` to redact person identifiable information). + + 3. Print the struct using the expression syntax, for example: + `User.new(13, "Jane", "Earth")`. This assumes there is a `User.new/3` + function. This option is mostly used as an alternative to option 2 + for representing custom data structures, such as `MapSet`, `Date.Range`, + and others. + + You can implement the Inspect protocol for your own structs while + adhering to the conventions above. Option 1 is the default representation + and you can quickly achieve option 2 by deriving the `Inspect` protocol. + For option 3, you need your custom implementation. + + ## Deriving + + The `Inspect` protocol can be derived to customize the order of fields + (the default is alphabetical) and hide certain fields from structs, + so they don't show up in logs, inspects and similar. The latter is + especially useful for fields containing private information. + + The supported options are: + + * `:only` - only include the given fields when inspecting. + + * `:except` - remove the given fields when inspecting. + + * `:optional` - (since v1.14.0) do not include a field if it + matches its default value. This can be used to simplify the + struct representation at the cost of hiding information. - ## Examples + Whenever `:only` or `:except` are used to restrict fields, + the struct will be printed using the `#User<...>` notation, + as the struct can no longer be copy and pasted as valid Elixir + code. Let's see an example: + + defmodule User do + @derive {Inspect, only: [:id, :name]} + defstruct [:id, :name, :address] + end + + inspect(%User{id: 1, name: "Jane", address: "Earth"}) + #=> #User + + If you use only the `:optional` option, the struct will still be + printed as `%User{...}`. + + ## Custom implementation + + You can also define your custom protocol implementation by + defining the `inspect/2` function. The function receives the + entity to be inspected followed by the inspecting options, + represented by the struct `Inspect.Opts`. Building of the + algebra document is done with `Inspect.Algebra`. Many times, inspecting a structure can be implemented in function of existing entities. For example, here is `MapSet`'s `inspect/2` @@ -27,23 +96,24 @@ defprotocol Inspect do import Inspect.Algebra def inspect(map_set, opts) do - concat(["#MapSet<", to_doc(MapSet.to_list(map_set), opts), ">"]) + concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"]) end end The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from `Inspect.Algebra` and it concatenates algebra documents together. - In the example above it is concatenating the string `"#MapSet<"`, + In the example above it is concatenating the string `"MapSet.new("`, the document returned by `Inspect.Algebra.to_doc/2`, and the final - string `">"`. We prefix the module name `#` to denote the inspect - presentation is not actually valid Elixir syntax. + string `")"`. Therefore, the MapSet with the numbers 1, 2, and 3 + will be printed as: - Finally, note strings themselves are valid algebra documents that - keep their formatting when pretty printed. This means your `Inspect` - implementation may simply return a string, although that will devoid - it of any pretty-printing. + iex> MapSet.new([1, 2, 3], fn x -> x * 2 end) + MapSet.new([2, 4, 6]) - ## Error handling + In other words, `MapSet`'s inspect representation returns an expression + that, when evaluated, builds the `MapSet` itself. + + ### Error handling In case there is an error while your structure is being inspected, Elixir will raise an `ArgumentError` error and will automatically fall back @@ -55,24 +125,6 @@ defprotocol Inspect do Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{}) - ## Deriving - - The `Inspect` protocol can be derived to hide certain fields from - structs, so they don't show up in logs, inspects and similar. This - is especially useful for fields containing private information. - - The options `:only` and `:except` can be used with `@derive` to - specify which fields should and should not appear in the - algebra document: - - defmodule User do - @derive {Inspect, only: [:id, :name]} - defstruct [:id, :name, :address] - end - - inspect(%User{id: 1, name: "Homer", address: "742 Evergreen Terrace"}) - #=> #User - """ # Handle structs in Any @@ -250,29 +302,34 @@ end defimpl Inspect, for: Map do def inspect(map, opts) do - inspect(map, "", opts) - end + list = Map.to_list(map) - def inspect(map, name, opts) do - map = Map.to_list(map) - open = color("%" <> name <> "{", :map, opts) - sep = color(",", :map, opts) - close = color("}", :map, opts) - container_doc(open, map, close, opts, traverse_fun(map, opts), separator: sep, break: :strict) + fun = + if Inspect.List.keyword?(list) do + &Inspect.List.keyword/2 + else + sep = color(" => ", :map, opts) + &to_assoc(&1, &2, sep) + end + + map_container_doc(list, "", opts, fun) end - defp traverse_fun(list, opts) do - if Inspect.List.keyword?(list) do - &Inspect.List.keyword/2 - else - sep = color(" => ", :map, opts) - &to_map(&1, &2, sep) - end + def inspect(map, name, infos, opts) do + fun = fn %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) end + map_container_doc(infos, name, opts, fun) end - defp to_map({key, value}, opts, sep) do + defp to_assoc({key, value}, opts, sep) do concat(concat(to_doc(key, opts), sep), to_doc(value, opts)) end + + defp map_container_doc(list, name, opts, fun) do + open = color("%" <> name <> "{", :map, opts) + sep = color(",", :map, opts) + close = color("}", :map, opts) + container_doc(open, list, close, opts, fun, separator: sep, break: :strict) + end end defimpl Inspect, for: Integer do @@ -454,72 +511,99 @@ defimpl Inspect, for: Any do fields = Map.keys(struct) -- [:__exception__, :__struct__] only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) + optional = Keyword.get(options, :optional, []) - :ok = validate_option(only, fields, module) - :ok = validate_option(except, fields, module) + :ok = validate_option(:only, only, fields, module) + :ok = validate_option(:except, except, fields, module) + :ok = validate_option(:optional, optional, fields, module) + + inspect_module = + if fields == only and except == [] do + Inspect.Map + else + Inspect.Any + end filtered_fields = fields |> Enum.reject(&(&1 in except)) |> Enum.filter(&(&1 in only)) - inspect_module = - if fields == only and except == [] do - Inspect.Map + optional? = + if optional == [] do + false else - Inspect.Any + optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)} + + quote do + case unquote(Macro.escape(optional_map)) do + %{^var!(field) => var!(default)} -> + var!(default) == Map.get(var!(struct), var!(field)) + + %{} -> + false + end + end end quote do defimpl Inspect, for: unquote(module) do def inspect(var!(struct), var!(opts)) do - var!(map) = Map.take(var!(struct), unquote(filtered_fields)) + var!(infos) = + for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct), + var!(field) in unquote(filtered_fields) and not unquote(optional?), + do: var!(info) + var!(name) = Macro.inspect_atom(:literal, unquote(module)) - unquote(inspect_module).inspect(var!(map), var!(name), var!(opts)) + unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts)) end end end end - defp validate_option(option_list, fields, module) do + defp validate_option(option, option_list, fields, module) do case option_list -- fields do [] -> :ok unknown_fields -> raise ArgumentError, - "unknown fields #{Kernel.inspect(unknown_fields)} given when deriving the Inspect protocol for #{Kernel.inspect(module)}. :only and :except values must match struct fields" + "unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <> + "when deriving the Inspect protocol for #{Kernel.inspect(module)}" end end def inspect(%module{} = struct, opts) do try do - module.__struct__() + {module.__struct__(), module.__info__(:struct)} rescue _ -> Inspect.Map.inspect(struct, opts) else - dunder -> + {dunder, fields} -> if Map.keys(dunder) == Map.keys(struct) do - pruned = Map.drop(struct, [:__struct__, :__exception__]) - Inspect.Map.inspect(pruned, Macro.inspect_atom(:literal, module), opts) + infos = + for %{field: field} = info <- fields, + field not in [:__struct__, :__exception__], + do: info + + Inspect.Map.inspect(struct, Macro.inspect_atom(:literal, module), infos, opts) else Inspect.Map.inspect(struct, opts) end end end - def inspect(map, name, opts) do - map = Map.to_list(map) ++ [:...] + def inspect(map, name, infos, opts) do open = color("#" <> name <> "<", :map, opts) sep = color(",", :map, opts) close = color(">", :map, opts) fun = fn - {key, value}, opts -> Inspect.List.keyword({key, value}, opts) + %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) :..., _opts -> "..." end - container_doc(open, map, close, opts, fun, separator: sep, break: :strict) + container_doc(open, infos ++ [:...], close, opts, fun, separator: sep, break: :strict) end end diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index 7bc6c939e4a..f298d251372 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -469,8 +469,9 @@ defmodule Inspect.Algebra do defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) and is_limit(limit) do - limit = decrement(limit) - doc = fun.(term, %{opts | limit: limit}) + new_limit = decrement(limit) + doc = fun.(term, %{opts | limit: new_limit}) + limit = if doc == :doc_nil, do: limit, else: new_limit container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc)) end diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 3bad1ad1608..bc6f04114bc 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5119,13 +5119,21 @@ defmodule Kernel do list before invoking `defstruct/1`: defmodule User do - @derive [MyProtocol] - defstruct name: nil, age: 10 + 11 + @derive MyProtocol + defstruct name: nil, age: nil end MyProtocol.call(john) # it works! - For each protocol in the `@derive` list, Elixir will assert the protocol has + A common example is to `@derive` the `Inspect` protocol to hide certain fields + when the struct is printed: + + defmodule User do + @derive {Inspect, only: :name} + defstruct name: nil, age: nil + end + + For each protocol in `@derive`, Elixir will assert the protocol has been implemented for `Any`. If the `Any` implementation defines a `__deriving__/3` callback, the callback will be invoked and it should define the implementation module. Otherwise an implementation that simply points to diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 712583c26f2..b5ef70eea5f 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -157,7 +157,7 @@ defmodule Kernel.Utils do [] -> quote do Enum.reduce(var!(kv), @__struct__, fn {key, val}, map -> - Map.replace!(map, key, val) + %{map | key => val} end) end @@ -166,7 +166,7 @@ defmodule Kernel.Utils do {map, keys} = Enum.reduce(var!(kv), {@__struct__, unquote(enforce_keys)}, fn {key, val}, {map, keys} -> - {Map.replace!(map, key, val), List.delete(keys, key)} + {%{map | key => val}, List.delete(keys, key)} end) case keys do @@ -193,10 +193,17 @@ defmodule Kernel.Utils do case enforce_keys -- :maps.keys(struct) do [] -> - derive = Module.get_attribute(module, :derive) + # The __struct__ field is used for expansion and for loading remote structs Module.put_attribute(module, :__struct__, struct) - Module.put_attribute(module, :__derived__, derive) - {struct, Module.get_attribute(module, :derive), body} + + # Finally store all field metadata to go into __info__(:struct) + mapper = fn {key, val} -> + %{field: key, default: val, required: :lists.member(key, enforce_keys)} + end + + {set, _} = :elixir_module.data_tables(module) + :ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)}) + {struct, Module.delete_attribute(module, :derive), body} error_keys -> raise ArgumentError, diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index c7da8376342..d5dba470fd8 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -550,6 +550,8 @@ defmodule Module do * `:module` - the module atom name + * `:struct` - if the module defines a struct and if so each field in order + """ @callback __info__(:attributes) :: keyword() @callback __info__(:compile) :: [term()] @@ -557,6 +559,7 @@ defmodule Module do @callback __info__(:macros) :: keyword() @callback __info__(:md5) :: binary() @callback __info__(:module) :: module() + @callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil @doc """ Returns information about module attributes used by Elixir. @@ -1814,22 +1817,17 @@ defmodule Module do [] -> :ok - to_derive -> - case :ets.lookup(set, :__derived__) do - [{_, derived, _}] -> - if to_derive != :lists.reverse(derived) do - message = - "warning: module attribute @derive was set after defstruct, all @derive calls must come before defstruct" - - IO.warn(message, env) - end - - [] -> - message = + _ -> + message = + case :ets.lookup(set, :__struct__) do + [] -> "warning: module attribute @derive was set but never used (it must come before defstruct)" - IO.warn(message, env) - end + _ -> + "warning: module attribute @derive was set after defstruct, all @derive calls must come before defstruct" + end + + IO.warn(message, env) end end diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index 5036e5ac049..bfb2ebd0793 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -5,29 +5,33 @@ defmodule URI do This module provides functions for working with URIs (for example, parsing URIs or encoding query strings). The functions in this module are implemented according to [RFC 3986](https://tools.ietf.org/html/rfc3986). - - URIs are structs behind the scenes. If you are creating `URI` structs manually, - be aware that the `authority` field is deprecated and should not be populated. """ - defstruct scheme: nil, - path: nil, - query: nil, - fragment: nil, - authority: nil, - userinfo: nil, - host: nil, - port: nil + @doc """ + The URI struct. + + The fields are defined to match the following URI representation + (with field names between brackets): + + [scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment] + + + Note the `authority` field is deprecated. `parse/1` will still + populate it for backwards compatibility but you should generally + avoid setting or getting it. + """ + @derive {Inspect, optional: [:authority]} + defstruct [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] @type t :: %__MODULE__{ + scheme: nil | binary, authority: authority, - fragment: nil | binary, + userinfo: nil | binary, host: nil | binary, - path: nil | binary, port: nil | :inet.port_number(), + path: nil | binary, query: nil | binary, - scheme: nil | binary, - userinfo: nil | binary + fragment: nil | binary } @typedoc deprecated: "The authority field is deprecated" @@ -707,7 +711,6 @@ defmodule URI do iex> URI.parse("/foo/bar") %URI{ - authority: nil, fragment: nil, host: nil, path: "/foo/bar", @@ -719,7 +722,6 @@ defmodule URI do iex> URI.parse("foo/bar") %URI{ - authority: nil, fragment: nil, host: nil, path: "foo/bar", @@ -734,7 +736,6 @@ defmodule URI do iex> URI.parse("/invalid_greater_than_in_path/>") %URI{ - authority: nil, fragment: nil, host: nil, path: "/invalid_greater_than_in_path/>", @@ -750,7 +751,6 @@ defmodule URI do iex> URI.parse("/?foo[bar]=baz") %URI{ - authority: nil, fragment: nil, host: nil, path: "/", diff --git a/lib/elixir/lib/version.ex b/lib/elixir/lib/version.ex index fb869959fee..61e9d760234 100644 --- a/lib/elixir/lib/version.ex +++ b/lib/elixir/lib/version.ex @@ -26,14 +26,6 @@ defmodule Version do "1.0.0-alpha.3+20130417140000.amd64" - ## Struct - - The version is represented by the `Version` struct and fields - are named according to SemVer 2.0: `:major`, `:minor`, `:patch`, - `:pre`, and `:build`. You can read those fields but you should - not create a new `Version` directly via the struct syntax. Instead - use the functions in this module. - ## Requirements Requirements allow you to specify which versions of a given @@ -99,8 +91,19 @@ defmodule Version do import Kernel, except: [match?: 2] + @doc """ + The Version struct. + + It contains the fields `:major`, `:minor`, `:patch`, `:pre`, and + `:build` according to SemVer 2.0, where `:pre` is a list. + + You can read those fields but you should not create a new `Version` + directly via the struct syntax. Instead use the functions in this + module. + """ @enforce_keys [:major, :minor, :patch] - defstruct [:major, :minor, :patch, :build, pre: []] + @derive {Inspect, optional: [:pre, :build]} + defstruct [:major, :minor, :patch, pre: [], build: nil] @type version :: String.t() | t @type requirement :: String.t() | Version.Requirement.t() @@ -353,9 +356,8 @@ defmodule Version do ## Examples - iex> {:ok, version} = Version.parse("2.0.1-alpha1") - iex> version - Version.parse!("2.0.1-alpha1") + iex> Version.parse("2.0.1-alpha1") + {:ok, %Version{major: 2, minor: 0, patch: 1, pre: ["alpha1"]}} iex> Version.parse("2.0-alpha1") :error @@ -382,7 +384,7 @@ defmodule Version do ## Examples iex> Version.parse!("2.0.1-alpha1") - Version.parse!("2.0.1-alpha1") + %Version{major: 2, minor: 0, patch: 1, pre: ["alpha1"]} iex> Version.parse!("2.0-alpha1") ** (Version.InvalidVersionError) invalid version: "2.0-alpha1" @@ -664,12 +666,6 @@ defimpl String.Chars, for: Version do defdelegate to_string(version), to: Version end -defimpl Inspect, for: Version do - def inspect(self, _opts) do - "Version.parse!(\"" <> Version.to_string(self) <> "\")" - end -end - defimpl String.Chars, for: Version.Requirement do def to_string(%Version.Requirement{source: source}) do source diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 05523fa36ef..0f8757b0cb2 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -75,6 +75,9 @@ elixir_to_erl([], Ann) -> {nil, Ann}; elixir_to_erl(<<>>, Ann) -> {bin, Ann, []}; +elixir_to_erl(#{} = Map, Ann) -> + Assocs = [{map_field_assoc, Ann, elixir_to_erl(K, Ann), elixir_to_erl(V, Ann)} || {K, V} <- maps:to_list(Map)], + {map, Ann, Assocs}; elixir_to_erl(Tree, Ann) when is_list(Tree) -> elixir_to_erl_cons(Tree, Ann); elixir_to_erl(Tree, Ann) when is_atom(Tree) -> @@ -249,7 +252,7 @@ functions_form(Line, Module, Def, Defmacro, Exports, Body, Deprecated, Struct) - [{attribute, Line, export, lists:sort([{'__info__', 1} | Exports])}, Spec, Info | Body]. add_info_function(Line, Module, Def, Defmacro, Deprecated, Struct) -> - AllowedAttrs = [attributes, compile, functions, macros, md5, exports_md5, module, deprecated], + AllowedAttrs = [attributes, compile, functions, macros, md5, exports_md5, module, deprecated, struct], AllowedArgs = lists:map(fun(Atom) -> {atom, Line, Atom} end, AllowedAttrs), SortedDef = lists:sort(Def), SortedDefmacro = lists:sort(Defmacro), @@ -269,6 +272,7 @@ add_info_function(Line, Module, Def, Defmacro, Deprecated, Struct) -> get_module_info(Module), functions_info(SortedDef), macros_info(SortedDefmacro), + struct_info(Struct), exports_md5_info(Struct, SortedDef, SortedDefmacro), get_module_info(Module, attributes), get_module_info(Module, compile), @@ -285,20 +289,26 @@ exports_md5_info(Struct, Def, Defmacro) -> %% Deprecations do not need to be part of exports_md5 because it is always %% checked by the runtime pass, so it is not really part of compilation. Md5 = erlang:md5(erlang:term_to_binary({Def, Defmacro, Struct})), - {clause, 0, [{atom, 0, exports_md5}], [], [elixir_erl:elixir_to_erl(Md5)]}. + {clause, 0, [{atom, 0, exports_md5}], [], [elixir_to_erl(Md5)]}. functions_info(Def) -> - {clause, 0, [{atom, 0, functions}], [], [elixir_erl:elixir_to_erl(Def)]}. + {clause, 0, [{atom, 0, functions}], [], [elixir_to_erl(Def)]}. macros_info(Defmacro) -> - {clause, 0, [{atom, 0, macros}], [], [elixir_erl:elixir_to_erl(Defmacro)]}. + {clause, 0, [{atom, 0, macros}], [], [elixir_to_erl(Defmacro)]}. + +struct_info(nil) -> + {clause, 0, [{atom, 0, struct}], [], [{atom, 0, nil}]}; +struct_info(Fields) -> + FieldsWithoutDefault = [maps:remove(default, FieldInfo) || FieldInfo <- Fields], + {clause, 0, [{atom, 0, struct}], [], [elixir_to_erl(FieldsWithoutDefault)]}. get_module_info(Module, Key) -> Call = ?remote(0, erlang, get_module_info, [{atom, 0, Module}, {var, 0, 'Key'}]), {clause, 0, [{match, 0, {var, 0, 'Key'}, {atom, 0, Key}}], [], [Call]}. deprecated_info(Deprecated) -> - {clause, 0, [{atom, 0, deprecated}], [], [elixir_erl:elixir_to_erl(Deprecated)]}. + {clause, 0, [{atom, 0, deprecated}], [], [elixir_to_erl(Deprecated)]}. % Typespecs diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 92d5d9783cd..81155dcab7d 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -430,14 +430,9 @@ warn_unused_attributes(File, DataSet, DataBag, PersistedAttrs) -> || [Key, Line] <- ets:select(DataSet, Query)]. get_struct(Set) -> - case ets:lookup(Set, '__struct__') of + case ets:lookup(Set, {elixir, struct}) of [] -> nil; - [{_, Struct, _}] -> - case ets:lookup(Set, enforce_keys) of - [] -> {Struct, []}; - [{_, EnforceKeys, _}] when is_list(EnforceKeys) -> {Struct, EnforceKeys}; - [{_, EnforceKeys, _}] -> {Struct, [EnforceKeys]} - end + [{_, Struct}] -> Struct end. get_deprecated(Bag) -> diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index e9b97c1f3d4..fd720df874a 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -453,7 +453,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/2: # 1 - %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} + %URI{scheme: \"https\", authority: \"elixir-lang.org\", userinfo: nil, host: \"elixir-lang.org\", port: 443, path: nil, query: nil, fragment: nil} # 2 URI @@ -469,7 +469,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/1: # 1 - %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} + %URI{scheme: \"https\", authority: \"elixir-lang.org\", userinfo: nil, host: \"elixir-lang.org\", port: 443, path: nil, query: nil, fragment: nil} Attempted function clauses (showing 1 out of 1): diff --git a/lib/elixir/test/elixir/inspect/algebra_test.exs b/lib/elixir/test/elixir/inspect/algebra_test.exs index 3c6477a4d9e..e5cb8b70111 100644 --- a/lib/elixir/test/elixir/inspect/algebra_test.exs +++ b/lib/elixir/test/elixir/inspect/algebra_test.exs @@ -308,4 +308,12 @@ defmodule Inspect.AlgebraTest do assert sm.(["a" | empty()]) |> render(80) == "[a]" assert sm.([empty() | "b"]) |> render(80) == "[b]" end + + test "formatting container_doc with empty and limit" do + opts = %Inspect.Opts{limit: 2} + value = ["a", empty(), "b"] + + assert container_doc("[", value, "]", opts, fn d, _ -> d end, separator: ",") |> render(80) == + "[a, b]" + end end diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 860da0e76b7..27f76ced173 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -696,7 +696,7 @@ defmodule Inspect.MapTest do test "struct missing fields in the :only option" do assert_raise ArgumentError, - "unknown fields [:c] given when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInOnlyOption. :only and :except values must match struct fields", + "unknown fields [:c] in :only when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInOnlyOption", fn -> defmodule StructMissingFieldsInOnlyOption do @derive {Inspect, only: [:c]} @@ -707,7 +707,7 @@ defmodule Inspect.MapTest do test "struct missing fields in the :except option" do assert_raise ArgumentError, - "unknown fields [:c, :d] given when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInExceptOption. :only and :except values must match struct fields", + "unknown fields [:c, :d] in :except when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInExceptOption", fn -> defmodule StructMissingFieldsInExceptOption do @derive {Inspect, except: [:c, :d]} @@ -741,6 +741,38 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "#Inspect.MapTest.StructWithBothOnlyAndExceptOptions<\n a: 1,\n ...\n>" end + + defmodule StructWithOptionalAndOrder do + @derive {Inspect, optional: [:b, :c]} + defstruct [:c, :d, :a, :b] + end + + test "struct with both :order and :optional options" do + struct = %StructWithOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} + + assert inspect(struct) == + "%Inspect.MapTest.StructWithOptionalAndOrder{c: 3, d: 4, a: 1, b: 2}" + + struct = %StructWithOptionalAndOrder{} + assert inspect(struct) == "%Inspect.MapTest.StructWithOptionalAndOrder{d: nil, a: nil}" + end + + defmodule StructWithExceptOptionalAndOrder do + @derive {Inspect, optional: [:b, :c], except: [:e]} + defstruct [:c, :d, :e, :a, :b] + end + + test "struct with :except, :order, and :optional options" do + struct = %StructWithExceptOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} + + assert inspect(struct) == + "#Inspect.MapTest.StructWithExceptOptionalAndOrder" + + struct = %StructWithExceptOptionalAndOrder{} + + assert inspect(struct) == + "#Inspect.MapTest.StructWithExceptOptionalAndOrder" + end end defmodule Inspect.OthersTest do diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index 0d7d592d943..f170c94751e 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -811,6 +811,11 @@ defmodule KernelTest do refute {:__info__, 1} in Kernel.__info__(:functions) end + test ":struct" do + assert Kernel.__info__(:struct) == nil + assert hd(URI.__info__(:struct)) == %{field: :scheme, required: false} + end + test "others" do assert Kernel.__info__(:module) == Kernel assert is_list(Kernel.__info__(:compile)) diff --git a/lib/iex/test/iex/info_test.exs b/lib/iex/test/iex/info_test.exs index b68c7fe6a7e..33f0794ded3 100644 --- a/lib/iex/test/iex/info_test.exs +++ b/lib/iex/test/iex/info_test.exs @@ -170,7 +170,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "Date" assert get_key(info, "Raw representation") == - "%Date{calendar: Calendar.ISO, day: 1, month: 1, year: 2017}" + "%Date{year: 2017, month: 1, day: 1, calendar: Calendar.ISO}" assert get_key(info, "Reference modules") == "Date, Calendar, Map" assert get_key(info, "Description") =~ "a date" @@ -183,7 +183,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "Time" assert get_key(info, "Raw representation") == - "%Time{calendar: Calendar.ISO, hour: 23, microsecond: {0, 0}, minute: 59, second: 59}" + "%Time{hour: 23, minute: 59, second: 59, microsecond: {0, 0}, calendar: Calendar.ISO}" assert get_key(info, "Reference modules") == "Time, Calendar, Map" assert get_key(info, "Description") =~ "a time" @@ -196,7 +196,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "NaiveDateTime" assert get_key(info, "Raw representation") == - "%NaiveDateTime{calendar: Calendar.ISO, day: 1, hour: 23, microsecond: {0, 0}, minute: 59, month: 1, second: 59, year: 2017}" + "%NaiveDateTime{year: 2017, month: 1, day: 1, hour: 23, minute: 59, second: 59, microsecond: {0, 0}, calendar: Calendar.ISO}" assert get_key(info, "Reference modules") == "NaiveDateTime, Calendar, Map"