Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5131c6be9d | ||
|
|
e3f3dce208 | ||
|
|
5ebd0d2b0b | ||
|
|
e25c37108a | ||
|
|
0b30f6495b | ||
|
|
f126e9f6f6 | ||
|
|
ca06fbf6e0 | ||
|
|
20621d41c6 | ||
|
|
600ca02749 | ||
|
|
241e13da98 | ||
|
|
9c8250a092 | ||
|
|
867da6f876 | ||
|
|
b2b974dc71 | ||
|
|
7c0f6dd121 | ||
|
|
f8d775cb68 | ||
|
|
da10a7d2dd | ||
|
|
d4756f26da | ||
|
|
b9e2197ae3 | ||
|
|
16ea29d518 | ||
|
|
16a90cedda | ||
|
|
812b1cdc16 | ||
|
|
c6f7958ae8 | ||
|
|
68c5c31f81 | ||
|
|
dd79a3eda7 | ||
|
|
aca50d4d86 | ||
|
|
e33f3002ad | ||
|
|
9074a99199 | ||
|
|
c7729df924 | ||
|
|
61bb6c147f | ||
|
|
f27df203ab | ||
|
|
f252c9e00e | ||
|
|
dad832d5a3 | ||
|
|
094b64c13b | ||
|
|
1ca103d048 | ||
|
|
f3cbf1cdec | ||
|
|
ee4f3d55eb | ||
|
|
3d756ac5ad | ||
|
|
e6ed1d58d7 | ||
|
|
176501cb62 | ||
|
|
be4b7cd78d |
+78
-6
@@ -109,6 +109,75 @@ You can now also install archives from Hex in this way. Since they are fetched a
|
||||
|
||||
It is also possible to install escripts and archives by providing a Git/GitHub repo. See `mix help escript.install` and `mix help archive.install` for more details.
|
||||
|
||||
## v1.4.5 (2017-06-22)
|
||||
|
||||
This version includes changes that make Elixir fully compatible with Erlang/OTP 20.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Handle changes to crash reports in OTP 20
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [DateTime] Fix `DateTime.from_iso8601/2` when offset has no colon
|
||||
* [Registry] Do not leak EXIT messages on `Registry.dispatch/3`
|
||||
|
||||
## v1.4.4 (2017-05-15)
|
||||
|
||||
This version includes changes that make Elixir fully compatible with Erlang/OTP 20-rc.1.
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Map] Fix regression on struct update syntax
|
||||
|
||||
## v1.4.3 (2017-05-15)
|
||||
|
||||
This version includes changes that make Elixir fully compatible with Erlang/OTP 20-rc.1.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Improve compilation time for modules with many clauses
|
||||
* [Map] Warn when attempting to override `__struct__` key
|
||||
* [Regex] Add `recompile/1` and `recompile!/1` to ease transition to OTP 20 for archives and stored regexes
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger.Translator] Handle OTP 20 GenServer log messages
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Recompile projects if OTP version changes
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix code generation when non-binary bitstrings are in AST
|
||||
* [Record] Properly escape fields passed to `defrecord`
|
||||
* [Version] Reject leading zeros according to the SemVer spec
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Do not fail when comparing maps with nil or boolean keys
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Do not log exits on `IEx.Helpers.c/2` failures
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix archive.install] Detect proper path on URLs with query strings
|
||||
* [mix loadpaths] Do not assume all paths in loadpaths exist
|
||||
|
||||
|
||||
## v1.4.2 (2017-02-16)
|
||||
|
||||
### 1. Bug fixes
|
||||
@@ -121,7 +190,7 @@ It is also possible to install escripts and archives by providing a Git/GitHub r
|
||||
|
||||
* [Calendar] Correct typo on Calendar types
|
||||
* [Kernel] Ensure redefined functions point to the proper source
|
||||
* [OptionParser] Add `:allow_inexistent_atoms` to support unsafe behaviour prior to v1.4
|
||||
* [OptionParser] Add `:allow_nonexistent_atoms` to support unsafe behaviour prior to v1.4
|
||||
* [Stream] Allow consuming multiple items from suspended enumerable in `Stream.transform/3`
|
||||
* [String] Incorporate new grapheme rules in Unicode 9
|
||||
|
||||
@@ -269,14 +338,17 @@ It is also possible to install escripts and archives by providing a Git/GitHub r
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] `Access.key/1` is deprecated due to erratic behaviour for missing keys, please use Access.key/2 instead with proper default values
|
||||
* [Access] `Access.key/1` is deprecated due to erratic behaviour for missing keys, please use `Access.key/2` instead with proper default values
|
||||
* [Behaviour] The `Behaviour` module is deprecated. Callbacks may now be defined directly via the `@callback` attribute
|
||||
* [Enum] Deprecate `Enum.uniq/2` in favor of `Enum.uniq_by/2`
|
||||
* [Float] `Float.to_char_list/2` and `Float.to_string/2` are deprecated (use the :erlang functions if such conversions are desired)
|
||||
* [Kernel] Deprecate support for making private functions overridable. Overridable functions must always be public as they must be contracts
|
||||
* [Float] `Float.to_char_list/2` and `Float.to_string/2` are deprecated (use the `:erlang.float_to_list/2` and `:erlang.float_to_binary/2` functions if such conversions are desired)
|
||||
* [HashDict] The `HashDict` module is deprecated, in favour of the `Map`
|
||||
* [HashSet] The `HashDict` module is deprecated, in favour of the `MapSet`
|
||||
* [Kernel] Deprecate support for making private functions overridable. Overridable functions must always be public as they must be contracts
|
||||
* [Kernel] Anonymous functions with no expression after `->`, in favor of using an expression or returning `nil`
|
||||
* [Kernel] Warn if variable is used as a function call
|
||||
* [OptionParser] Deprecate aliases with multiple letters, such as `-abc`
|
||||
* [Set] Deprecate the `Set` module
|
||||
* [Set] Deprecation of the `Set` module in favor of `MapSet`
|
||||
* [Stream] Deprecate `Stream.uniq/2` in favor of `Stream.uniq_by/2`
|
||||
|
||||
#### IEx
|
||||
@@ -285,7 +357,7 @@ It is also possible to install escripts and archives by providing a Git/GitHub r
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix.Utils] `underscore/1` and `camelize/1` are deprecated
|
||||
* [Mix.Utils] `underscore/1` and `camelize/1` are deprecated in favor of `Macro.underscore/1` and `Macro.camelize/1`
|
||||
|
||||
## v1.3
|
||||
|
||||
|
||||
@@ -240,6 +240,14 @@ defmodule Calendar.ISO do
|
||||
do: parse_offset(1, hour, min, rest)
|
||||
def parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(-1, hour, min, rest)
|
||||
def parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(1, hour, min, rest)
|
||||
def parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
|
||||
do: parse_offset(-1, hour, min, rest)
|
||||
def parse_offset(<<?+, hour::2-bytes, rest::binary>>),
|
||||
do: parse_offset(1, hour, "00", rest)
|
||||
def parse_offset(<<?-, hour::2-bytes, rest::binary>>),
|
||||
do: parse_offset(-1, hour, "00", rest)
|
||||
def parse_offset(_),
|
||||
do: :error
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullets [?*, ?-, ?+]
|
||||
@spaces [" ", "\n", "\t"]
|
||||
|
||||
@doc """
|
||||
The default options used by this module.
|
||||
@@ -204,7 +205,7 @@ defmodule IO.ANSI.Docs do
|
||||
|> Enum.join(" ")
|
||||
|> handle_links
|
||||
|> handle_inline(options)
|
||||
|> String.split(~r{\s})
|
||||
|> String.split(@spaces)
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap)
|
||||
|
||||
unless no_wrap, do: newline_after_block()
|
||||
@@ -266,8 +267,10 @@ defmodule IO.ANSI.Docs do
|
||||
count = Enum.map(lines, &length/1) |> Enum.max
|
||||
lines = Enum.map(lines, &pad_to_number_of_columns(&1, count))
|
||||
|
||||
widths = for line <- lines, do:
|
||||
(for {_col, length} <- line, do: length)
|
||||
widths =
|
||||
for line <- lines do
|
||||
for {_col, length} <- line, do: length
|
||||
end
|
||||
|
||||
col_widths = Enum.reduce(widths,
|
||||
List.duplicate(0, count),
|
||||
@@ -280,15 +283,16 @@ defmodule IO.ANSI.Docs do
|
||||
line
|
||||
|> String.trim("|")
|
||||
|> String.trim()
|
||||
|> String.split(~r/\s\|\s/)
|
||||
|> String.split(" | ")
|
||||
|> Enum.map(&render_column(&1, options))
|
||||
end
|
||||
|
||||
defp render_column(col, options) do
|
||||
col = col
|
||||
|> String.replace(~r/\\ \|/x, "|")
|
||||
|> handle_links
|
||||
|> handle_inline(options)
|
||||
col =
|
||||
col
|
||||
|> String.replace("\\\|", "|")
|
||||
|> handle_links
|
||||
|> handle_inline(options)
|
||||
{col, length_without_escape(col, 0)}
|
||||
end
|
||||
|
||||
@@ -319,8 +323,17 @@ defmodule IO.ANSI.Docs do
|
||||
defp render_table([], _, _),
|
||||
do: nil
|
||||
|
||||
defp table_header?(row), do:
|
||||
Enum.all?(row, fn {col, _} -> col =~ ~r/^:?-+:?$/ end)
|
||||
defp table_header?(row) do
|
||||
Enum.all?(row, fn {col, _} -> table_header_column?(col) end)
|
||||
end
|
||||
|
||||
defp table_header_column?(":" <> row), do: table_header_contents?(row)
|
||||
defp table_header_column?(row), do: table_header_contents?(row)
|
||||
|
||||
defp table_header_contents?("-" <> row), do: table_header_contents?(row)
|
||||
defp table_header_contents?(":"), do: true
|
||||
defp table_header_contents?(""), do: true
|
||||
defp table_header_contents?(_), do: false
|
||||
|
||||
defp draw_table_row(cols_and_widths, options, heading \\ false) do
|
||||
columns =
|
||||
@@ -336,11 +349,7 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp table_line?(line) do
|
||||
Regex.match?(~r'''
|
||||
( ^ \s{0,3} \| (?: [^|]+ \|)+ \s* $ )
|
||||
|
|
||||
(\s \| \s)
|
||||
'''x, line)
|
||||
line =~ " | "
|
||||
end
|
||||
|
||||
## Helpers
|
||||
@@ -416,14 +425,17 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp escape_underlines_in_link(text) do
|
||||
Regex.replace(~r{https?\S*}, text, &String.replace(&1, "_", "\\_"))
|
||||
~r{https?\S*}
|
||||
|> Regex.recompile!
|
||||
|> Regex.replace(text, &String.replace(&1, "_", "\\_"))
|
||||
end
|
||||
|
||||
defp remove_square_brackets_in_link(text) do
|
||||
Regex.replace(~r{\[(.*?)\]\((.*?)\)}, text, "\\1 (\\2)")
|
||||
~r{\[(.*?)\]\((.*?)\)}
|
||||
|> Regex.recompile!
|
||||
|> Regex.replace(text, "\\1 (\\2)")
|
||||
end
|
||||
|
||||
|
||||
# We have four entries: **, *, _ and `.
|
||||
#
|
||||
# The first three behave the same while the last one is simpler
|
||||
|
||||
@@ -93,18 +93,14 @@ defmodule MapSet do
|
||||
end
|
||||
|
||||
defp do_new([], acc) do
|
||||
acc
|
||||
|> :lists.reverse
|
||||
|> :maps.from_list
|
||||
:maps.from_list(acc)
|
||||
end
|
||||
defp do_new([item | rest], acc) do
|
||||
do_new(rest, [{item, true} | acc])
|
||||
end
|
||||
|
||||
defp do_new_transform([], _fun, acc) do
|
||||
acc
|
||||
|> :lists.reverse
|
||||
|> :maps.from_list
|
||||
:maps.from_list(acc)
|
||||
end
|
||||
defp do_new_transform([item | rest], fun, acc) do
|
||||
do_new_transform(rest, fun, [{fun.(item), true} | acc])
|
||||
|
||||
@@ -316,6 +316,7 @@ defmodule Record do
|
||||
Keyword.keyword?(args) ->
|
||||
create(atom, fields, args, caller)
|
||||
true ->
|
||||
fields = Macro.escape(fields)
|
||||
case Macro.expand(args, caller) do
|
||||
{:{}, _, [^atom | list]} when length(list) == length(fields) ->
|
||||
record = List.to_tuple([atom | list])
|
||||
|
||||
+98
-23
@@ -1,14 +1,13 @@
|
||||
defmodule Regex do
|
||||
@moduledoc ~S"""
|
||||
Provides regular expressions for Elixir. Built on top of Erlang's `:re`
|
||||
module.
|
||||
Provides regular expressions for Elixir.
|
||||
|
||||
As the `:re` module, Regex is based on PCRE
|
||||
(Perl Compatible Regular Expressions). More information can be
|
||||
found in the [`:re` module documentation](http://www.erlang.org/doc/man/re.html).
|
||||
Regex is based on PCRE (Perl Compatible Regular Expressions) and
|
||||
built on top of Erlang's `:re` module. More information can be found
|
||||
in the [`:re` module documentation](http://www.erlang.org/doc/man/re.html).
|
||||
|
||||
Regular expressions in Elixir can be created using `Regex.compile!/2`
|
||||
or using the special form with [`~r`](Kernel.html#sigil_r/2) or [`~R`](Kernel.html#sigil_R/2):
|
||||
Regular expressions in Elixir can be created using the sigils
|
||||
[`~r`](Kernel.html#sigil_r/2) or [`~R`](Kernel.html#sigil_R/2):
|
||||
|
||||
# A simple regular expressions that matches foo anywhere in the string
|
||||
~r/foo/
|
||||
@@ -16,9 +15,38 @@ defmodule Regex do
|
||||
# A regular expression with case insensitive and Unicode options
|
||||
~r/foo/iu
|
||||
|
||||
Regular expressions created via sigils are pre-compiled and stored
|
||||
in the `.beam` file. Notice this may be a problem if you are precompiling
|
||||
Elixir, see the "Precompilation" section for more information.
|
||||
|
||||
A Regex is represented internally as the `Regex` struct. Therefore,
|
||||
`%Regex{}` can be used whenever there is a need to match on them.
|
||||
|
||||
Keep in mind it is not guaranteed two regular expressions from the
|
||||
same source are equal, for example:
|
||||
|
||||
~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/
|
||||
|
||||
may return `true` or `false` depending on your machine, endianess,
|
||||
available optimizations and others. You can, however, retrieve the source
|
||||
of a compiled regular expression by accessing the `source` field, and then
|
||||
compare those directly:
|
||||
|
||||
~r/(?<foo>.)(?<bar>.)/.source == ~r/(?<foo>.)(?<bar>.)/.source
|
||||
|
||||
## Precompilation
|
||||
|
||||
Regular expressions built with sigil are precompiled and stored in `.beam`
|
||||
files. This may be a problem if you are precompiling Elixir to run in
|
||||
different OTP releases, as OTP releases may update the underlying regular
|
||||
expression engine at any time.
|
||||
|
||||
For such reasons, we always recomend precompiling Elixir projects using
|
||||
the OTP version meant to run in production. In case cross-compilation is
|
||||
really necessary, you can manually invoke `Regex.recompile/1` or `Regex.
|
||||
recompile!/1` to perform a runtime version check and recompile the regex
|
||||
if necessary.
|
||||
|
||||
## Modifiers
|
||||
|
||||
The modifiers available when creating a Regex are:
|
||||
@@ -78,7 +106,7 @@ defmodule Regex do
|
||||
|
||||
"""
|
||||
|
||||
defstruct re_pattern: nil, source: "", opts: ""
|
||||
defstruct re_pattern: nil, source: "", opts: "", re_version: ""
|
||||
|
||||
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary}
|
||||
|
||||
@@ -91,7 +119,7 @@ defmodule Regex do
|
||||
|
||||
The given options can either be a binary with the characters
|
||||
representing the same regex options given to the `~r` sigil,
|
||||
or a list of options, as expected by the Erlang's [`:re` module](http://www.erlang.org/doc/man/re.html).
|
||||
or a list of options, as expected by the Erlang's `:re` module.
|
||||
|
||||
It returns `{:ok, regex}` in case of success,
|
||||
`{:error, reason}` otherwise.
|
||||
@@ -106,40 +134,84 @@ defmodule Regex do
|
||||
|
||||
"""
|
||||
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
|
||||
def compile(source, options \\ "")
|
||||
def compile(source, options \\ "") do
|
||||
compile(source, options, version())
|
||||
end
|
||||
|
||||
def compile(source, options) when is_binary(options) do
|
||||
defp compile(source, options, version) when is_binary(options) do
|
||||
case translate_options(options, []) do
|
||||
{:error, rest} ->
|
||||
{:error, {:invalid_option, rest}}
|
||||
|
||||
translated_options ->
|
||||
compile(source, translated_options, options)
|
||||
compile(source, translated_options, options, version)
|
||||
end
|
||||
end
|
||||
|
||||
def compile(source, options) when is_list(options) do
|
||||
compile(source, options, "")
|
||||
defp compile(source, options, version) when is_list(options) do
|
||||
compile(source, options, "", version)
|
||||
end
|
||||
|
||||
defp compile(source, opts, doc_opts) when is_binary(source) do
|
||||
defp compile(source, opts, doc_opts, version) when is_binary(source) do
|
||||
case :re.compile(source, opts) do
|
||||
{:ok, re_pattern} ->
|
||||
{:ok, %Regex{re_pattern: re_pattern, source: source, opts: doc_opts}}
|
||||
{:ok, %Regex{re_pattern: re_pattern, re_version: version, source: source, opts: doc_opts}}
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the regular expression according to the given options.
|
||||
Fails with `Regex.CompileError` if the regex cannot be compiled.
|
||||
Compiles the regular expression and raises `Regex.CompileError` in case of errors.
|
||||
"""
|
||||
@spec compile!(binary, binary | [term]) :: t
|
||||
def compile!(source, options \\ "") do
|
||||
case compile(source, options) do
|
||||
{:ok, regex} -> regex
|
||||
{:error, {reason, at}} -> raise Regex.CompileError, message: "#{reason} at position #{at}"
|
||||
{:error, {reason, at}} -> raise Regex.CompileError, "#{reason} at position #{at}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recompiles the existing regular expression if necessary.
|
||||
|
||||
This checks the version stored in the regular expression
|
||||
and recompiles the regex in case of version mismatch.
|
||||
"""
|
||||
@spec recompile(t) :: t
|
||||
def recompile(%Regex{} = regex) do
|
||||
version = version()
|
||||
|
||||
# We use Map.get/3 by choice to support old regexes versions.
|
||||
case Map.get(regex, :re_version, :error) do
|
||||
^version ->
|
||||
{:ok, regex}
|
||||
_ ->
|
||||
%{source: source, opts: opts} = regex
|
||||
compile(source, opts, version)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
|
||||
"""
|
||||
@spec recompile!(t) :: t
|
||||
def recompile!(regex) do
|
||||
case recompile(regex) do
|
||||
{:ok, regex} -> regex
|
||||
{:error, {reason, at}} -> raise Regex.CompileError, "#{reason} at position #{at}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the version of the underlying Regex engine.
|
||||
"""
|
||||
# TODO: No longer check for function_exported? on OTP 20+.
|
||||
def version do
|
||||
if function_exported?(:re, :version, 0) do
|
||||
:re.version()
|
||||
else
|
||||
"8.33 2013-05-29"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -629,9 +701,6 @@ defmodule Regex do
|
||||
[get_index(string, h) | get_indexes(string, t, arity - 1)]
|
||||
end
|
||||
|
||||
{:ok, pattern} = :re.compile(~S"[.^$*+?()\[\]{}\\\|\s#-]", [:unicode])
|
||||
@escape_pattern pattern
|
||||
|
||||
@doc ~S"""
|
||||
Escapes a string to be literally matched in a regex.
|
||||
|
||||
@@ -646,9 +715,15 @@ defmodule Regex do
|
||||
"""
|
||||
@spec escape(String.t) :: String.t
|
||||
def escape(string) when is_binary(string) do
|
||||
:re.replace(string, @escape_pattern, "\\\\&", [:global, {:return, :binary}])
|
||||
escape(string, [])
|
||||
end
|
||||
|
||||
for char <- '.^$*+?()[]{}|#-\\\t\n\v\f\r\s' do
|
||||
defp escape(<<unquote(char), rest::binary>>, acc), do: escape(rest, [acc, ?\\, unquote(char)])
|
||||
end
|
||||
defp escape(<<char, rest::binary>>, acc), do: escape(rest, [acc, char])
|
||||
defp escape(<<>>, acc), do: IO.iodata_to_binary(acc)
|
||||
|
||||
# Helpers
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -359,11 +359,13 @@ defmodule Registry do
|
||||
end
|
||||
defp dispatch_task(registry, key, mfa_or_fun, partition) do
|
||||
partition = partition - 1
|
||||
parent = self()
|
||||
task = Task.async(fn ->
|
||||
registry
|
||||
|> key_ets!(partition)
|
||||
|> safe_lookup_second(key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
Process.unlink(parent)
|
||||
:ok
|
||||
end)
|
||||
[task | dispatch_task(registry, key, mfa_or_fun, partition)]
|
||||
|
||||
@@ -400,8 +400,8 @@ defmodule URI do
|
||||
def parse(%URI{} = uri), do: uri
|
||||
|
||||
def parse(string) when is_binary(string) do
|
||||
# From http://tools.ietf.org/html/rfc3986#appendix-B
|
||||
regex = ~r/^(([a-z][a-z0-9\+\-\.]*):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/i
|
||||
# From https://tools.ietf.org/html/rfc3986#appendix-B
|
||||
regex = Regex.recompile!(~r/^(([a-z][a-z0-9\+\-\.]*):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/i)
|
||||
parts = nillify(Regex.run(regex, string))
|
||||
|
||||
destructure [_, _, scheme, _, authority, path, _, query, _, fragment], parts
|
||||
@@ -419,7 +419,8 @@ defmodule URI do
|
||||
|
||||
# Split an authority into its userinfo, host and port parts.
|
||||
defp split_authority(string) do
|
||||
components = Regex.run(~r/(^(.*)@)?(\[[a-zA-Z0-9:.]*\]|[^:]*)(:(\d*))?/, string || "")
|
||||
regex = Regex.recompile!(~r/(^(.*)@)?(\[[a-zA-Z0-9:.]*\]|[^:]*)(:(\d*))?/)
|
||||
components = Regex.run(regex, string || "")
|
||||
|
||||
destructure [_, _, userinfo, host, _, port], nillify(components)
|
||||
host = if host, do: host |> String.trim_leading("[") |> String.trim_trailing("]")
|
||||
@@ -473,6 +474,9 @@ defmodule URI do
|
||||
def merge(_base, %URI{scheme: rel_scheme} = rel) when rel_scheme != nil do
|
||||
rel
|
||||
end
|
||||
def merge(base, %URI{authority: authority} = rel) when authority != nil do
|
||||
%{rel | scheme: base.scheme}
|
||||
end
|
||||
def merge(%URI{} = base, %URI{path: rel_path} = rel) when rel_path in ["", nil] do
|
||||
%{base | query: rel.query || base.query, fragment: rel.fragment}
|
||||
end
|
||||
|
||||
+70
-54
@@ -238,9 +238,10 @@ defmodule Version do
|
||||
@spec parse(String.t) :: {:ok, t} | :error
|
||||
def parse(string) when is_binary(string) do
|
||||
case Version.Parser.parse_version(string) do
|
||||
{:ok, {major, minor, patch, pre}} ->
|
||||
{:ok, {major, minor, patch, pre, build_parts}} ->
|
||||
build = if build_parts == [], do: nil, else: Enum.join(build_parts, "")
|
||||
version = %Version{major: major, minor: minor, patch: patch,
|
||||
pre: pre, build: get_build(string)}
|
||||
pre: pre, build: build}
|
||||
{:ok, version}
|
||||
:error ->
|
||||
:error
|
||||
@@ -312,23 +313,13 @@ defmodule Version do
|
||||
|
||||
defp to_matchable(string, allow_pre?) do
|
||||
case Version.Parser.parse_version(string) do
|
||||
{:ok, {major, minor, patch, pre}} ->
|
||||
{:ok, {major, minor, patch, pre, _build_parts}} ->
|
||||
{major, minor, patch, pre, allow_pre?}
|
||||
:error ->
|
||||
raise InvalidVersionError, message: string
|
||||
end
|
||||
end
|
||||
|
||||
defp get_build(string) do
|
||||
case Regex.run(~r/\+([^\s]+)$/, string) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
[_, build] ->
|
||||
build
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Parser.DSL do
|
||||
@moduledoc false
|
||||
|
||||
@@ -396,67 +387,92 @@ defmodule Version do
|
||||
Enum.filter(Enum.reverse(acc), &(&1 != :' '))
|
||||
end
|
||||
|
||||
@version_regex ~r/^
|
||||
(\d+) # major
|
||||
(?:\.(\d+))? # minor
|
||||
(?:\.(\d+))? # patch
|
||||
(?:\-([\d\w\.\-]+))? # pre
|
||||
(?:\+([\d\w\.\-]+))? # build
|
||||
$/x
|
||||
|
||||
@spec parse_requirement(String.t) :: {:ok, term} | :error
|
||||
def parse_requirement(source) do
|
||||
lexed = lexer(source, [])
|
||||
to_matchspec(lexed)
|
||||
end
|
||||
|
||||
defp nillify(""), do: nil
|
||||
defp nillify(o), do: o
|
||||
|
||||
@spec parse_version(String.t) :: {:ok, Version.matchable} | :error
|
||||
def parse_version(string, approximate? \\ false) when is_binary(string) do
|
||||
if parsed = Regex.run(@version_regex, string) do
|
||||
destructure [_, major, minor, patch, pre], parsed
|
||||
patch = nillify(patch)
|
||||
pre = nillify(pre)
|
||||
destructure [version_with_pre, build], String.split(string, "+", parts: 2)
|
||||
destructure [version, pre], String.split(version_with_pre, "-", parts: 2)
|
||||
destructure [major, minor, patch], String.split(version, ".")
|
||||
|
||||
if is_nil(minor) or (is_nil(patch) and not approximate?) do
|
||||
:error
|
||||
else
|
||||
major = String.to_integer(major)
|
||||
minor = String.to_integer(minor)
|
||||
patch = patch && String.to_integer(patch)
|
||||
with {:ok, major} <- require_digits(major),
|
||||
{:ok, minor} <- require_digits(minor),
|
||||
{:ok, patch} <- maybe_patch(patch, approximate?),
|
||||
{:ok, pre_parts} <- optional_dot_separated(pre),
|
||||
{:ok, pre_parts} <- convert_parts_to_integer(pre_parts, []),
|
||||
{:ok, build_parts} <- optional_dot_separated(build) do
|
||||
{:ok, {major, minor, patch, pre_parts, build_parts}}
|
||||
else
|
||||
_other -> :error
|
||||
end
|
||||
end
|
||||
|
||||
case parse_pre(pre) do
|
||||
{:ok, pre} ->
|
||||
{:ok, {major, minor, patch, pre}}
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
defp require_digits(nil), do: :error
|
||||
defp require_digits(string) do
|
||||
if leading_zero?(string), do: :error, else: parse_digits(string, "")
|
||||
end
|
||||
|
||||
defp leading_zero?(<<?0, _, _::binary>>), do: true
|
||||
defp leading_zero?(_), do: false
|
||||
|
||||
defp parse_digits(<<char, rest::binary>>, acc) when char in ?0..?9,
|
||||
do: parse_digits(rest, <<acc::binary, char>>)
|
||||
defp parse_digits(<<>>, acc) when byte_size(acc) > 0,
|
||||
do: {:ok, String.to_integer(acc)}
|
||||
defp parse_digits(_, _acc),
|
||||
do: :error
|
||||
|
||||
defp maybe_patch(patch, approximate?)
|
||||
defp maybe_patch(nil, true), do: {:ok, nil}
|
||||
defp maybe_patch(patch, _), do: require_digits(patch)
|
||||
|
||||
defp optional_dot_separated(nil), do: {:ok, []}
|
||||
defp optional_dot_separated(string) do
|
||||
parts = String.split(string, ".")
|
||||
if Enum.all?(parts, &(&1 != "" and valid_identifier?(&1))) do
|
||||
{:ok, parts}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_pre(nil), do: {:ok, []}
|
||||
defp parse_pre(pre), do: parse_pre(String.split(pre, "."), [])
|
||||
|
||||
defp parse_pre([piece | t], acc) do
|
||||
cond do
|
||||
piece =~ ~r/^(0|[1-9][0-9]*)$/ ->
|
||||
parse_pre(t, [String.to_integer(piece) | acc])
|
||||
piece =~ ~r/^[0-9]*$/ ->
|
||||
:error
|
||||
true ->
|
||||
parse_pre(t, [piece | acc])
|
||||
defp convert_parts_to_integer([part | rest], acc) do
|
||||
case parse_digits(part, "") do
|
||||
{:ok, integer} ->
|
||||
if leading_zero?(part) do
|
||||
:error
|
||||
else
|
||||
convert_parts_to_integer(rest, [integer | acc])
|
||||
end
|
||||
:error ->
|
||||
convert_parts_to_integer(rest, [part | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_pre([], acc) do
|
||||
defp convert_parts_to_integer([], acc) do
|
||||
{:ok, Enum.reverse(acc)}
|
||||
end
|
||||
|
||||
defp valid_identifier?(<<char, rest::binary>>)
|
||||
when char in ?0..?9
|
||||
when char in ?a..?z
|
||||
when char in ?A..?Z
|
||||
when char == ?- do
|
||||
valid_identifier?(rest)
|
||||
end
|
||||
|
||||
defp valid_identifier?(<<>>) do
|
||||
true
|
||||
end
|
||||
|
||||
defp valid_identifier?(_other) do
|
||||
false
|
||||
end
|
||||
|
||||
defp valid_requirement?([]), do: false
|
||||
defp valid_requirement?([a | next]), do: valid_requirement?(a, next)
|
||||
|
||||
@@ -574,7 +590,7 @@ defmodule Version do
|
||||
|
||||
defp parse_condition(version, approximate? \\ false) do
|
||||
case parse_version(version, approximate?) do
|
||||
{:ok, version} -> version
|
||||
{:ok, {major, minor, patch, pre, _build}} -> {major, minor, patch, pre}
|
||||
:error -> throw :invalid_matchspec
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,10 @@ Elixir treats documentation as a first-class citizen. This means documentation s
|
||||
|
||||
## Markdown
|
||||
|
||||
Elixir documentation is written using Markdown. There are plenty of guides on Markdown online, we recommend the ones available at GitHub as a getting started point:
|
||||
Elixir documentation is written using Markdown. There are plenty of guides on Markdown online, we recommend the ones available on GitHub as a getting started point:
|
||||
|
||||
* https://help.github.com/articles/markdown-basics/
|
||||
* https://help.github.com/articles/github-flavored-markdown/
|
||||
* [Basic writing and formatting syntax](https://help.github.com/articles/basic-writing-and-formatting-syntax/)
|
||||
* [Mastering Markdown](https://guides.github.com/features/mastering-markdown/)
|
||||
|
||||
## Module Attributes
|
||||
|
||||
@@ -34,7 +34,7 @@ Documentation in Elixir is usually attached to module attributes. Let's see an e
|
||||
end
|
||||
end
|
||||
|
||||
The `@moduledoc` attribute is used to add documentation to the module. `@doc` is used before a function to provide documentation for it. Besides the attributes above, `@typedoc` can also be used to attach documentation to types defined as part of typespecs.
|
||||
The `@moduledoc` attribute is used to add documentation to the module. `@doc` is used before a function to provide documentation for the function that follows. Besides the attributes above, `@typedoc` can also be used to attach documentation to types defined as part of typespecs.
|
||||
|
||||
## Function Arguments
|
||||
|
||||
@@ -55,11 +55,11 @@ The compiler will infer this argument as `map`. Sometimes the inference will be
|
||||
|
||||
When writing documentation:
|
||||
|
||||
* Keep the first paragraph of the documentation concise and simple, typically one-line. Tools like [ExDoc](https://github.com/elixir-lang/ex_doc/) use the first line to generate a summary.
|
||||
* Keep the first paragraph of the documentation concise and simple, typically one line. Tools like [ExDoc](https://github.com/elixir-lang/ex_doc/) use the first line to generate a summary.
|
||||
|
||||
* Reference modules by their full name.
|
||||
|
||||
Markdown uses backticks (`` ` ``) to quote code. Elixir builds on top of that to automatically generate links when module or function names are referenced. For this reason, always use full module names. If you have a module called `MyApp.Hello`, always reference it as `` `MyApp.Hello` `` and never as `` `Hello` ``.
|
||||
Markdown uses backticks (`` ` ``) to quote code. Elixir builds on top of that to automatically generate links when module or function names are referenced. For this reason, always use full module names. If you have a module called `MyApp.Hello`, always reference it as `` `MyApp.Hello` `` and never as `` `Hello` ``.
|
||||
|
||||
* Reference functions by name and arity if they are local, as in `` `world/1` ``, or by module, name and arity if pointing to an external module: `` `MyApp.Hello.world/1` ``.
|
||||
|
||||
@@ -73,21 +73,21 @@ When writing documentation:
|
||||
|
||||
## Doctests
|
||||
|
||||
We recommend that developers include examples in their documentation, often under their own `## Examples` heading. To ensure examples do not get out of date, Elixir's test framework (ExUnit) provides a feature called doctests that allows developers to test the examples in their documentation. Doctests work by parsing out code samples starting with `iex>` from the documentation. You can read more about it at `ExUnit.DocTest`.
|
||||
We recommend that developers include examples in their documentation, often under their own `## Examples` heading. To ensure examples do not get out of date, Elixir's test framework (ExUnit) provides a feature called doctests that allows developers to test the examples in their documentation. Doctests work by parsing out code samples starting with `iex>` from the documentation. You can read more about it in the documentation for `ExUnit.DocTest`.
|
||||
|
||||
Notice doctests have limitations. When you cannot doctest a function, because it relies on state or side-effects, we recommend developers include examples directly without the `iex>` prompt.
|
||||
Note that doctests have limitations. When you cannot doctest a function, because it relies on state or side-effects, we recommend developers include examples directly without the `iex>` prompt.
|
||||
|
||||
## Documentation != Comments
|
||||
## Documentation is not Comments
|
||||
|
||||
Elixir treats documentation and code comments as different concepts. Documentation is for users of your Application Programming Interface (API), be it your co-worker or your future self. Modules and functions must always be documented if they are part of your API.
|
||||
|
||||
Code comments are for developers reading the code. They are useful to mark improvements, leave notes for developers reading the code (for example, you decided not to call a function due to a bug in a library) and so forth.
|
||||
Code comments are for developers reading the code. They are useful to mark improvements, leave notes for developers reading the code (for example, you decided not to call a function due to a bug in a library), and so on.
|
||||
|
||||
In other words: documentation is required, code comments are optional.
|
||||
|
||||
## Hiding Internal Modules and Functions
|
||||
|
||||
Besides the modules and functions libraries provide as part of their public interface, libraries may also implement important functionality that is not part of their API. While these modules and functions can be accessed, they are meant to be internal to the library and thus should not have documentation for end users.
|
||||
Besides the modules and functions that libraries provide as part of their public interface, libraries may also implement important functionality that is not part of their API. While these modules and functions can be accessed, they are meant to be internal to the library and thus should not have documentation for end users.
|
||||
|
||||
Luckily, Elixir allows developers to hide modules and functions from the documentation. For example, one common practice for documenting internal behaviour is to set the `@moduledoc` attribute to `false` while documenting each function:
|
||||
|
||||
@@ -102,7 +102,7 @@ Luckily, Elixir allows developers to hide modules and functions from the documen
|
||||
end
|
||||
end
|
||||
|
||||
Similarly, developers can add `@doc false` to functions they do not want to be publicly exposed:
|
||||
Similarly, developers can add `@doc false` to functions they do not want to show up in the documentation:
|
||||
|
||||
defmodule MyApp.Sample do
|
||||
@doc false
|
||||
@@ -113,7 +113,7 @@ However, keep in mind that adding `@doc false` does not make the function privat
|
||||
|
||||
* Move the undocumented function to a module with `@moduledoc false`, like `MyApp.Hidden`, ensuring the function won't be accidentally exposed or imported. Remember you can use `@moduledoc false` to hide a whole module and still document each function with `@doc`. Tools will still ignore the module.
|
||||
|
||||
* Start the function name with one or two underscores, for example, `__add__/2`, and add `@doc false`. The compiler does not import functions with leading underscores and they hint to anyone reading the code of their intended private usage.
|
||||
* Start the function name with one or two underscores, for example, `__add__/2`, and add `@doc false`. The compiler does not import functions with leading underscores and they suggest to anyone reading the code that they're meant to be used privately.
|
||||
|
||||
## Documenting Private Functions
|
||||
|
||||
@@ -121,6 +121,6 @@ Elixir warns if a private function has a `@doc` attribute and discards its conte
|
||||
|
||||
Private functions may still need internal documentation for maintainers, though. That can be accomplished with code comments.
|
||||
|
||||
## Code.get_docs/2
|
||||
## Retrieving Documentation
|
||||
|
||||
Elixir stores documentation inside pre-defined chunks in the bytecode. It can be accessed from Elixir by using the `Code.get_docs/2` function. This also means documentation is only accessed when required and not when modules are loaded by the Virtual Machine. The only downside is that modules defined in-memory, like the ones defined in IEx, cannot have their documentation accessed as they do not have their bytecode written to disk.
|
||||
|
||||
@@ -47,6 +47,11 @@ start(_Type, _Args) ->
|
||||
erlang:halt(1)
|
||||
end,
|
||||
|
||||
%% We need to make sure the re module is preloaded
|
||||
%% to make function_exported checks on it fast.
|
||||
%% TODO: Remove this once we support OTP 20+.
|
||||
_ = code:ensure_loaded(re),
|
||||
|
||||
case code:ensure_loaded(?system) of
|
||||
{module, ?system} ->
|
||||
Endianness = ?system:endianness(),
|
||||
|
||||
@@ -271,11 +271,11 @@ export(Kind, {Name, Arity}) when Kind == def; Kind == defp ->
|
||||
function_for_stored_definition(Ann, {Name, Arity}, Clauses) ->
|
||||
{function, Ann, Name, Arity, Clauses}.
|
||||
|
||||
add_definition(_Line, nil, Body, {Head, Tail}) ->
|
||||
add_definition(_Ann, nil, Body, {Head, Tail}) ->
|
||||
{[Body | Head], Tail};
|
||||
add_definition(Line, Location, Body, {Head, Tail}) ->
|
||||
{Head,
|
||||
[{attribute, Line, file, Location}, Body | Tail]}.
|
||||
add_definition(Ann, Location, Body, {Head, Tail}) ->
|
||||
FileAnn = erl_anno:set_generated(false, Ann),
|
||||
{Head, [{attribute, FileAnn, file, Location}, Body | Tail]}.
|
||||
|
||||
default_function_for(Kind, Name, {clause, Ann, Args, _Guards, _Exprs} = Clause)
|
||||
when Kind == defmacro; Kind == defmacrop ->
|
||||
|
||||
@@ -140,6 +140,11 @@ handle_file_warning(_, _File, {_Line, v3_kernel, bad_call}) -> ok;
|
||||
%% We handle unused local warnings ourselves
|
||||
handle_file_warning(_, _File, {_Line, erl_lint, {unused_function, _}}) -> ok;
|
||||
|
||||
%% Properly format keys using inspect.
|
||||
handle_file_warning(_, File, {Line, v3_core, {map_key_repeated, Key}}) ->
|
||||
Message = io_lib:format("key ~ts will be overridden in map", ['Elixir.Kernel':inspect(Key)]),
|
||||
warn(Line, File, Message);
|
||||
|
||||
%% Make no_effect clauses pretty
|
||||
handle_file_warning(_, File, {Line, sys_core_fold, {no_effect, {erlang, F, A}}}) ->
|
||||
{Fmt, Args} = case erl_internal:comp_op(F, A) of
|
||||
|
||||
@@ -12,8 +12,9 @@ expand_map(Meta, Args, E) ->
|
||||
validate_kv(Meta, EArgs, Args, E),
|
||||
{{'%{}', Meta, EArgs}, EA}.
|
||||
|
||||
expand_struct(Meta, Left, Right, #{context := Context} = E) ->
|
||||
{[ELeft, ERight], EE} = elixir_exp:expand_args([Left, Right], E),
|
||||
expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, #{context := Context} = E) ->
|
||||
CleanArgs = clean_struct_key_from_map_args(Meta, MapArgs, E),
|
||||
{[ELeft, ERight], EE} = elixir_exp:expand_args([Left, {'%{}', MapMeta, CleanArgs}], E),
|
||||
|
||||
case validate_struct(ELeft, Context) of
|
||||
true when is_atom(ELeft) ->
|
||||
@@ -38,14 +39,24 @@ expand_struct(Meta, Left, Right, #{context := Context} = E) ->
|
||||
false -> Meta
|
||||
end,
|
||||
|
||||
case ERight of
|
||||
{'%{}', _, _} -> ok;
|
||||
_ -> compile_error(Meta, ?m(E, file),
|
||||
"expected struct to be followed by a map, got: ~ts",
|
||||
['Elixir.Macro':to_string(ERight)])
|
||||
end,
|
||||
{{'%', EMeta, [ELeft, ERight]}, EE};
|
||||
expand_struct(Meta, _Left, Right, E) ->
|
||||
compile_error(Meta, ?m(E, file), "expected struct to be followed by a map, got: ~ts",
|
||||
['Elixir.Macro':to_string(Right)]).
|
||||
|
||||
{{'%', EMeta, [ELeft, ERight]}, EE}.
|
||||
clean_struct_key_from_map_args(Meta, [{'|', PipeMeta, [Left, MapAssocs]}], E) ->
|
||||
[{'|', PipeMeta, [Left, clean_struct_key_from_map_assocs(Meta, MapAssocs, E)]}];
|
||||
clean_struct_key_from_map_args(Meta, MapAssocs, E) ->
|
||||
clean_struct_key_from_map_assocs(Meta, MapAssocs, E).
|
||||
|
||||
clean_struct_key_from_map_assocs(Meta, Assocs, E) ->
|
||||
case lists:keytake('__struct__', 1, Assocs) of
|
||||
{value, _, CleanAssocs} ->
|
||||
elixir_errors:warn(?line(Meta), ?m(E, file), "key :__struct__ is ignored when using structs"),
|
||||
CleanAssocs;
|
||||
false ->
|
||||
Assocs
|
||||
end.
|
||||
|
||||
validate_struct({'^', _, [{Var, _, Ctx}]}, match) when is_atom(Var), is_atom(Ctx) -> true;
|
||||
validate_struct({Var, _Meta, Ctx}, match) when is_atom(Var), is_atom(Ctx) -> true;
|
||||
@@ -66,7 +77,7 @@ translate_map(Meta, Args, S) ->
|
||||
translate_map(Meta, Assocs, TUpdate, US).
|
||||
|
||||
translate_struct(_Meta, Name, {'%{}', MapMeta, Assocs}, S) when is_tuple(Name) ->
|
||||
translate_map(MapMeta, Assocs ++ [{'__struct__', Name}], nil, S);
|
||||
translate_map(MapMeta, [{'__struct__', Name} | Assocs], nil, S);
|
||||
|
||||
translate_struct(Meta, Name, {'%{}', MapMeta, Args}, S) ->
|
||||
{Assocs, TUpdate, US} = extract_assoc_update(Args, S),
|
||||
@@ -98,11 +109,11 @@ translate_struct(Meta, Name, {'%{}', MapMeta, Args}, S) ->
|
||||
{clause, Generated, [Var], [], [elixir_utils:erl_call(Ann, erlang, error, [Error])]}
|
||||
]}, TS};
|
||||
match ->
|
||||
translate_map(MapMeta, Assocs ++ [{'__struct__', Name}], nil, US);
|
||||
translate_map(MapMeta, [{'__struct__', Name} | Assocs], nil, US);
|
||||
expand ->
|
||||
Keys = [K || {K, _} <- Assocs],
|
||||
Keys = ['__struct__'] ++ [K || {K, _} <- Assocs],
|
||||
{StructAssocs, _} = elixir_quote:escape(maps:to_list(maps:without(Keys, Struct)), false),
|
||||
translate_map(MapMeta, StructAssocs ++ Assocs ++ [{'__struct__', Name}], nil, US)
|
||||
translate_map(MapMeta, [{'__struct__', Name}] ++ StructAssocs ++ Assocs, nil, US)
|
||||
end.
|
||||
|
||||
%% Helpers
|
||||
|
||||
@@ -141,7 +141,7 @@ build(Line, File, Module, Docs, Lexical) ->
|
||||
end,
|
||||
|
||||
Data = ets:new(Module, [set, public]),
|
||||
Defs = ets:new(Module, [bag, public]),
|
||||
Defs = ets:new(Module, [duplicate_bag, public]),
|
||||
Ref = elixir_code_server:call({defmodule, self(),
|
||||
{Module, Data, Defs, Line, File}}),
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ do_quote(BitString, #elixir_quote{escape=true} = Q, _) when is_bitstring(BitStri
|
||||
{BitString, Q};
|
||||
Size ->
|
||||
<<Bits:Size, Bytes/binary>> = BitString,
|
||||
{{'<<>>', [], [{'::', [], [Bits, Size]}, Bytes]}, Q}
|
||||
{{'<<>>', [], [{'::', [], [Bits, {size, [], [Size]}]}, {'::', [], [Bytes, {binary, [], []}]}]}, Q}
|
||||
end;
|
||||
|
||||
do_quote(Map, #elixir_quote{escape=true} = Q, E) when is_map(Map) ->
|
||||
|
||||
@@ -163,4 +163,36 @@ defmodule DateTimeTest do
|
||||
assert DateTime.compare(datetime1, datetime2) == :lt
|
||||
assert DateTime.compare(datetime2, datetime1) == :gt
|
||||
end
|
||||
|
||||
test "from_iso8601/1 with tz offsets" do
|
||||
assert DateTime.from_iso8601("2017-06-02T14:00:00+01:00") |> elem(1) ==
|
||||
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
|
||||
hour: 13, minute: 0, second: 0, microsecond: {0, 0},
|
||||
utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
|
||||
assert DateTime.from_iso8601("2017-06-02T14:00:00-04:00") |> elem(1) ==
|
||||
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
|
||||
hour: 18, minute: 0, second: 0, microsecond: {0, 0},
|
||||
utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
|
||||
assert DateTime.from_iso8601("2017-06-02T14:00:00+0100") |> elem(1) ==
|
||||
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
|
||||
hour: 13, minute: 0, second: 0, microsecond: {0, 0},
|
||||
utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
|
||||
assert DateTime.from_iso8601("2017-06-02T14:00:00-0400") |> elem(1) ==
|
||||
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
|
||||
hour: 18, minute: 0, second: 0, microsecond: {0, 0},
|
||||
utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
|
||||
assert DateTime.from_iso8601("2017-06-02T14:00:00+01") |> elem(1) ==
|
||||
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
|
||||
hour: 13, minute: 0, second: 0, microsecond: {0, 0},
|
||||
utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
|
||||
assert DateTime.from_iso8601("2017-06-02T14:00:00-04") |> elem(1) ==
|
||||
%DateTime{year: 2017, month: 6, day: 2, zone_abbr: "UTC",
|
||||
hour: 18, minute: 0, second: 0, microsecond: {0, 0},
|
||||
utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -263,25 +263,6 @@ defmodule ExceptionTest do
|
||||
assert Exception.format_exit(reason) |> String.starts_with?(expected_to_start_with)
|
||||
end
|
||||
|
||||
test "format_exit with call with exception" do
|
||||
Process.flag(:trap_exit, true)
|
||||
# Fake reason to prevent error_logger printing to stdout
|
||||
exit_reason = {%ArgumentError{}, [{:not_a_real_module, :function, 0, []}]}
|
||||
exit_fun = fn() -> receive do: (_ -> exit(exit_reason)) end
|
||||
reason =
|
||||
try do
|
||||
:gen_fsm.sync_send_event(spawn_link(exit_fun), :hello)
|
||||
catch
|
||||
:exit, reason -> reason
|
||||
end
|
||||
|
||||
formatted = Exception.format_exit(reason)
|
||||
assert formatted =~ ~r"exited in: :gen_fsm\.sync_send_event\(#PID<\d+\.\d+\.\d+>, :hello\)"
|
||||
assert formatted =~ ~r"\s{4}\*\* \(EXIT\) an exception was raised:\n"
|
||||
assert formatted =~ ~r"\s{8}\*\* \(ArgumentError\) argument error\n"
|
||||
assert formatted =~ ~r"\s{12}:not_a_real_module\.function/0"
|
||||
end
|
||||
|
||||
test "format_exit with nested calls" do
|
||||
Process.flag(:trap_exit, true)
|
||||
# Fake reason to prevent error_logger printing to stdout
|
||||
|
||||
@@ -373,6 +373,19 @@ defmodule Kernel.WarningTest do
|
||||
purge Sample
|
||||
end
|
||||
|
||||
# TODO: Remove this check once we depend only on 19
|
||||
if :erlang.system_info(:otp_release) >= '20' do
|
||||
test "duplicate map keys" do
|
||||
assert capture_err(fn ->
|
||||
defmodule DuplicateKeys do
|
||||
assert %{a: :b, a: :c} == %{a: :c}
|
||||
assert %{1 => 2, 1 => 3} == %{1 => 3}
|
||||
assert %{:a => :b, a: :c} == %{a: :c}
|
||||
end
|
||||
end) =~ "key :a will be overridden in map"
|
||||
end
|
||||
end
|
||||
|
||||
test "unused guard" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string """
|
||||
@@ -768,6 +781,27 @@ defmodule Kernel.WarningTest do
|
||||
purge Sample
|
||||
end
|
||||
|
||||
defmodule User do
|
||||
defstruct [:name]
|
||||
end
|
||||
|
||||
test ":__struct__ is ignored when using structs" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string """
|
||||
assert %Kernel.WarningTest.User{__struct__: Ignored, name: "joe"} ==
|
||||
%Kernel.WarningTest.User{name: "joe"}
|
||||
""", [], __ENV__
|
||||
end) =~ "key :__struct__ is ignored when using structs"
|
||||
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string """
|
||||
user = %Kernel.WarningTest.User{name: "meg"}
|
||||
assert %Kernel.WarningTest.User{user | __struct__: Ignored, name: "joe"} ==
|
||||
%Kernel.WarningTest.User{__struct__: Kernel.WarningTest.User, name: "joe"}
|
||||
""", [], __ENV__
|
||||
end) =~ "key :__struct__ is ignored when using structs"
|
||||
end
|
||||
|
||||
defp purge(list) when is_list(list) do
|
||||
Enum.each list, &purge/1
|
||||
end
|
||||
|
||||
@@ -43,7 +43,7 @@ defmodule MacroTest do
|
||||
end
|
||||
|
||||
test "escape handles bitstring" do
|
||||
assert {:<<>>, [], [{:::, [], [1, 4]}, ","]} == Macro.escape(<<300::12>>)
|
||||
assert {:<<>>, [], [{:::, [], [1, {:size, [], [4]}]}, {:::, [], [",", {:binary, [], []}]}]} == Macro.escape(<<300::12>>)
|
||||
end
|
||||
|
||||
test "escape works recursively" do
|
||||
|
||||
@@ -86,12 +86,6 @@ defmodule MapTest do
|
||||
assert %{1 => 2, a: :b,} == %{1 => 2, a: :b}
|
||||
end
|
||||
|
||||
test "maps with duplicate keys" do
|
||||
assert %{a: :b, a: :c} == %{a: :c}
|
||||
assert %{1 => 2, 1 => 3} == %{1 => 3}
|
||||
assert %{:a => :b, a: :c} == %{a: :c}
|
||||
end
|
||||
|
||||
test "update maps" do
|
||||
assert %{@sample | a: 3} == %{a: 3, b: 2}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ defmodule RecordTest do
|
||||
binary: <<1, 2, 3>>,
|
||||
charlist: 'abc'
|
||||
|
||||
test "records with literal defaults" do
|
||||
test "records with literal defaults and on-the-fly record" do
|
||||
assert defaults(defaults()) == [
|
||||
struct: ~D[2016-01-01],
|
||||
map: %{},
|
||||
@@ -163,6 +163,35 @@ defmodule RecordTest do
|
||||
assert defaults(defaults(), :charlist) == 'abc'
|
||||
end
|
||||
|
||||
test "records with literal defaults and record in a variable" do
|
||||
defaults = defaults()
|
||||
|
||||
assert defaults(defaults) == [
|
||||
struct: ~D[2016-01-01],
|
||||
map: %{},
|
||||
tuple_zero: {},
|
||||
tuple_one: {1},
|
||||
tuple_two: {1, 2},
|
||||
tuple_three: {1, 2, 3},
|
||||
list: [1, 2, 3],
|
||||
call: MapSet.new,
|
||||
string: "abc",
|
||||
binary: <<1, 2, 3>>,
|
||||
charlist: 'abc'
|
||||
]
|
||||
assert defaults(defaults, :struct) == ~D[2016-01-01]
|
||||
assert defaults(defaults, :map) == %{}
|
||||
assert defaults(defaults, :tuple_zero) == {}
|
||||
assert defaults(defaults, :tuple_one) == {1}
|
||||
assert defaults(defaults, :tuple_two) == {1, 2}
|
||||
assert defaults(defaults, :tuple_three) == {1, 2, 3}
|
||||
assert defaults(defaults, :list) == [1, 2, 3]
|
||||
assert defaults(defaults, :call) == MapSet.new
|
||||
assert defaults(defaults, :string) == "abc"
|
||||
assert defaults(defaults, :binary) == <<1, 2, 3>>
|
||||
assert defaults(defaults, :charlist) == 'abc'
|
||||
end
|
||||
|
||||
test "records with dynamic arguments" do
|
||||
record = file_info()
|
||||
assert file_info(record, :size) == :undefined
|
||||
|
||||
@@ -91,7 +91,55 @@ defmodule RegexTest do
|
||||
refute <<?<, 255, ?>>> =~ ~r/<.>/u
|
||||
end
|
||||
|
||||
test "names" do
|
||||
test "ungreedy" do
|
||||
assert Regex.run(~r/[\d ]+/, "1 2 3 4 5"), ["1 2 3 4 5"]
|
||||
assert Regex.run(~r/[\d ]?+/, "1 2 3 4 5"), ["1"]
|
||||
assert Regex.run(~r/[\d ]+/U, "1 2 3 4 5"), ["1"]
|
||||
end
|
||||
|
||||
test "regex?/1" do
|
||||
assert Regex.regex?(~r/foo/)
|
||||
refute Regex.regex?(0)
|
||||
end
|
||||
|
||||
test "compile/1" do
|
||||
{:ok, regex} = Regex.compile("foo")
|
||||
assert Regex.regex?(regex)
|
||||
assert {:error, _} = Regex.compile("*foo")
|
||||
assert {:error, _} = Regex.compile("foo", "y")
|
||||
assert {:error, _} = Regex.compile("foo", "uy")
|
||||
end
|
||||
|
||||
test "compile/1 with Erlang options" do
|
||||
{:ok, regex} = Regex.compile("foo\\sbar", [:dotall, {:newline, :anycrlf}])
|
||||
assert "foo\nbar" =~ regex
|
||||
end
|
||||
|
||||
test "compile!/1" do
|
||||
assert Regex.regex?(Regex.compile!("foo"))
|
||||
|
||||
assert_raise Regex.CompileError, ~r/position 0$/, fn ->
|
||||
Regex.compile!("*foo")
|
||||
end
|
||||
end
|
||||
|
||||
test "recompile/1" do
|
||||
new_regex = ~r/foo/
|
||||
{:ok, regex} = Regex.recompile(new_regex)
|
||||
assert Regex.regex?(regex)
|
||||
assert Regex.regex?(Regex.recompile!(new_regex))
|
||||
|
||||
old_regex = Map.delete(~r/foo/, :re_version)
|
||||
{:ok, regex} = Regex.recompile(old_regex)
|
||||
assert Regex.regex?(regex)
|
||||
assert Regex.regex?(Regex.recompile!(old_regex))
|
||||
end
|
||||
|
||||
test "opts/1" do
|
||||
assert Regex.opts(Regex.compile!("foo", "i")) == "i"
|
||||
end
|
||||
|
||||
test "names/1" do
|
||||
assert Regex.names(~r/(?<FOO>foo)/) == ["FOO"]
|
||||
end
|
||||
|
||||
@@ -219,12 +267,6 @@ defmodule RegexTest do
|
||||
assert Regex.replace(~r[a(b)c], "abcabc", fn "abc", "b" -> "ac" end, global: false) == "acabc"
|
||||
end
|
||||
|
||||
test "ungreedy" do
|
||||
assert Regex.run(~r/[\d ]+/, "1 2 3 4 5"), ["1 2 3 4 5"]
|
||||
assert Regex.run(~r/[\d ]?+/, "1 2 3 4 5"), ["1"]
|
||||
assert Regex.run(~r/[\d ]+/U, "1 2 3 4 5"), ["1"]
|
||||
end
|
||||
|
||||
test "escape" do
|
||||
assert matches_escaped?(".")
|
||||
refute matches_escaped?(".", "x")
|
||||
|
||||
@@ -207,6 +207,8 @@ defmodule RegistryTest do
|
||||
end
|
||||
|
||||
test "dispatches to multiple keys", %{registry: registry} do
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
assert Registry.dispatch(registry, "hello", fn _ ->
|
||||
raise "will never be invoked"
|
||||
end) == :ok
|
||||
@@ -230,6 +232,8 @@ defmodule RegistryTest do
|
||||
refute_received {:dispatch, :value1}
|
||||
refute_received {:dispatch, :value2}
|
||||
assert_received {:dispatch, :value3}
|
||||
|
||||
refute_received {:EXIT, _, _}
|
||||
end
|
||||
|
||||
test "allows process unregistering", %{registry: registry} do
|
||||
|
||||
@@ -238,6 +238,7 @@ defmodule URITest do
|
||||
end
|
||||
|
||||
assert URI.merge("http://google.com/foo", "http://example.com/baz") |> to_string == "http://example.com/baz"
|
||||
assert URI.merge("http://google.com/foo", "//example.com/baz") |> to_string == "http://example.com/baz"
|
||||
|
||||
assert URI.merge("http://example.com", URI.parse("/foo")) |> to_string == "http://example.com/foo"
|
||||
|
||||
|
||||
@@ -66,9 +66,17 @@ defmodule VersionTest do
|
||||
assert {:ok, %V{major: 1, minor: 4, patch: 5, pre: ["6-g3318bd5"]}} = V.parse("1.4.5-6-g3318bd5+ignore")
|
||||
|
||||
assert :error = V.parse("foobar")
|
||||
assert :error = V.parse("2.3")
|
||||
assert :error = V.parse("2")
|
||||
assert :error = V.parse("2.")
|
||||
assert :error = V.parse("2.3")
|
||||
assert :error = V.parse("2.3.")
|
||||
assert :error = V.parse("2.3.0-")
|
||||
assert :error = V.parse("2.3.0+")
|
||||
assert :error = V.parse("2.3.0-01")
|
||||
assert :error = V.parse("2.3.00-1")
|
||||
assert :error = V.parse("2.3.00")
|
||||
assert :error = V.parse("2.03.0")
|
||||
assert :error = V.parse("02.3.0")
|
||||
end
|
||||
|
||||
test "to_string" do
|
||||
|
||||
@@ -436,6 +436,10 @@ defmodule ExUnit.Diff do
|
||||
inspect(key) <> " => "
|
||||
end
|
||||
|
||||
defp format_key(key, true) when is_nil(key) or is_boolean(key) do
|
||||
inspect(key) <> ": "
|
||||
end
|
||||
|
||||
defp format_key(key, true) do
|
||||
":" <> result = inspect(key)
|
||||
result <> ": "
|
||||
|
||||
@@ -426,7 +426,7 @@ defmodule ExUnit.DocTest do
|
||||
end
|
||||
|
||||
defp extract_tests(line_no, doc, module) do
|
||||
all_lines = String.split(doc, ~r/\n/, trim: false)
|
||||
all_lines = String.split(doc, "\n", trim: false)
|
||||
lines = adjust_indent(all_lines, line_no + 1, module)
|
||||
extract_tests(lines, "", "", [], true, module)
|
||||
end
|
||||
@@ -626,11 +626,29 @@ defmodule ExUnit.DocTest do
|
||||
[mod, message] = :binary.split(error, ")")
|
||||
{:error, Module.concat([mod]), String.trim_leading(message)}
|
||||
_ ->
|
||||
if string =~ ~r/\A#[A-Z][\w\.]*</mu do
|
||||
if is_inspected?(string) do
|
||||
{:inspect, inspect(string)}
|
||||
else
|
||||
{:test, string}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp is_inspected?(<<?#, char, rest::binary>>) when char in ?A..?Z,
|
||||
do: is_inspected_end?(rest)
|
||||
defp is_inspected?(_),
|
||||
do: false
|
||||
|
||||
defp is_inspected_end?(<<?., char, rest::binary>>) when char in ?A..?Z,
|
||||
do: is_inspected_end?(rest)
|
||||
defp is_inspected_end?(<<char, rest::binary>>)
|
||||
when char in ?A..?Z
|
||||
when char in ?a..?z
|
||||
when char in ?0..?9
|
||||
when char == ?_,
|
||||
do: is_inspected_end?(rest)
|
||||
defp is_inspected_end?(<<?<, _::binary>>),
|
||||
do: true
|
||||
defp is_inspected_end?(_),
|
||||
do: false
|
||||
end
|
||||
|
||||
@@ -14,10 +14,11 @@ defmodule ExUnit.Filters do
|
||||
"""
|
||||
@spec parse_path(String.t) :: {String.t, any}
|
||||
def parse_path(file) do
|
||||
case Regex.run(~r/^(.+):(\d+)$/, file, capture: :all_but_first) do
|
||||
[file, line_number] ->
|
||||
{file, exclude: [:test], include: [line: line_number]}
|
||||
nil ->
|
||||
{paths, [line]} = file |> String.split(":") |> Enum.split(-1)
|
||||
case Integer.parse(line) do
|
||||
{_, ""} ->
|
||||
{Enum.join(paths, ":"), exclude: [:test], include: [line: line]}
|
||||
_ ->
|
||||
{file, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule ExUnit.OnExitHandler do
|
||||
Agent.update(@name, &Map.put(&1, pid, []))
|
||||
end
|
||||
|
||||
@spec add(pid, term, (()-> term)) :: :ok | :error
|
||||
@spec add(pid, term, (() -> term)) :: :ok | :error
|
||||
def add(pid, name_or_ref, callback) when is_pid(pid) and is_function(callback, 0) do
|
||||
Agent.get_and_update(@name, fn map ->
|
||||
if entries = Map.get(map, pid) do
|
||||
|
||||
@@ -289,6 +289,10 @@ defmodule ExUnit.DiffTest do
|
||||
|
||||
assert script(%{"foo-bar": 1}, %{}) == [{:eq, "%{"}, [[del: "\"foo-bar\": 1"]], {:eq, "}"}]
|
||||
assert script(%{}, %{}) == [eq: "%{}"]
|
||||
|
||||
assert script(%{nil: 42}, %{}) == [{:eq, "%{"}, [[del: "nil: 42"]], {:eq, "}"}]
|
||||
assert script(%{true: 42}, %{}) == [{:eq, "%{"}, [[del: "true: 42"]], {:eq, "}"}]
|
||||
assert script(%{false: 42}, %{}) == [{:eq, "%{"}, [[del: "false: 42"]], {:eq, "}"}]
|
||||
end
|
||||
|
||||
test "structs" do
|
||||
|
||||
@@ -102,7 +102,7 @@ defmodule IEx.Autocomplete do
|
||||
if prefix in [0, length] do
|
||||
yes("", Enum.flat_map(entries, &to_entries/1))
|
||||
else
|
||||
yes(:binary.part(first.name, prefix, length-prefix), [])
|
||||
yes(:binary.part(first.name, prefix, length - prefix), [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -212,7 +212,7 @@ defmodule IEx.Autocomplete do
|
||||
for mod <- match_modules(base, module === Elixir),
|
||||
parts = String.split(mod, "."),
|
||||
depth <= length(parts) do
|
||||
%{kind: :module, type: :elixir, name: Enum.at(parts, depth-1)}
|
||||
%{kind: :module, type: :elixir, name: Enum.at(parts, depth - 1)}
|
||||
end
|
||||
|> Enum.uniq
|
||||
end
|
||||
|
||||
@@ -158,7 +158,7 @@ defmodule IEx.Helpers do
|
||||
|
||||
{erls, exs} = Enum.split_with(found, &String.ends_with?(&1, ".erl"))
|
||||
|
||||
modules = Enum.map(erls, fn(source) ->
|
||||
erl_modules = Enum.map(erls, fn(source) ->
|
||||
{module, binary} = compile_erlang(source)
|
||||
unless path == :in_memory do
|
||||
base = source |> Path.basename |> Path.rootname
|
||||
@@ -167,7 +167,13 @@ defmodule IEx.Helpers do
|
||||
module
|
||||
end)
|
||||
|
||||
modules ++ compile_elixir(exs, path)
|
||||
ex_modules = try do
|
||||
compile_elixir(exs, path)
|
||||
catch
|
||||
_, _ -> raise CompileError
|
||||
end
|
||||
|
||||
erl_modules ++ ex_modules
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -269,7 +269,11 @@ defmodule IEx.Server do
|
||||
|
||||
defp allow_take?(identifier) do
|
||||
message = IEx.color(:eval_interrupt, "#{identifier}\nAllow? [Yn] ")
|
||||
IO.gets(:stdio, message) =~ ~r/^(Y(es)?)?$/i
|
||||
yes?(IO.gets(:stdio, message))
|
||||
end
|
||||
|
||||
defp yes?(string) do
|
||||
is_binary(string) and String.trim(string) in ["", "y", "Y", "yes", "YES", "Yes"]
|
||||
end
|
||||
|
||||
## State
|
||||
|
||||
@@ -296,19 +296,12 @@ defmodule IEx.HelpersTest do
|
||||
cleanup_modules([Sample])
|
||||
end
|
||||
|
||||
test "c/2 helper" do
|
||||
assert_raise UndefinedFunctionError, ~r"function Sample\.run/0 is undefined", fn ->
|
||||
Sample.run
|
||||
test "c helper with error" do
|
||||
ExUnit.CaptureIO.capture_io fn ->
|
||||
with_file "sample.ex", "raise \"oops\"", fn ->
|
||||
assert_raise CompileError, fn -> c("sample.ex") end
|
||||
end
|
||||
end
|
||||
|
||||
filename = "sample.ex"
|
||||
with_file filename, test_module_code(), fn ->
|
||||
assert c(filename, ".") == [Sample]
|
||||
assert File.exists?("Elixir.Sample.beam")
|
||||
assert Sample.run == :run
|
||||
end
|
||||
after
|
||||
cleanup_modules([Sample])
|
||||
end
|
||||
|
||||
test "c helper with full path" do
|
||||
@@ -366,21 +359,6 @@ defmodule IEx.HelpersTest do
|
||||
cleanup_modules([:sample])
|
||||
end
|
||||
|
||||
test "c/2 helper erlang" do
|
||||
assert_raise UndefinedFunctionError, ~r"function :sample.hello/0 is undefined", fn ->
|
||||
:sample.hello
|
||||
end
|
||||
|
||||
filename = "sample.erl"
|
||||
with_file filename, erlang_module_code(), fn ->
|
||||
assert c(filename, ".") == [:sample]
|
||||
assert :sample.hello == :world
|
||||
assert File.exists?("sample.beam")
|
||||
end
|
||||
after
|
||||
cleanup_modules([:sample])
|
||||
end
|
||||
|
||||
test "c helper skips unknown files" do
|
||||
assert_raise UndefinedFunctionError, ~r"function :sample.hello/0 is undefined", fn ->
|
||||
:sample.hello
|
||||
@@ -396,6 +374,20 @@ defmodule IEx.HelpersTest do
|
||||
cleanup_modules([:sample, Sample2])
|
||||
end
|
||||
|
||||
test "c helper with path" do
|
||||
assert_raise UndefinedFunctionError, ~r"function Sample\.run/0 is undefined", fn ->
|
||||
Sample.run
|
||||
end
|
||||
|
||||
filename = "sample.ex"
|
||||
with_file filename, test_module_code(), fn ->
|
||||
assert c(filename, ".") == [Sample]
|
||||
assert File.exists?("Elixir.Sample.beam")
|
||||
assert Sample.run == :run
|
||||
end
|
||||
after
|
||||
cleanup_modules([Sample])
|
||||
end
|
||||
|
||||
test "l helper" do
|
||||
assert_raise UndefinedFunctionError, ~r"function Sample.run/0 is undefined", fn ->
|
||||
|
||||
@@ -84,7 +84,9 @@ defmodule Logger.Formatter do
|
||||
def compile({mod, fun}) when is_atom(mod) and is_atom(fun), do: {mod, fun}
|
||||
|
||||
def compile(str) do
|
||||
for part <- Regex.split(~r/(?<head>)\$[a-z]+(?<tail>)/, str, on: [:head, :tail], trim: true) do
|
||||
regex = Regex.recompile!(~r/(?<head>)\$[a-z]+(?<tail>)/)
|
||||
|
||||
for part <- Regex.split(regex, str, on: [:head, :tail], trim: true) do
|
||||
case part do
|
||||
"$" <> code -> compile_code(String.to_atom(code))
|
||||
_ -> part
|
||||
|
||||
@@ -36,11 +36,12 @@ defmodule Logger.Translator do
|
||||
opts = Application.get_env(:logger, :translator_inspect_opts)
|
||||
|
||||
case message do
|
||||
{'** Generic server ' ++ _, [name, last, state, reason]} ->
|
||||
{'** Generic server ' ++ _, [name, last, state, reason | client]} ->
|
||||
msg = ["GenServer #{inspect name} terminating", format_stop(reason),
|
||||
"\nLast message: #{inspect last, opts}"]
|
||||
"\nLast message#{format_from(client)}: #{inspect last, opts}"]
|
||||
if min_level == :debug do
|
||||
{:ok, [msg | "\nState: #{inspect state, opts}"]}
|
||||
{:ok, [msg, "\nState: #{inspect state, opts}" |
|
||||
format_client(client)]}
|
||||
else
|
||||
{:ok, msg}
|
||||
end
|
||||
@@ -96,7 +97,7 @@ defmodule Logger.Translator do
|
||||
[supervisor: sup, errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, pid}, {name_or_id, name} | offender]])
|
||||
when is_pid(pid) and context !== :shutdown and name_or_id in [:name, :id] do
|
||||
when is_pid(pid) and context !== :shutdown and name_or_id in [:name, :id] do
|
||||
{:ok, ["Child ", inspect(name), " of Supervisor ",
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context),
|
||||
@@ -105,37 +106,37 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
defp translate_supervisor(min_level,
|
||||
[supervisor: sup, errorContext: context,
|
||||
[supervisor: sup, errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, _pid},
|
||||
{name_or_id, name} | offender]]) when name_or_id in [:name, :id] do
|
||||
{:ok, ["Child ", inspect(name), " of Supervisor ",
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context) |
|
||||
child_info(min_level, offender)]}
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context) |
|
||||
child_info(min_level, offender)]}
|
||||
end
|
||||
|
||||
defp translate_supervisor(min_level,
|
||||
[supervisor: sup, errorContext: context,
|
||||
[supervisor: sup, errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, pid} | offender]]) do
|
||||
{:ok, ["Child of Supervisor ",
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context),
|
||||
"\nPid: ", inspect(pid) |
|
||||
child_info(min_level, offender)]}
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context),
|
||||
"\nPid: ", inspect(pid) |
|
||||
child_info(min_level, offender)]}
|
||||
end
|
||||
|
||||
defp translate_supervisor(min_level,
|
||||
[supervisor: sup, errorContext: context,
|
||||
[supervisor: sup, errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:nb_children, n},
|
||||
{name_or_id, name} | offender]]) when name_or_id in [:name, :id] do
|
||||
{:ok, ["Children ", inspect(name), " of Supervisor ",
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context),
|
||||
"\nNumber: ", inspect(n) |
|
||||
child_info(min_level, offender)]}
|
||||
sup_name(sup), ?\s, sup_context(context),
|
||||
"\n** (exit) ", offender_reason(reason, context),
|
||||
"\nNumber: ", inspect(n) |
|
||||
child_info(min_level, offender)]}
|
||||
end
|
||||
|
||||
defp translate_supervisor(_min_level, _other), do: :none
|
||||
@@ -147,19 +148,19 @@ defmodule Logger.Translator do
|
||||
|
||||
defp translate_progress(min_level,
|
||||
[supervisor: sup,
|
||||
started: [{:pid, pid}, {name_or_id, name} | started]]) when name_or_id in [:name, :id] do
|
||||
started: [{:pid, pid}, {name_or_id, name} | started]]) when name_or_id in [:name, :id] do
|
||||
{:ok, ["Child ", inspect(name), " of Supervisor ",
|
||||
sup_name(sup), " started",
|
||||
"\nPid: ", inspect(pid) |
|
||||
child_info(min_level, started)]}
|
||||
sup_name(sup), " started",
|
||||
"\nPid: ", inspect(pid) |
|
||||
child_info(min_level, started)]}
|
||||
end
|
||||
|
||||
defp translate_progress(min_level,
|
||||
[supervisor: sup,
|
||||
started: [{:pid, pid} | started]]) do
|
||||
started: [{:pid, pid} | started]]) do
|
||||
{:ok, ["Child of Supervisor ", sup_name(sup), " started",
|
||||
"\nPid: ", inspect(pid) |
|
||||
child_info(min_level, started)]}
|
||||
"\nPid: ", inspect(pid) |
|
||||
child_info(min_level, started)]}
|
||||
end
|
||||
|
||||
defp translate_progress(_min_level, _other), do: :none
|
||||
@@ -176,17 +177,17 @@ defmodule Logger.Translator do
|
||||
|
||||
defp child_info(min_level, [{:mfargs, {mod, fun, args}} | debug]) do
|
||||
["\nStart Call: ", format_mfa(mod, fun, args) |
|
||||
child_debug(min_level, debug)]
|
||||
child_debug(min_level, debug)]
|
||||
end
|
||||
|
||||
defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
|
||||
["\nStart Call: ", format_mfa(mod, fun, args) |
|
||||
child_debug(min_level, debug)]
|
||||
child_debug(min_level, debug)]
|
||||
end
|
||||
|
||||
defp child_info(min_level, [{:mod, mod} | debug]) do
|
||||
["\nStart Module: ", inspect(mod) |
|
||||
child_debug(min_level, debug)]
|
||||
child_debug(min_level, debug)]
|
||||
end
|
||||
|
||||
defp child_debug(:debug,
|
||||
@@ -216,9 +217,9 @@ defmodule Logger.Translator do
|
||||
{:error_info, {kind, exception, stack}} | crashed],
|
||||
linked]) do
|
||||
{:ok, ["Process ", crash_name(pid, name), " terminating",
|
||||
format(kind, exception, stack),
|
||||
crash_info(min_level, [initial_call | crashed]) |
|
||||
crash_linked(min_level, linked)]}
|
||||
format(kind, exception, stack),
|
||||
crash_info(min_level, [initial_call | crashed]) |
|
||||
crash_linked(min_level, linked)]}
|
||||
end
|
||||
|
||||
defp translate_crash(min_level,
|
||||
@@ -227,9 +228,9 @@ defmodule Logger.Translator do
|
||||
{:error_info, {kind, exception, stack}} | crashed],
|
||||
linked]) do
|
||||
{:ok, ["Process ", crash_name(pid, name), " terminating",
|
||||
format(kind, exception, stack),
|
||||
crash_info(min_level, crashed),
|
||||
crash_linked(min_level, linked)]}
|
||||
format(kind, exception, stack),
|
||||
crash_info(min_level, crashed),
|
||||
crash_linked(min_level, linked)]}
|
||||
end
|
||||
|
||||
defp crash_name(pid, []), do: inspect(pid)
|
||||
@@ -240,13 +241,13 @@ defmodule Logger.Translator do
|
||||
defp crash_info(min_level,
|
||||
[{:initial_call, {mod, fun, args}} | info], prefix) do
|
||||
[prefix, "Initial Call: ", crash_call(mod, fun, args) |
|
||||
crash_info(min_level, info, prefix)]
|
||||
crash_info(min_level, info, prefix)]
|
||||
end
|
||||
|
||||
defp crash_info(min_level,
|
||||
[{:current_function, {mod, fun, args}} | info], prefix) do
|
||||
[prefix, "Current Call: ", crash_call(mod, fun, args) |
|
||||
crash_info(min_level, info, prefix)]
|
||||
crash_info(min_level, info, prefix)]
|
||||
end
|
||||
|
||||
defp crash_info(min_level, [{:current_function, []} | info], prefix) do
|
||||
@@ -256,7 +257,17 @@ defmodule Logger.Translator do
|
||||
defp crash_info(min_level,
|
||||
[{:ancestors, ancestors} | debug], prefix) do
|
||||
[prefix, "Ancestors: ", inspect(ancestors) |
|
||||
crash_debug(min_level, debug, prefix)]
|
||||
crash_info(min_level, debug, prefix)]
|
||||
end
|
||||
|
||||
defp crash_info(:debug, debug, prefix) do
|
||||
for {key, value} <- debug do
|
||||
crash_debug(key, value, prefix)
|
||||
end
|
||||
end
|
||||
|
||||
defp crash_info(_, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp crash_call(mod, fun, arity) when is_integer(arity) do
|
||||
@@ -267,22 +278,28 @@ defmodule Logger.Translator do
|
||||
format_mfa(mod, fun, length(args))
|
||||
end
|
||||
|
||||
defp crash_debug(:debug,
|
||||
[messages: msgs, links: links, dictionary: dict,
|
||||
trap_exit: trap, status: status, heap_size: heap_size,
|
||||
stack_size: stack_size, reductions: reductions], prefix) do
|
||||
[prefix, "Messages: ", inspect(msgs),
|
||||
prefix, "Links: ", inspect(links),
|
||||
prefix, "Dictionary: ", inspect(dict),
|
||||
prefix, "Trapping Exits: ", inspect(trap),
|
||||
prefix, "Status: ", inspect(status),
|
||||
prefix, "Heap Size: ", inspect(heap_size),
|
||||
prefix, "Stack Size: ", inspect(stack_size),
|
||||
prefix, "Reductions: ", inspect(reductions)]
|
||||
defp crash_debug(:current_stacktrace, stack, prefix) do
|
||||
stack_prefix = [prefix | " "]
|
||||
[prefix, "Current Stacktrace:" |
|
||||
Enum.map(stack, &[stack_prefix | Exception.format_stacktrace_entry(&1)])]
|
||||
end
|
||||
|
||||
defp crash_debug(_min_level, _info, _prefix) do
|
||||
[]
|
||||
defp crash_debug(key, value, prefix) do
|
||||
[prefix, crash_debug_key(key), ?:, ?\s, inspect(value)]
|
||||
end
|
||||
|
||||
defp crash_debug_key(key) do
|
||||
case key do
|
||||
:message_queue_len -> "Message Queue Length"
|
||||
:messages -> "Messages"
|
||||
:links -> "Links"
|
||||
:dictionary -> "Dictionary"
|
||||
:trap_exit -> "Trapping Exits"
|
||||
:status -> "Status"
|
||||
:heap_size -> "Heap Size"
|
||||
:stack_size -> "Stack Size"
|
||||
:reductions -> "Reductions"
|
||||
end
|
||||
end
|
||||
|
||||
defp crash_linked(_min_level, []), do: []
|
||||
@@ -297,17 +314,17 @@ defmodule Logger.Translator do
|
||||
[{:pid, pid}, {:registered_name, []} | info]) do
|
||||
indent = " "
|
||||
[?\n, indent, inspect(pid) |
|
||||
crash_info(min_level, info, [?\n, indent | indent])]
|
||||
crash_info(min_level, info, [?\n, indent | indent])]
|
||||
end
|
||||
|
||||
defp crash_neighbour(min_level,
|
||||
[{:pid, pid}, {:registered_name, name} | info]) do
|
||||
indent = " "
|
||||
[?\n, indent, inspect(name), " (", inspect(pid), ")" |
|
||||
crash_info(min_level, info, [?\n, indent | indent])]
|
||||
crash_info(min_level, info, [?\n, indent | indent])]
|
||||
end
|
||||
|
||||
defp format_stop({maybe_exception, [_ | _ ] = maybe_stacktrace} = reason) do
|
||||
defp format_stop({maybe_exception, [_ | _] = maybe_stacktrace} = reason) do
|
||||
try do
|
||||
format_stacktrace(maybe_stacktrace)
|
||||
catch
|
||||
@@ -315,8 +332,7 @@ defmodule Logger.Translator do
|
||||
format_stop_banner(reason)
|
||||
else
|
||||
formatted_stacktrace ->
|
||||
[format_stop_banner(maybe_exception, maybe_stacktrace) |
|
||||
formatted_stacktrace]
|
||||
[format_stop_banner(maybe_exception, maybe_stacktrace) | formatted_stacktrace]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -331,8 +347,8 @@ defmodule Logger.Translator do
|
||||
# OTP processes rewrite the :undef error to these reasons when logging
|
||||
@gen_undef [:"module could not be loaded", :"function not exported"]
|
||||
|
||||
defp format_stop_banner(undef, [{mod, fun, args, _info} | _ ] = stacktrace)
|
||||
when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
|
||||
defp format_stop_banner(undef, [{mod, fun, args, _info} | _] = stacktrace)
|
||||
when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
|
||||
cond do
|
||||
is_list(args) ->
|
||||
format_undef(mod, fun, length(args), undef, stacktrace)
|
||||
@@ -344,8 +360,11 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
defp format_stop_banner(reason, stacktrace) do
|
||||
# If this is already an exception (even an ErlangError), we format it as an
|
||||
# exception. Otherwise, we try to normalize it, and if it's normalized as an
|
||||
# ErlangError we instead format it as an exit.
|
||||
if Exception.exception?(reason) do
|
||||
[?\n | Exception.format_banner(:error, reason, stacktrace)]
|
||||
[?\n | Exception.format_banner(:error, reason, stacktrace)]
|
||||
else
|
||||
case Exception.normalize(:error, reason, stacktrace) do
|
||||
%ErlangError{} ->
|
||||
@@ -368,11 +387,31 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
defp format_stacktrace(stacktrace) do
|
||||
for entry <- stacktrace do
|
||||
[<<"\n ">> | Exception.format_stacktrace_entry(entry)]
|
||||
end
|
||||
for entry <- stacktrace do
|
||||
[<<"\n ">> | Exception.format_stacktrace_entry(entry)]
|
||||
end
|
||||
end
|
||||
|
||||
defp format_mfa(mod, fun, :undefined), do: [inspect(mod), ?., to_string(fun) | "/?"]
|
||||
defp format_mfa(mod, fun, args), do: Exception.format_mfa(mod, fun, args)
|
||||
|
||||
defp format_from([]),
|
||||
do: ""
|
||||
defp format_from([from]),
|
||||
do: " (from #{inspect(from)})"
|
||||
defp format_from([from, stacktrace]) when is_list(stacktrace),
|
||||
do: " (from #{inspect(from)})"
|
||||
defp format_from([from, node_name]) when is_atom(node_name),
|
||||
do: " (from #{inspect(from)} on #{inspect(node_name)})"
|
||||
|
||||
defp format_client([from]) do
|
||||
"\nClient #{inspect(from)} is dead"
|
||||
end
|
||||
defp format_client([from, stacktrace]) when is_list(stacktrace) do
|
||||
["\nClient #{inspect(from)} is alive\n" |
|
||||
Exception.format_stacktrace(stacktrace)]
|
||||
end
|
||||
defp format_client(_) do
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,10 +32,10 @@ defmodule Logger.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
defp truncate_n(int, n) when int in 0..127, do: {int, n-1}
|
||||
defp truncate_n(int, n) when int in 127..0x07FF, do: {int, n-2}
|
||||
defp truncate_n(int, n) when int in 0x800..0xFFFF, do: {int, n-3}
|
||||
defp truncate_n(int, n) when int >= 0x10000 and is_integer(int), do: {int, n-4}
|
||||
defp truncate_n(int, n) when int in 0..127, do: {int, n - 1}
|
||||
defp truncate_n(int, n) when int in 127..0x07FF, do: {int, n - 2}
|
||||
defp truncate_n(int, n) when int in 0x800..0xFFFF, do: {int, n - 3}
|
||||
defp truncate_n(int, n) when int >= 0x10000 and is_integer(int), do: {int, n - 4}
|
||||
|
||||
defp truncate_n(list, n) when is_list(list) do
|
||||
truncate_n_list(list, n, [])
|
||||
|
||||
@@ -5,9 +5,18 @@ defmodule Logger.TranslatorTest do
|
||||
defmodule MyGenServer do
|
||||
use GenServer
|
||||
|
||||
def handle_cast(:error, _) do
|
||||
raise "oops"
|
||||
end
|
||||
|
||||
def handle_call(:error, _, _) do
|
||||
raise "oops"
|
||||
end
|
||||
def handle_call(:error_on_down, {pid, _}, _) do
|
||||
mon = Process.monitor(pid)
|
||||
assert_receive {:DOWN, ^mon, _, _, _}
|
||||
raise "oops"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MyGenEvent do
|
||||
@@ -93,20 +102,101 @@ defmodule Logger.TranslatorTest do
|
||||
Application.put_env(:logger, :translator_inspect_opts, [])
|
||||
end
|
||||
|
||||
test "translates GenServer crashes on debug" do
|
||||
# TODO: Remove this check once we depend only on 20
|
||||
if :erlang.system_info(:otp_release) >= '20' do
|
||||
test "translates GenServer crashes on debug" do
|
||||
{:ok, pid} = GenServer.start(MyGenServer, :ok)
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(GenServer.call(pid, :error))
|
||||
end) =~ ~r"""
|
||||
\[error\] GenServer #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message \(from #PID<\d+\.\d+\.\d+>\): :error
|
||||
State: :ok
|
||||
Client #PID<\d+\.\d+\.\d+> is alive
|
||||
.*
|
||||
"""s
|
||||
end
|
||||
|
||||
test "translates GenServer crashes with named client on debug" do
|
||||
{:ok, pid} = GenServer.start(MyGenServer, :ok)
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
Process.register(self(), :named_client)
|
||||
catch_exit(GenServer.call(pid, :error))
|
||||
end) =~ ~r"""
|
||||
\[error\] GenServer #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message \(from :named_client\): :error
|
||||
State: :ok
|
||||
Client :named_client is alive
|
||||
.*
|
||||
"""s
|
||||
end
|
||||
|
||||
test "translates GenServer crashes with dead client on debug" do
|
||||
{:ok, pid} = GenServer.start(MyGenServer, :ok)
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
mon = Process.monitor(pid)
|
||||
spawn_link(fn() ->
|
||||
catch_exit(GenServer.call(pid, :error_on_down, 0))
|
||||
end)
|
||||
assert_receive {:DOWN, ^mon, _, _, _}
|
||||
end) =~ ~r"""
|
||||
\[error\] GenServer #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message \(from #PID<\d+\.\d+\.\d+>\): :error_on_down
|
||||
State: :ok
|
||||
Client #PID<\d+\.\d+\.\d+> is dead
|
||||
"""s
|
||||
end
|
||||
else
|
||||
test "translates GenServer crashes on debug" do
|
||||
{:ok, pid} = GenServer.start(MyGenServer, :ok)
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(GenServer.call(pid, :error))
|
||||
end) =~ ~r"""
|
||||
\[error\] GenServer #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message: :error
|
||||
State: :ok
|
||||
"""s
|
||||
end
|
||||
end
|
||||
|
||||
test "translates GenServer crashes with no client" do
|
||||
{:ok, pid} = GenServer.start(MyGenServer, :ok)
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(GenServer.call(pid, :error))
|
||||
mon = Process.monitor(pid)
|
||||
GenServer.cast(pid, :error)
|
||||
assert_receive {:DOWN, ^mon, _, _, _}
|
||||
end) =~ ~r"""
|
||||
\[error\] GenServer #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message: :error
|
||||
Last message: {:"\$gen_cast", :error}
|
||||
State: :ok
|
||||
"""s
|
||||
end
|
||||
|
||||
test "translates GenServer crashes with no client on debug" do
|
||||
{:ok, pid} = GenServer.start(MyGenServer, :ok)
|
||||
|
||||
refute capture_log(:debug, fn ->
|
||||
mon = Process.monitor(pid)
|
||||
GenServer.cast(pid, :error)
|
||||
assert_receive {:DOWN, ^mon, _, _, _}
|
||||
end) =~ "Client"
|
||||
end
|
||||
|
||||
test "translates :gen_event crashes" do
|
||||
{:ok, pid} = :gen_event.start()
|
||||
:ok = :gen_event.add_handler(pid, MyGenEvent, :ok)
|
||||
@@ -382,7 +472,8 @@ defmodule Logger.TranslatorTest do
|
||||
send(pid, :go)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\](?:
|
||||
Message Queue Length: 1(?#TODO: Require once depend on 20)|)
|
||||
Messages: \[:message\]
|
||||
Links: \[\]
|
||||
Dictionary: \[\]
|
||||
@@ -403,15 +494,18 @@ defmodule Logger.TranslatorTest do
|
||||
send(pid, :go)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>, #PID<\d+\.\d+\.\d+>\]
|
||||
Messages: \[\]
|
||||
Links: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Dictionary: \[\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>, #PID<\d+\.\d+\.\d+>\](?:
|
||||
Message Queue Length: 0|
|
||||
Messages: \[\](?# TODO: Remove once depend on 20))
|
||||
Links: \[#PID<\d+\.\d+\.\d+>\](?:|
|
||||
Dictionary: \[\](?# TODO: Remove once depend on 20))
|
||||
Trapping Exits: false
|
||||
Status: :waiting
|
||||
Heap Size: \d+
|
||||
Stack Size: \d+
|
||||
Reductions: \d+
|
||||
Reductions: \d+(?:
|
||||
Current Stacktrace:
|
||||
test/logger/translator_test.exs:\d+: Logger.TranslatorTest.sleep/1(?#TODO: Require once depend on 20)|)
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ defmodule Mix.Dep do
|
||||
end
|
||||
|
||||
defp check_manifest(%{scm: scm} = dep, build_path) do
|
||||
vsn = System.version
|
||||
vsn = {System.version, :erlang.system_info(:otp_release)}
|
||||
|
||||
case Mix.Dep.ElixirSCM.read(build_path) do
|
||||
{:ok, old_vsn, _} when old_vsn != vsn ->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
defmodule Mix.Dep.ElixirSCM do
|
||||
@moduledoc false
|
||||
@manifest ".compile.elixir_scm"
|
||||
@manifest_vsn :v1
|
||||
@manifest_vsn :v2
|
||||
|
||||
def manifest(manifest_path \\ Mix.Project.manifest_path) do
|
||||
Path.join(manifest_path, @manifest)
|
||||
@@ -13,7 +13,7 @@ defmodule Mix.Dep.ElixirSCM do
|
||||
File.mkdir_p!(manifest_path)
|
||||
|
||||
manifest_data =
|
||||
{@manifest_vsn, System.version, config[:build_scm]}
|
||||
{@manifest_vsn, {System.version, :erlang.system_info(:otp_release)}, config[:build_scm]}
|
||||
|> :erlang.term_to_binary(compressed: 9)
|
||||
|
||||
File.write!(manifest(manifest_path), manifest_data)
|
||||
@@ -23,15 +23,10 @@ defmodule Mix.Dep.ElixirSCM do
|
||||
case File.read(manifest(manifest_path)) do
|
||||
{:ok, contents} ->
|
||||
try do
|
||||
:erlang.binary_to_term(contents)
|
||||
else
|
||||
{@manifest_vsn, vsn, scm} ->
|
||||
{:ok, vsn, scm}
|
||||
_ ->
|
||||
{:ok, "1.0.0", nil} # Force old version if file exists but old format
|
||||
{@manifest_vsn, vsn, scm} = :erlang.binary_to_term(contents)
|
||||
{:ok, vsn, scm}
|
||||
rescue
|
||||
_ ->
|
||||
{:ok, "1.0.0", nil} # Force old version if file exists but old format
|
||||
_ -> {:ok, {"1.0.0", '17'}, nil}
|
||||
end
|
||||
_ ->
|
||||
:error
|
||||
|
||||
@@ -78,7 +78,7 @@ defmodule Mix.Generator do
|
||||
@file file
|
||||
File.read!(file)
|
||||
c when is_binary(c) ->
|
||||
@file {__ENV__.file, __ENV__.line+1}
|
||||
@file {__ENV__.file, __ENV__.line + 1}
|
||||
c
|
||||
_ ->
|
||||
raise ArgumentError, "expected string or from_file: file"
|
||||
|
||||
@@ -99,7 +99,7 @@ defmodule Mix.Local.Installer do
|
||||
defp do_install({module, name}, src, opts) do
|
||||
src_basename = Path.basename(URI.parse(src).path)
|
||||
dst = Path.join(Mix.Local.path_for(name), src_basename)
|
||||
previous_files = module.find_previous_versions(src, dst)
|
||||
previous_files = module.find_previous_versions(src_basename, dst)
|
||||
|
||||
if opts[:force] || should_install?(name, src, previous_files) do
|
||||
case Mix.Utils.read_path(src, opts) do
|
||||
@@ -255,7 +255,7 @@ defmodule Mix.Local.Installer do
|
||||
path = Path.join(root, name)
|
||||
cond do
|
||||
not File.exists?(path) ->
|
||||
Mix.shell.error "Could not find a local #{item_name} named #{inspect name}. "<>
|
||||
Mix.shell.error "Could not find a local #{item_name} named #{inspect name}. " <>
|
||||
"Existing #{item_plural} are:"
|
||||
Mix.Task.run item_name
|
||||
nil
|
||||
|
||||
@@ -46,31 +46,27 @@ defmodule Mix.Shell.IO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints a message and prompts the user for
|
||||
input. Input will be consumed until Enter is pressed.
|
||||
Prints a message and prompts the user for input.
|
||||
|
||||
Input will be consumed until Enter is pressed.
|
||||
"""
|
||||
def prompt(message) do
|
||||
print_app()
|
||||
IO.gets message <> " "
|
||||
IO.gets(message <> " ")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints a message and asks the user if they want to proceed.
|
||||
The user must press Enter or type anything that matches the "yes"
|
||||
regex `~r/^Y(es)?$/i`.
|
||||
|
||||
The user must press Enter or type one of "y", "yes", "Y", "YES" or
|
||||
"Yes".
|
||||
"""
|
||||
def yes?(message) do
|
||||
print_app()
|
||||
got_yes? IO.gets(message <> " [Yn] ")
|
||||
answer = IO.gets(message <> " [Yn] ")
|
||||
is_binary(answer) and String.trim(answer) in ["", "y", "Y", "yes", "YES", "Yes"]
|
||||
end
|
||||
|
||||
defp got_yes?(answer) when is_binary(answer) do
|
||||
answer =~ ~r/^(Y(es)?)?$/i
|
||||
end
|
||||
|
||||
# The IO server may return :eof or :error
|
||||
defp got_yes?(_), do: false
|
||||
|
||||
defp red(message) do
|
||||
[:red, :bright, message]
|
||||
end
|
||||
|
||||
@@ -32,15 +32,17 @@ defmodule Mix.Shell.Quiet do
|
||||
defdelegate error(message), to: Mix.Shell.IO
|
||||
|
||||
@doc """
|
||||
Prints a message and prompts the user for
|
||||
input. Input will be consumed until Enter is pressed.
|
||||
Prints a message and prompts the user for input.
|
||||
|
||||
Input will be consumed until Enter is pressed.
|
||||
"""
|
||||
defdelegate prompt(message), to: Mix.Shell.IO
|
||||
|
||||
@doc """
|
||||
Receives a message and asks the user if they want to proceed.
|
||||
The user must press enter or type anything that matches the "yes"
|
||||
regex `~r/^Y(es)?$/i`.
|
||||
Prints a message and asks the user if they want to proceed.
|
||||
|
||||
The user must press Enter or type one of "y", "yes", "Y", "YES" or
|
||||
"Yes".
|
||||
"""
|
||||
defdelegate yes?(message), to: Mix.Shell.IO
|
||||
end
|
||||
|
||||
@@ -68,13 +68,19 @@ defmodule Mix.Task do
|
||||
# entire load path so make sure we only return unique modules.
|
||||
|
||||
for(dir <- dirs,
|
||||
{:ok, files} = :erl_prim_loader.list_dir(to_charlist(dir)),
|
||||
file <- files,
|
||||
file <- safe_list_dir(to_charlist(dir)),
|
||||
mod = task_from_path(file),
|
||||
do: mod)
|
||||
|> Enum.uniq
|
||||
end
|
||||
|
||||
defp safe_list_dir(path) do
|
||||
case :erl_prim_loader.list_dir(path) do
|
||||
{:ok, paths} -> paths
|
||||
{:error, _} -> []
|
||||
end
|
||||
end
|
||||
|
||||
@prefix_size byte_size("Elixir.Mix.Tasks.")
|
||||
@suffix_size byte_size(".beam")
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ defmodule Mix.Tasks.Loadpaths do
|
||||
end
|
||||
|
||||
defp load_project(config, _args) do
|
||||
vsn = System.version
|
||||
vsn = {System.version, :erlang.system_info(:otp_release)}
|
||||
scm = config[:build_scm]
|
||||
|
||||
# Erase the app build if we have lock mismatch.
|
||||
|
||||
@@ -119,11 +119,11 @@ defmodule Mix.Tasks.New do
|
||||
defp otp_app(mod, true) do
|
||||
" [extra_applications: [:logger],\n mod: {#{mod}.Application, []}]"
|
||||
end
|
||||
|
||||
|
||||
defp cd_path(".") do
|
||||
""
|
||||
end
|
||||
|
||||
|
||||
defp cd_path(path) do
|
||||
"cd #{path}\n "
|
||||
end
|
||||
@@ -158,7 +158,7 @@ defmodule Mix.Tasks.New do
|
||||
end
|
||||
|
||||
defp check_application_name!(name, inferred?) do
|
||||
unless name =~ ~r/^[a-z][\w_]*$/ do
|
||||
unless name =~ Regex.recompile!(~r/^[a-z][a-z0-9_]*$/) do
|
||||
Mix.raise "Application name must start with a letter and have only lowercase " <>
|
||||
"letters, numbers and underscore, got: #{inspect name}" <>
|
||||
(if inferred? do
|
||||
@@ -171,7 +171,7 @@ defmodule Mix.Tasks.New do
|
||||
end
|
||||
|
||||
defp check_mod_name_validity!(name) do
|
||||
unless name =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/ do
|
||||
unless name =~ Regex.recompile!(~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do
|
||||
Mix.raise "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect name}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,6 +52,11 @@ defmodule Mix.Tasks.ArchiveTest do
|
||||
archive = tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin")
|
||||
assert to_charlist(archive) in :code.get_path
|
||||
|
||||
# Try to override it with URL
|
||||
send self(), {:mix_shell_input, :yes?, false}
|
||||
Mix.Tasks.Archive.Install.run ["https://example.com/archive-0.1.0?hello.ez"]
|
||||
assert_received {:mix_shell, :yes?, ["Found existing archive: " <> _]}
|
||||
|
||||
# Loading the archive should emit warning again
|
||||
Mix.Local.append_archives
|
||||
assert_received {:mix_shell, :error, [^version_error]}
|
||||
|
||||
@@ -8,6 +8,8 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
:ok
|
||||
end
|
||||
|
||||
@elixir_otp_version {System.version, :erlang.system_info(:otp_release)}
|
||||
|
||||
test "compiles a project without per environment build" do
|
||||
Mix.Project.pop
|
||||
Mix.ProjectStack.post_config [build_per_environment: false]
|
||||
@@ -43,7 +45,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
|
||||
assert File.exists?("_build/dev/lib/sample")
|
||||
assert File.exists?("_build/dev/consolidated")
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, System.version, Mix.SCM.Path}
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, @elixir_otp_version, Mix.SCM.Path}
|
||||
|
||||
Mix.Task.clear
|
||||
File.write!("_build/dev/consolidated/.to_be_removed", "")
|
||||
@@ -52,7 +54,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
File.touch!("_build/dev/lib/sample/.compile.elixir_scm", {{2010, 1, 1}, {0, 0, 0}})
|
||||
|
||||
Mix.Tasks.Compile.run []
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, System.version, Mix.SCM.Path}
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, @elixir_otp_version, Mix.SCM.Path}
|
||||
assert File.stat!("_build/dev/lib/sample/.compile.elixir_scm").mtime > {{2010, 1, 1}, {0, 0, 0}}
|
||||
refute File.exists?("_build/dev/consolidated/.to_be_removed")
|
||||
end
|
||||
@@ -64,15 +66,15 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
purge [A, B]
|
||||
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, System.version, Mix.SCM.Path}
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, @elixir_otp_version, Mix.SCM.Path}
|
||||
|
||||
Mix.Task.clear
|
||||
manifest_data = :erlang.term_to_binary({:v1, System.version, :another})
|
||||
manifest_data = :erlang.term_to_binary({:v2, @elixir_otp_version, :another})
|
||||
File.write!("_build/dev/lib/sample/.compile.elixir_scm", manifest_data)
|
||||
File.touch!("_build/dev/lib/sample/.compile.elixir_scm", {{2010, 1, 1}, {0, 0, 0}})
|
||||
|
||||
Mix.Tasks.Compile.run []
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, System.version, Mix.SCM.Path}
|
||||
assert Mix.Dep.ElixirSCM.read == {:ok, @elixir_otp_version, Mix.SCM.Path}
|
||||
assert File.stat!("_build/dev/lib/sample/.compile.elixir_scm").mtime > {{2010, 1, 1}, {0, 0, 0}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -571,7 +571,7 @@ defmodule Mix.Tasks.DepsTest do
|
||||
Mix.Tasks.Deps.Loadpaths.run []
|
||||
|
||||
File.mkdir_p!("_build/dev/lib/ok/ebin")
|
||||
manifest_data = :erlang.term_to_binary({:v1, System.version, :scm})
|
||||
manifest_data = :erlang.term_to_binary({:v2, {System.version, :erlang.system_info(:otp_release)}, :scm})
|
||||
File.write!("_build/dev/lib/ok/.compile.elixir_scm", manifest_data)
|
||||
Mix.Task.clear
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{application, elixir,
|
||||
[{description, "elixir"},
|
||||
{vsn, "1.4.2"},
|
||||
{vsn, "1.4.5"},
|
||||
{modules, [
|
||||
elixir
|
||||
]},
|
||||
|
||||
Reference in New Issue
Block a user