Compare commits
61
Commits
v1.14.0-rc.1
...
v1.14.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0909940b04 | ||
|
|
d061cb96ef | ||
|
|
d1f7457907 | ||
|
|
123034569f | ||
|
|
bdea27c1e3 | ||
|
|
7f70f64a01 | ||
|
|
2acd0949dc | ||
|
|
7996c0a966 | ||
|
|
c01658b12d | ||
|
|
fdf96ad48a | ||
|
|
c80f8b6698 | ||
|
|
e65dae9f54 | ||
|
|
6c5d7bfe3f | ||
|
|
804d03431a | ||
|
|
29a2bb49d0 | ||
|
|
eb43a6a444 | ||
|
|
fcb7b69cb5 | ||
|
|
32c2a2db62 | ||
|
|
a285e5876b | ||
|
|
bc936f77c7 | ||
|
|
ee602cd33e | ||
|
|
b5941d2f6b | ||
|
|
5f20a94895 | ||
|
|
fd97c2f00f | ||
|
|
3d2c3101c3 | ||
|
|
278eba5767 | ||
|
|
b2130136fb | ||
|
|
7e76039434 | ||
|
|
375f2bd1cd | ||
|
|
f6f4f817f9 | ||
|
|
c2426ce98a | ||
|
|
e62a2d74ca | ||
|
|
61b2601488 | ||
|
|
87dc7929fd | ||
|
|
78d6b88e33 | ||
|
|
7f494e6293 | ||
|
|
41fa1db031 | ||
|
|
887e2182dc | ||
|
|
1c391417d4 | ||
|
|
3f2ded08a5 | ||
|
|
d60040636e | ||
|
|
d84f88954c | ||
|
|
d11d619c20 | ||
|
|
f0943e74a7 | ||
|
|
d1055330c2 | ||
|
|
efcb90bf10 | ||
|
|
1457728e54 | ||
|
|
bd63670765 | ||
|
|
b78b480587 | ||
|
|
62ec71a489 | ||
|
|
cfb51e3be9 | ||
|
|
c5151e6890 | ||
|
|
b0153d5db4 | ||
|
|
d522f96fa5 | ||
|
|
e281427cb8 | ||
|
|
f1b8f8ce30 | ||
|
|
8f1296964d | ||
|
|
ba03b8ec05 | ||
|
|
a3f4d0aecc | ||
|
|
391c3a9097 | ||
|
|
ed076eb2fe |
@@ -25,4 +25,4 @@ jobs:
|
||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
|
||||
run: |
|
||||
elixir .github./workflows/notify.exs ${{ github.ref_name }}
|
||||
elixir .github/workflows/notify.exs ${{ github.ref_name }}
|
||||
|
||||
+66
-17
@@ -197,7 +197,7 @@ Enum.slice(letters, 0..5//2)
|
||||
|
||||
```elixir
|
||||
binary_slice("Elixir", 1..5//2)
|
||||
#=> "lx"
|
||||
#=> "lxr"
|
||||
```
|
||||
|
||||
## Expression-based inspection and `Inspect` improvements
|
||||
@@ -241,38 +241,81 @@ protocol, giving developers more control over the struct representation.
|
||||
See the updated documentation for `Inspect` for a general rundown on
|
||||
the approaches and options available.
|
||||
|
||||
## v1.14.0-rc.1 (2022-08-15)
|
||||
## v1.14.2 (2022-11-11)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### IEx
|
||||
#### Elixir
|
||||
|
||||
* [IEx.Helpers] Support sigils in `h/1`
|
||||
* [Code] Add `Code.eval_quoted_with_env/4` with support for the `:prune_binding` option
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Case] Allow test cases to not be registered on use
|
||||
* [ExUnit.DocTest] Include `:doctest` and `:doctest_line` as meta tags
|
||||
* [ExUnit.Formatter] Expose `ExUnit.Formatter.format_assertion_diff/4`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add `:config_path` and `:lockfile` options to `Mix.install/2`
|
||||
* [Mix] `Mix.install/2` accepts atoms as paths
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Enum] Fix usage of range with `steps != 1` in a few functions (regression)
|
||||
* [Kernel] Fix usage of range with `steps != 1` on `binary_slice/2` (regression)
|
||||
* [Kernel] Recursively expand pipelines on right-hand side of `|>` (regression)
|
||||
* [Kernel] Fix equality in guards for dynamic ranges without steps
|
||||
* [Module] Fix loop while unifying type variables
|
||||
* [System] Raise non-generic exception on missing env in `System.fetch_env!/1` to mirror map operations
|
||||
* [Code.Formatter] Fix `size*unit` shortcut in bitstring
|
||||
* [Kernel] Generate unique variables for macro expansion of `defguard`
|
||||
* [Protocol] Expand `:for` in protocols with the appropriate env
|
||||
|
||||
#### IEx
|
||||
#### ExUnit
|
||||
|
||||
* [IEx] Continue parsing after `--no-pry` (regression)
|
||||
* [ExUnit] Do not run duplicate cases on `ExUnit.run/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Properly compile-dependencies on `mix format`
|
||||
* [mix test] Ensure proper error message when there is no test directory
|
||||
|
||||
## v1.14.0-rc.0 (2022-08-01)
|
||||
## v1.14.1 (2022-10-10)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Perform partial expansion of literals in module attributes
|
||||
* [Kernel] Do not add compile-time dependencies for literals as defaults in `Application.compile_env/3` inside module attributes
|
||||
* [Macro] Add `Macro.expand_literals/2` and `Macro.expand_literals/3`
|
||||
* [System] Add `:close_stdin` to `System.shell/2`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix test] Accept `--all-warnings` option
|
||||
|
||||
## 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix misleading warning when `:uniq` is given in `for` comprehensions and the result is unused
|
||||
* [Kernel] Improve error message for when there is a conflicting struct and ignoring module conflict
|
||||
* [Kernel] Do not delete `@enforce_keys` attribute after `defstruct` declaration
|
||||
* [Kernel] Do not crash the checker on modules with missing `:debug_info` chunk
|
||||
* [Macro] Fix error in `Macro.to_string/2` when converting an AST with `:erlang.binary_to_atom/2`
|
||||
* [String] Fix `String.split/3` and `String.next_grapheme/1` returning invalid results on invalid UTF-8 encoding
|
||||
* [System] Do not close stdin by default in `System.shell/2`
|
||||
* [URI] Do not return `uri.port` as `:undefined` in certain cases in `URI.new/1`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.DocTest] Do not crash when both `:moduledoc` and functions are specified in `:only`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [CLI] Fix invalid argument handling when `--no-pry` is given
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Do not cache inputs from `.formatter.exs` so they are properly re-evaluted on every call
|
||||
|
||||
## v1.14.0 (2022-09-01)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
@@ -318,7 +361,7 @@ the approaches and options available.
|
||||
* [Keyword] Add `Keyword.from_keys/2` and `Keyword.replace_lazy/3`
|
||||
* [List] Add `List.keysort/3` with support for a `sorter` function
|
||||
* [Macro] Add `Macro.classify_atom/1` and `Macro.inspect_atom/2`
|
||||
* [Macro] Add `Macro.expand_literal/2` and `Macro.path/2`
|
||||
* [Macro] Add `Macro.path/2`
|
||||
* [Macro.Env] Add `Macro.Env.prune_compile_info/1`
|
||||
* [Map] Add `Map.from_keys/2` and `Map.replace_lazy/3`
|
||||
* [MapSet] Add `MapSet.filter/2`, `MapSet.reject/2`, and `MapSet.symmetric_difference/2`
|
||||
@@ -349,6 +392,7 @@ the approaches and options available.
|
||||
* [IEx] Add line-by-line evaluation of IEx breakpoints
|
||||
* [IEx.Autocomplete] Autocomplete bitstrings modifiers (after `::` inside `<<...>>`)
|
||||
* [IEx.Helpers] Allow an atom to be given to `pid/1`
|
||||
* [IEx.Helpers] Support sigils in `h/1`
|
||||
|
||||
#### Logger
|
||||
|
||||
@@ -356,6 +400,7 @@ the approaches and options available.
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add `:config_path` and `:lockfile` options to `Mix.install/2`
|
||||
* [mix compile] Add `--no-optional-deps` to skip optional dependencies to test compilation works without optional dependencies
|
||||
* [mix compile] Include column information on error diagnostics when possible
|
||||
* [mix deps] `Mix.Dep.Converger` now tells which deps formed a cycle
|
||||
@@ -386,9 +431,12 @@ the approaches and options available.
|
||||
* [Kernel] Warn on underived `@derive` attributes
|
||||
* [Kernel] Remove compile-time dependency from `defimpl :for`
|
||||
* [Kernel] Track all arities on imported functions
|
||||
* [Kernel] Fix equality in guards for dynamic ranges without steps
|
||||
* [Module] Fix loop while unifying type variables
|
||||
* [Protocol] Warn if a protocol has no definitions
|
||||
* [Regex] Show list options when inspecting a Regex manually defined with `Regex.compile/2`
|
||||
* [String] Allow slices to overflow on both starting and ending positions
|
||||
* [String] Allow slices to overflow on both starting and ending positions
|
||||
* [System] Raise non-generic exception on missing env in `System.fetch_env!/1` to mirror map operations
|
||||
|
||||
#### ExUnit
|
||||
|
||||
@@ -410,6 +458,7 @@ the approaches and options available.
|
||||
* [mix compile.elixir] Ensure semantic recompilation cascades to path dependencies
|
||||
* [mix compile.elixir] Lock the compiler to avoid concurrent usage
|
||||
* [mix format] Do not add new lines if the formatted file is empty
|
||||
* [mix format] Properly compile dependencies on `mix format`
|
||||
* [mix release] Only set `RELEASE_MODE` after `env.{sh,bat}` are executed
|
||||
* [mix release] Allow application mode configuration to cascade to dependencies
|
||||
* [mix xref] Do not emit already consolidated warnings during `mix xref trace`
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.14.0-rc.1
|
||||
ELIXIR_VERSION=1.14.2
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
@@ -112,10 +112,10 @@ while [ $I -le $LENGTH ]; do
|
||||
C=1
|
||||
MODE="elixirc"
|
||||
;;
|
||||
-v|--no-halt)
|
||||
-v|--no-halt|--no-pry)
|
||||
C=1
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--app|--eval|--remsh|--dot-iex|--no-pry)
|
||||
-e|-r|-pr|-pa|-pz|--app|--eval|--remsh|--dot-iex)
|
||||
C=2
|
||||
;;
|
||||
--rpc-eval)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
|
||||
set ELIXIR_VERSION=1.14.0-rc.1
|
||||
set ELIXIR_VERSION=1.14.2
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -532,7 +532,7 @@ defmodule Application do
|
||||
raise "Application.compile_env/3 cannot be called inside functions, only in the module body"
|
||||
end
|
||||
|
||||
key_or_path = Macro.expand_literal(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
|
||||
key_or_path = Macro.expand_literals(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
|
||||
|
||||
quote do
|
||||
Application.compile_env(__ENV__, unquote(app), unquote(key_or_path), unquote(default))
|
||||
@@ -572,7 +572,7 @@ defmodule Application do
|
||||
raise "Application.compile_env!/2 cannot be called inside functions, only in the module body"
|
||||
end
|
||||
|
||||
key_or_path = Macro.expand_literal(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
|
||||
key_or_path = Macro.expand_literals(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
|
||||
|
||||
quote do
|
||||
Application.compile_env!(__ENV__, unquote(app), unquote(key_or_path))
|
||||
|
||||
@@ -1529,6 +1529,12 @@ defmodule DateTime do
|
||||
microsecond: {_, precision}
|
||||
} = datetime
|
||||
|
||||
if not is_integer(unit) and
|
||||
unit not in ~w(second millisecond microsecond nanosecond)a do
|
||||
raise ArgumentError,
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
ppd = System.convert_time_unit(86400, :second, unit)
|
||||
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
|
||||
|
||||
|
||||
+43
-24
@@ -100,10 +100,15 @@ defmodule Code do
|
||||
following events are available to tracers:
|
||||
|
||||
* `:start` - (since v1.11.0) invoked whenever the compiler starts to trace
|
||||
a new lexical context, such as a new file. Keep in mind the compiler runs
|
||||
in parallel, so multiple files may invoke `:start` and run at the same
|
||||
time. The value of the `lexical_tracker` of the macro environment, albeit
|
||||
opaque, can be used to uniquely identify the environment.
|
||||
a new lexical context. A lexical context is started when compiling a new
|
||||
file or when defining a module within a function. Note evaluated code
|
||||
does not start a new lexical context (because they don't track unused
|
||||
aliases, imports, etc) but defining a module inside evaluated code will.
|
||||
|
||||
Note this event may be emitted in parallel, where multiple files/modules
|
||||
invoke `:start` and run at the same time. The value of the `lexical_tracker`
|
||||
of the macro environment, albeit opaque, can be used to uniquely identify
|
||||
the environment.
|
||||
|
||||
* `:stop` - (since v1.11.0) invoked whenever the compiler stops tracing a
|
||||
new lexical context, such as a new file.
|
||||
@@ -181,7 +186,7 @@ defmodule Code do
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
A list with all variable bindings.
|
||||
A list with all variables and their values.
|
||||
|
||||
The binding keys are usually atoms, but they may be a tuple for variables
|
||||
defined in a different context.
|
||||
@@ -344,7 +349,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Evaluates the contents given by `string`.
|
||||
|
||||
The `binding` argument is a list of variable bindings.
|
||||
The `binding` argument is a list of all variables and their values.
|
||||
The `opts` argument is a keyword list of environment options.
|
||||
|
||||
**Warning**: `string` can be any Elixir code and will be executed with
|
||||
@@ -362,17 +367,15 @@ defmodule Code do
|
||||
* `:line` - the line on which the script starts
|
||||
|
||||
Additionally, you may also pass an environment as second argument,
|
||||
so the evaluation happens within that environment. However, if the evaluated
|
||||
code requires or compiles another file, the environment given to this function
|
||||
will not apply to said files.
|
||||
so the evaluation happens within that environment.
|
||||
|
||||
Returns a tuple of the form `{value, binding}`, where `value` is the value
|
||||
returned from evaluating `string`. If an error occurs while evaluating
|
||||
`string` an exception will be raised.
|
||||
`string`, an exception will be raised.
|
||||
|
||||
`binding` is a list with all variable bindings after evaluating `string`.
|
||||
The binding keys are usually atoms, but they may be a tuple for variables
|
||||
defined in a different context.
|
||||
`binding` is a list with all variable names and their values after evaluating
|
||||
`string`. The binding keys are usually atoms, but they may be a tuple for variables
|
||||
defined in a different context. The names are in no particular order.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -419,13 +422,13 @@ defmodule Code do
|
||||
defp validated_eval_string(string, binding, opts_or_env) do
|
||||
%{line: line, file: file} = env = env_for_eval(opts_or_env)
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
|
||||
{value, binding, _env} = eval_verify(:eval_forms, forms, binding, env)
|
||||
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
defp eval_verify(fun, forms, binding, env) do
|
||||
defp eval_verify(fun, args) do
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
apply(:elixir, fun, [forms, binding, env])
|
||||
apply(:elixir, fun, args)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -822,7 +825,9 @@ defmodule Code do
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
|
||||
{value, binding, _env} = eval_verify(:eval_quoted, quoted, binding, env_for_eval(env_or_opts))
|
||||
{value, binding, _env} =
|
||||
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
|
||||
|
||||
{value, binding}
|
||||
end
|
||||
|
||||
@@ -859,10 +864,20 @@ defmodule Code do
|
||||
Therefore, the first time you call this function, you must compute
|
||||
the initial environment with `env_for_eval/1`. The remaining calls
|
||||
must pass the environment that was returned by this function.
|
||||
|
||||
## Options
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env) when is_list(binding) do
|
||||
eval_verify(:eval_forms, quoted, binding, env)
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
|
||||
when is_list(binding) do
|
||||
eval_verify(:eval_quoted, [quoted, binding, env, opts])
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -1334,12 +1349,16 @@ defmodule Code do
|
||||
|
||||
Available options are:
|
||||
|
||||
* `:docs` - when `true`, retain documentation in the compiled module.
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:debug_info` - when `true`, retain debug information in the compiled
|
||||
module. This allows a developer to reconstruct the original source
|
||||
code. Defaults to `true`.
|
||||
* `:debug_info` - when `true`, retains debug information in the compiled
|
||||
module. This enables static analysis tools as it allows developers to
|
||||
partially reconstruct the original source code. Therefore, disabling
|
||||
`:debug_info` is not recommended as it removes the ability of the
|
||||
Elixir compiler and other tools to provide feedback. If you want to
|
||||
remove the `:debug_info` while deploying, tools like `mix release`
|
||||
already do such by default.
|
||||
|
||||
* `:ignore_already_consolidated` - when `true`, does not warn when a protocol
|
||||
has already been consolidated and a new implementation is added. Defaults
|
||||
@@ -1348,7 +1367,7 @@ defmodule Code do
|
||||
* `:ignore_module_conflict` - when `true`, does not warn when a module has
|
||||
already been defined. Defaults to `false`.
|
||||
|
||||
* `:relative_paths` - when `true`, use relative paths in quoted nodes,
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
|
||||
|
||||
@@ -1411,7 +1411,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp bitstring_segment_to_algebra({{:"::", _, [segment, spec]}, i}, state, last) do
|
||||
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
|
||||
{spec, state} = bitstring_spec_to_algebra(spec, state)
|
||||
{spec, state} = bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers)
|
||||
|
||||
spec = wrap_in_parens_if_inspected_atom(spec)
|
||||
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
|
||||
@@ -1430,26 +1430,29 @@ defmodule Code.Formatter do
|
||||
{bitstring_wrap_parens(doc, i, last), state}
|
||||
end
|
||||
|
||||
defp bitstring_spec_to_algebra({op, _, [left, right]}, state) when op in [:-, :*] do
|
||||
{left, state} = bitstring_spec_to_algebra(left, state)
|
||||
{right, state} = bitstring_spec_element_to_algebra(right, state)
|
||||
defp bitstring_spec_to_algebra({op, _, [left, right]}, state, normalize_modifiers)
|
||||
when op in [:-, :*] do
|
||||
normalize_modifiers = normalize_modifiers && op != :*
|
||||
{left, state} = bitstring_spec_to_algebra(left, state, normalize_modifiers)
|
||||
{right, state} = bitstring_spec_element_to_algebra(right, state, normalize_modifiers)
|
||||
{concat(concat(left, Atom.to_string(op)), right), state}
|
||||
end
|
||||
|
||||
defp bitstring_spec_to_algebra(spec, state) do
|
||||
bitstring_spec_element_to_algebra(spec, state)
|
||||
defp bitstring_spec_to_algebra(spec, state, normalize_modifiers) do
|
||||
bitstring_spec_element_to_algebra(spec, state, normalize_modifiers)
|
||||
end
|
||||
|
||||
defp bitstring_spec_element_to_algebra(
|
||||
{atom, meta, empty_args},
|
||||
state = %{normalize_bitstring_modifiers: true}
|
||||
state,
|
||||
_normalize_modifiers = true
|
||||
)
|
||||
when is_atom(atom) and empty_args in [nil, []] do
|
||||
empty_args = bitstring_spec_normalize_empty_args(atom)
|
||||
quoted_to_algebra_with_parens_if_operator({atom, meta, empty_args}, :parens_arg, state)
|
||||
end
|
||||
|
||||
defp bitstring_spec_element_to_algebra(spec_element, state) do
|
||||
defp bitstring_spec_element_to_algebra(spec_element, state, _normalize_modifiers) do
|
||||
quoted_to_algebra_with_parens_if_operator(spec_element, :parens_arg, state)
|
||||
end
|
||||
|
||||
|
||||
@@ -77,6 +77,14 @@ defmodule Code.Normalizer do
|
||||
dot_meta = patch_meta_line(dot_meta, state.parent_meta)
|
||||
call_meta = patch_meta_line(call_meta, dot_meta)
|
||||
|
||||
utf8 =
|
||||
if args == [] or interpolated?(string) do
|
||||
# a non-normalized :utf8 atom signals an atom interpolation
|
||||
:utf8
|
||||
else
|
||||
normalize_literal(:utf8, [], state)
|
||||
end
|
||||
|
||||
string =
|
||||
if state.escape do
|
||||
normalize_bitstring(string, state, true)
|
||||
@@ -84,7 +92,7 @@ defmodule Code.Normalizer do
|
||||
normalize_bitstring(string, state)
|
||||
end
|
||||
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, :utf8]}
|
||||
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
|
||||
end
|
||||
|
||||
# Charlists with interpolations
|
||||
@@ -201,7 +209,7 @@ defmodule Code.Normalizer do
|
||||
defp do_normalize({:@, meta, [{name, name_meta, [value]}]}, state) do
|
||||
value =
|
||||
cond do
|
||||
keyword?(value) ->
|
||||
keyword?(value) and value != [] ->
|
||||
normalize_kw_args(value, state, true)
|
||||
|
||||
is_list(value) ->
|
||||
|
||||
+16
-2
@@ -119,7 +119,8 @@ defmodule Float do
|
||||
returned.
|
||||
|
||||
If the size of float exceeds the maximum size of `1.7976931348623157e+308`,
|
||||
the `ArgumentError` exception is raised.
|
||||
`:error` is returned even though the textual representation itself might be
|
||||
well formed.
|
||||
|
||||
If you want to convert a string-formatted float directly to a float,
|
||||
`String.to_float/1` can be used instead.
|
||||
@@ -135,6 +136,8 @@ defmodule Float do
|
||||
|
||||
iex> Float.parse("pi")
|
||||
:error
|
||||
iex> Float.parse("1.7976931348623159e+308")
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec parse(binary) :: {float, binary} | :error
|
||||
@@ -172,7 +175,18 @@ defmodule Float do
|
||||
when exp_marker in 'eE' and sign in '-+' and digit in ?0..?9,
|
||||
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
|
||||
|
||||
defp parse_unsigned(rest, dot?, _e?, acc),
|
||||
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
|
||||
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
|
||||
# rescue the ArgumentError here and return an error.
|
||||
defp parse_unsigned(rest, dot?, true = _e?, acc) do
|
||||
:erlang.binary_to_float(add_dot(acc, dot?))
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
else
|
||||
float -> {float, rest}
|
||||
end
|
||||
|
||||
defp parse_unsigned(rest, dot?, false = _e?, acc),
|
||||
do: {:erlang.binary_to_float(add_dot(acc, dot?)), rest}
|
||||
|
||||
defp add_dot(acc, true), do: acc
|
||||
|
||||
+52
-33
@@ -3587,35 +3587,34 @@ defmodule Kernel do
|
||||
# Those are always compile-time dependencies, so we can skip the trace.
|
||||
defp collect_traces(:before_compile, arg, _env), do: {arg, []}
|
||||
defp collect_traces(:after_compile, arg, _env), do: {arg, []}
|
||||
defp collect_traces(:after_verify, arg, _env), do: {arg, []}
|
||||
defp collect_traces(:on_definition, arg, _env), do: {arg, []}
|
||||
|
||||
defp collect_traces(_name, arg, env) do
|
||||
case is_pid(env.lexical_tracker) and Macro.quoted_literal?(arg) do
|
||||
true ->
|
||||
env = %{env | function: {:__info__, 1}}
|
||||
defp collect_traces(_name, arg, %{lexical_tracker: pid} = env) when is_pid(pid) do
|
||||
env = %{env | function: {:__info__, 1}}
|
||||
|
||||
{arg, aliases} =
|
||||
Macro.prewalk(arg, %{}, fn
|
||||
{:__aliases__, _, _} = alias, acc ->
|
||||
case Macro.expand(alias, env) do
|
||||
atom when is_atom(atom) -> {atom, Map.put(acc, atom, [])}
|
||||
_ -> {alias, acc}
|
||||
end
|
||||
{arg, aliases} =
|
||||
Macro.expand_literals(arg, %{}, fn
|
||||
{:__aliases__, _, _} = alias, acc ->
|
||||
case Macro.expand(alias, env) do
|
||||
atom when is_atom(atom) -> {atom, Map.put(acc, atom, [])}
|
||||
_ -> {alias, acc}
|
||||
end
|
||||
|
||||
node, acc ->
|
||||
{node, acc}
|
||||
end)
|
||||
node, acc ->
|
||||
{Macro.expand(node, env), acc}
|
||||
end)
|
||||
|
||||
case map_size(aliases) do
|
||||
0 -> {arg, []}
|
||||
_ -> {arg, [{env.line, env.lexical_tracker, env.tracers, :maps.keys(aliases)}]}
|
||||
end
|
||||
|
||||
false ->
|
||||
{arg, []}
|
||||
case map_size(aliases) do
|
||||
0 -> {arg, []}
|
||||
_ -> {arg, [{env.line, env.lexical_tracker, env.tracers, :maps.keys(aliases)}]}
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_traces(_name, arg, _env) do
|
||||
{arg, []}
|
||||
end
|
||||
|
||||
defp typespec?(:type), do: true
|
||||
defp typespec?(:typep), do: true
|
||||
defp typespec?(:opaque), do: true
|
||||
@@ -4801,11 +4800,27 @@ defmodule Kernel do
|
||||
:elixir_quote.escape(block, :none, false)
|
||||
end
|
||||
|
||||
module_vars = :lists.map(&module_var/1, :maps.keys(env.versioned_vars))
|
||||
versioned_vars = env.versioned_vars
|
||||
prune = :erlang.is_map_key({:elixir, :prune_binding}, versioned_vars)
|
||||
|
||||
var_meta =
|
||||
case prune do
|
||||
true -> [generated: true, keep_unused: true]
|
||||
false -> [generated: false]
|
||||
end
|
||||
|
||||
module_vars = :lists.map(&module_var(&1, var_meta), :maps.keys(versioned_vars))
|
||||
|
||||
quote do
|
||||
unquote(with_alias)
|
||||
:elixir_module.compile(unquote(expanded), unquote(escaped), unquote(module_vars), __ENV__)
|
||||
|
||||
:elixir_module.compile(
|
||||
unquote(expanded),
|
||||
unquote(escaped),
|
||||
unquote(module_vars),
|
||||
unquote(prune),
|
||||
__ENV__
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4854,8 +4869,8 @@ defmodule Kernel do
|
||||
{module, module, nil}
|
||||
end
|
||||
|
||||
defp module_var({name, kind}) when is_atom(kind), do: {name, [generated: true], kind}
|
||||
defp module_var({name, kind}), do: {name, [counter: kind, generated: true], nil}
|
||||
defp module_var({name, kind}, meta) when is_atom(kind), do: {name, meta, kind}
|
||||
defp module_var({name, kind}, meta), do: {name, [counter: kind] ++ meta, nil}
|
||||
|
||||
@doc ~S"""
|
||||
Defines a public function with the given name and body.
|
||||
@@ -5745,7 +5760,7 @@ defmodule Kernel do
|
||||
"""
|
||||
defmacro defdelegate(funs, opts) do
|
||||
funs = Macro.escape(funs, unquote: true)
|
||||
opts = Macro.expand_literal(opts, %{__CALLER__ | function: {:__info__, 1}})
|
||||
opts = Macro.expand_literals(opts, %{__CALLER__ | function: {:__info__, 1}})
|
||||
|
||||
quote bind_quoted: [funs: funs, opts: opts] do
|
||||
target = Kernel.Utils.defdelegate_all(funs, opts, __ENV__)
|
||||
@@ -5806,18 +5821,22 @@ defmodule Kernel do
|
||||
## Configuring the debug function
|
||||
|
||||
One of the benefits of `dbg/2` is that its debugging logic is configurable,
|
||||
allowing tools to extend `dbg` with enhanced behaviour. The debug function
|
||||
can be configured at compile time through the `:dbg_callback` key of the `:elixir`
|
||||
application. The debug function must be a `{module, function, args}` tuple.
|
||||
The `function` function in `module` will be invoked with three arguments
|
||||
*prepended* to `args`:
|
||||
allowing tools to extend `dbg` with enhanced behaviour. This is done, for
|
||||
example, by `IEx` which extends `dbg` with an interactive shell where you
|
||||
can directly inspect and access values.
|
||||
|
||||
The debug function can be configured at compile time through the `:dbg_callback`
|
||||
key of the `:elixir` application. The debug function must be a
|
||||
`{module, function, args}` tuple. The `function` function in `module` will be
|
||||
invoked with three arguments *prepended* to `args`:
|
||||
|
||||
1. The AST of `code`
|
||||
2. The AST of `options`
|
||||
3. The `Macro.Env` environment of where `dbg/2` is invoked
|
||||
|
||||
Whatever is returned by the debug function is then the return value of `dbg/2`. The
|
||||
debug function is invoked at compile time.
|
||||
The debug function is invoked at compile time and it must also return an AST.
|
||||
The AST is expected to ultimately return the result of evaluating the debugged
|
||||
expression.
|
||||
|
||||
Here's a simple example:
|
||||
|
||||
|
||||
@@ -138,10 +138,17 @@ defmodule Kernel.Utils do
|
||||
fields = :lists.map(mapper, fields)
|
||||
|
||||
enforce_keys =
|
||||
case :ets.take(set, :enforce_keys) do
|
||||
[{_, enforce_keys, _, _}] when is_list(enforce_keys) -> enforce_keys
|
||||
[{_, enforce_key, _, _}] -> [enforce_key]
|
||||
[] -> []
|
||||
case :ets.lookup(set, :enforce_keys) do
|
||||
[{_, enforce_keys, _, _}] when is_list(enforce_keys) ->
|
||||
:ets.update_element(set, :enforce_keys, {3, :used})
|
||||
enforce_keys
|
||||
|
||||
[{_, enforce_key, _, _}] ->
|
||||
:ets.update_element(set, :enforce_keys, {3, :used})
|
||||
[enforce_key]
|
||||
|
||||
[] ->
|
||||
[]
|
||||
end
|
||||
|
||||
# TODO: Make it raise on v2.0
|
||||
@@ -368,7 +375,7 @@ defmodule Kernel.Utils do
|
||||
|
||||
%{} ->
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
new_var = Macro.var(generated, Elixir)
|
||||
new_var = Macro.unique_var(generated, Elixir)
|
||||
{new_var, Map.put(acc, pair, {new_var, var})}
|
||||
end
|
||||
|
||||
|
||||
+82
-19
@@ -1966,42 +1966,105 @@ defmodule Macro do
|
||||
|
||||
def quoted_literal?({:%{}, _, args}), do: quoted_literal?(args)
|
||||
def quoted_literal?({:{}, _, args}), do: quoted_literal?(args)
|
||||
def quoted_literal?({:__MODULE__, _, ctx}) when is_atom(ctx), do: true
|
||||
def quoted_literal?({left, right}), do: quoted_literal?(left) and quoted_literal?(right)
|
||||
def quoted_literal?(list) when is_list(list), do: :lists.all("ed_literal?/1, list)
|
||||
def quoted_literal?(term), do: is_atom(term) or is_number(term) or is_binary(term)
|
||||
|
||||
@doc """
|
||||
Expands the `ast` representing a quoted literal within the given `env`.
|
||||
@doc false
|
||||
@deprecated "Use Macro.expand_literals/2 instead"
|
||||
def expand_literal(ast, env) do
|
||||
expand_literals(ast, env)
|
||||
end
|
||||
|
||||
This function checks if the given AST represents a quoted literal
|
||||
(using `quoted_literal?/1`) and then expands all relevant nodes.
|
||||
If a literal node is not given, then it returns the AST as is.
|
||||
At the moment, the only expandable literal nodes in an AST are
|
||||
aliases, so this function only expands aliases.
|
||||
@doc """
|
||||
Expands all literals in `ast` with the given `env`.
|
||||
|
||||
This function is mostly used to remove compile-time dependencies
|
||||
from AST nodes. In such cases, the given environment is usually
|
||||
manipulate to represent a function:
|
||||
manipulated to represent a function:
|
||||
|
||||
Macro.expand_literal(ast, %{env | function: {:my_code, 1}})
|
||||
Macro.expand_literals(ast, %{env | function: {:my_code, 1}})
|
||||
|
||||
At the moment, the only expandable literal nodes in an AST are
|
||||
aliases, so this function only expands aliases.
|
||||
|
||||
However, be careful when removing compile-time dependencies between
|
||||
modules. If you remove them but you still invoke the module at
|
||||
compile-time, you may end-up with broken behaviour.
|
||||
compile-time, Elixir will be unable to properly recompile modules
|
||||
when they change.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec expand_literal(t(), Macro.Env.t()) :: t()
|
||||
def expand_literal(ast, env) do
|
||||
if quoted_literal?(ast) do
|
||||
prewalk(ast, fn
|
||||
{:__aliases__, _, _} = alias -> expand(alias, env)
|
||||
other -> other
|
||||
end)
|
||||
@doc since: "1.14.1"
|
||||
@spec expand_literals(t(), Macro.Env.t()) :: t()
|
||||
def expand_literals(ast, env) do
|
||||
{ast, :ok} = expand_literals(ast, :ok, fn node, :ok -> {expand(node, env), :ok} end)
|
||||
ast
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands all literals in `ast` with the given `acc` and `fun`.
|
||||
|
||||
`fun` will be invoked with an expandable AST node and `acc` and
|
||||
must return a new node with `acc`. This is a general version of
|
||||
`expand_literals/2` which supports a custom expansion function.
|
||||
Please check `expand_literals/2` for use cases and pitfalls.
|
||||
"""
|
||||
@doc since: "1.14.1"
|
||||
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
|
||||
def expand_literals(ast, acc, fun)
|
||||
|
||||
def expand_literals({:__aliases__, meta, args}, acc, fun) do
|
||||
{args, acc} = expand_literals(args, acc, fun)
|
||||
|
||||
if :lists.all(&is_atom/1, args) do
|
||||
fun.({:__aliases__, meta, args}, acc)
|
||||
else
|
||||
ast
|
||||
{{:__aliases__, meta, args}, acc}
|
||||
end
|
||||
end
|
||||
|
||||
def expand_literals({:__MODULE__, _meta, ctx} = node, acc, fun) when is_atom(ctx) do
|
||||
fun.(node, acc)
|
||||
end
|
||||
|
||||
def expand_literals({:%, meta, [left, right]}, acc, fun) do
|
||||
{left, acc} = expand_literals(left, acc, fun)
|
||||
{right, acc} = expand_literals(right, acc, fun)
|
||||
{{:%, meta, [left, right]}, acc}
|
||||
end
|
||||
|
||||
def expand_literals({:%{}, meta, args}, acc, fun) do
|
||||
{args, acc} = expand_literals(args, acc, fun)
|
||||
{{:%{}, meta, args}, acc}
|
||||
end
|
||||
|
||||
def expand_literals({:{}, meta, args}, acc, fun) do
|
||||
{args, acc} = expand_literals(args, acc, fun)
|
||||
{{:{}, meta, args}, acc}
|
||||
end
|
||||
|
||||
def expand_literals({left, right}, acc, fun) do
|
||||
{left, acc} = expand_literals(left, acc, fun)
|
||||
{right, acc} = expand_literals(right, acc, fun)
|
||||
{{left, right}, acc}
|
||||
end
|
||||
|
||||
def expand_literals(list, acc, fun) when is_list(list) do
|
||||
:lists.mapfoldl(&expand_literals(&1, &2, fun), acc, list)
|
||||
end
|
||||
|
||||
def expand_literals(
|
||||
{{:., _, [{:__aliases__, _, [:Application]}, :compile_env]} = node, meta,
|
||||
[app, key, default]},
|
||||
acc,
|
||||
fun
|
||||
) do
|
||||
{default, acc} = expand_literals(default, acc, fun)
|
||||
{{node, meta, [app, key, default]}, acc}
|
||||
end
|
||||
|
||||
def expand_literals(term, acc, _fun), do: {term, acc}
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it until it can no longer
|
||||
be expanded.
|
||||
|
||||
@@ -532,7 +532,11 @@ defmodule Module do
|
||||
corresponding setting in `Code.get_compiler_option/1`
|
||||
|
||||
* `@compile {:debug_info, false}` - disables `:debug_info` regardless
|
||||
of the corresponding setting in `Code.get_compiler_option/1`
|
||||
of the corresponding setting in `Code.get_compiler_option/1`. Note
|
||||
disabling `:debug_info` is not recommended as it removes the ability
|
||||
of the Elixir compiler and other tools to static analyse the code.
|
||||
If you want to remove the `:debug_info` while deploying, tools like
|
||||
`mix release` already do such by default.
|
||||
|
||||
* `@compile {:inline, some_fun: 2, other_fun: 3}` - inlines the given
|
||||
name/arity pairs. Inlining is applied locally, calls from another
|
||||
@@ -818,7 +822,7 @@ defmodule Module do
|
||||
next = :elixir_module.next_counter(nil)
|
||||
meta = Keyword.take(opts, [:line, :generated])
|
||||
quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted)
|
||||
:elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts))
|
||||
:elixir_module.compile(module, quoted, [], false, :elixir.env_for_eval(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -62,17 +62,24 @@ defmodule Module.ParallelChecker do
|
||||
if is_map(info) do
|
||||
info
|
||||
else
|
||||
info |> File.read!() |> fetch_module_map!(module)
|
||||
info |> File.read!() |> maybe_module_map(module)
|
||||
end
|
||||
|
||||
cache_from_module_map(ets, module_map)
|
||||
module_map && cache_from_module_map(ets, module_map)
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
{^ref, :check} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
warnings = check_module(module_map, {checker, ets})
|
||||
|
||||
warnings =
|
||||
if module_map do
|
||||
check_module(module_map, {checker, ets})
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
send(pid, {__MODULE__, module, warnings})
|
||||
send(checker, {__MODULE__, :done})
|
||||
end
|
||||
@@ -357,11 +364,16 @@ defmodule Module.ParallelChecker do
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
defp fetch_module_map!(binary, module) when is_binary(binary) do
|
||||
{:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
|
||||
{:debug_info_v1, backend, data} = chunk
|
||||
{:ok, module_map} = backend.debug_info(:elixir_v1, module, data, [])
|
||||
module_map
|
||||
defp maybe_module_map(binary, module) when is_binary(binary) do
|
||||
# If a module was compiled without debug_info,
|
||||
# then there is no module_map for further verification.
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
module_map
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_module_map(ets, map) do
|
||||
|
||||
@@ -211,17 +211,21 @@ defmodule Module.Types do
|
||||
## FORMAT WARNINGS
|
||||
|
||||
def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do
|
||||
{module, function, arity} = mfa
|
||||
mfa_args = Macro.generate_arguments(arity, __MODULE__)
|
||||
{module, function, ^arity} = call_to_mfa(erl_to_ex(module, function, mfa_args, []))
|
||||
{original_module, original_function, arity} = mfa
|
||||
{_, _, args} = mfa_or_fa = erl_to_ex(original_module, original_function, args, [])
|
||||
{module, function, ^arity} = call_to_mfa(mfa_or_fa)
|
||||
format_mfa = Exception.format_mfa(module, function, arity)
|
||||
{traces, [] = _hints} = format_traces(traces, [], false)
|
||||
|
||||
clauses =
|
||||
Enum.map(
|
||||
signature,
|
||||
&String.slice(IO.iodata_to_binary(Unify.format_type({:fun, [&1]}, false)), 1..-2)
|
||||
)
|
||||
Enum.map(signature, fn {ins, out} ->
|
||||
{_, _, ins} = erl_to_ex(original_module, original_function, ins, [])
|
||||
|
||||
{:fun, [{ins, out}]}
|
||||
|> Unify.format_type(false)
|
||||
|> IO.iodata_to_binary()
|
||||
|> binary_slice(1..-2//1)
|
||||
end)
|
||||
|
||||
[
|
||||
"expected #{format_mfa} to have signature:\n\n ",
|
||||
|
||||
@@ -153,14 +153,18 @@ defmodule Module.Types.Expr do
|
||||
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
|
||||
{:ok, type, context} <- unify(map_type, args_type, stack, context) do
|
||||
# Retrieve map type and overwrite with the new value types from the map update
|
||||
{:map, pairs} = resolve_var(type, context)
|
||||
case resolve_var(type, context) do
|
||||
{:map, pairs} ->
|
||||
updated_pairs =
|
||||
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
|
||||
List.keyreplace(pairs, key, 1, {:required, key, value})
|
||||
end)
|
||||
|
||||
updated_pairs =
|
||||
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
|
||||
List.keyreplace(pairs, key, 1, {:required, key, value})
|
||||
end)
|
||||
{:ok, {:map, updated_pairs}, context}
|
||||
|
||||
{:ok, {:map, updated_pairs}, context}
|
||||
_ ->
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -128,35 +128,10 @@ defmodule Module.Types.Unify do
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
%{^var => {:var, new_var} = var_type} ->
|
||||
unify_result =
|
||||
cond do
|
||||
recursive_type?(var_type, [], context) ->
|
||||
{:ok, var_type, put_in(context.types[var], var_type)}
|
||||
|
||||
var_source? ->
|
||||
unify(var_type, type, stack, context)
|
||||
|
||||
true ->
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, {type, reason, %{traces: error_traces} = error_context}} ->
|
||||
old_var_traces = Map.get(context.traces, new_var, [])
|
||||
new_var_traces = Map.get(error_traces, new_var, [])
|
||||
add_var_traces = Enum.drop(new_var_traces, -length(old_var_traces))
|
||||
|
||||
error_traces =
|
||||
error_traces
|
||||
|> Map.update(var, add_var_traces, &(add_var_traces ++ &1))
|
||||
|> Map.put(new_var, old_var_traces)
|
||||
|
||||
{:error, {type, reason, %{error_context | traces: error_traces}}}
|
||||
end
|
||||
%{^var => {:var, _} = var_type} ->
|
||||
# Do not recursively traverse type vars for now
|
||||
# to avoid pathological cases related to performance.
|
||||
{:ok, var_type, context}
|
||||
|
||||
%{^var => var_type} ->
|
||||
# Only add trace if the variable wasn't already "expanded"
|
||||
@@ -384,11 +359,11 @@ defmodule Module.Types.Unify do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resolves a variable raising if it is unbound.
|
||||
Maybe resolves a variable.
|
||||
"""
|
||||
def resolve_var({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> raise "cannot resolve unbound var"
|
||||
%{^var => :unbound} -> {:var, var}
|
||||
%{^var => type} -> resolve_var(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -931,7 +931,8 @@ defmodule Protocol do
|
||||
raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module"
|
||||
end)
|
||||
|
||||
for = Macro.expand_literal(for, %{env | module: Kernel, function: {:defimpl, 3}})
|
||||
for =
|
||||
Macro.expand_literals(for, %{env | module: env.module || Elixir, function: {:__impl__, 1}})
|
||||
|
||||
case opts do
|
||||
[] -> raise ArgumentError, "defimpl expects a do-end block"
|
||||
|
||||
@@ -24,7 +24,7 @@ defmodule Range do
|
||||
|
||||
When defining a range without a step, the step will be
|
||||
defined based on the first and last position of the
|
||||
range, If `first >= last`, it will be an increasing range
|
||||
range, If `last >= first`, it will be an increasing range
|
||||
with a step of 1. Otherwise, it is a decreasing range.
|
||||
Note however implicit decreasing ranges are deprecated.
|
||||
Therefore, if you need a decreasing range from `3` to `1`,
|
||||
|
||||
@@ -524,11 +524,11 @@ defmodule String do
|
||||
defp parts_to_index(n) when is_integer(n) and n > 0, do: n
|
||||
|
||||
defp split_empty("", true, 1), do: []
|
||||
defp split_empty(string, _, 1), do: [string]
|
||||
defp split_empty(string, _, 1), do: [IO.iodata_to_binary(string)]
|
||||
|
||||
defp split_empty(string, trim, count) do
|
||||
case :unicode_util.gc(string) do
|
||||
[gc] -> [grapheme_to_binary(gc) | split_empty(<<>>, trim, count - 1)]
|
||||
[gc] -> [grapheme_to_binary(gc) | split_empty("", trim, 1)]
|
||||
[gc | rest] -> [grapheme_to_binary(gc) | split_empty(rest, trim, count - 1)]
|
||||
[] -> split_empty("", trim, 1)
|
||||
{:error, <<byte, rest::bits>>} -> [<<byte>> | split_empty(rest, trim, count - 1)]
|
||||
@@ -1930,6 +1930,7 @@ defmodule String do
|
||||
def next_grapheme(string) when is_binary(string) do
|
||||
case :unicode_util.gc(string) do
|
||||
[gc] -> {grapheme_to_binary(gc), <<>>}
|
||||
[gc, rest] -> {grapheme_to_binary(gc), rest}
|
||||
[gc | rest] -> {grapheme_to_binary(gc), rest}
|
||||
[] -> nil
|
||||
{:error, <<byte, rest::bits>>} -> {<<byte>>, rest}
|
||||
|
||||
@@ -920,19 +920,26 @@ defmodule System do
|
||||
|
||||
## Options
|
||||
|
||||
It accepts the same options as `cmd/3`, except for `arg0`.
|
||||
It accepts the same options as `cmd/3` (except for `arg0`).
|
||||
It also accepts the following exclusive options:
|
||||
|
||||
* `:close_stdin` (since v1.14.1) - if the stdin should be closed
|
||||
on Unix systems, forcing any command that waits on stdin to
|
||||
immediately terminate. Defaults to false.
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec shell(binary, keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
|
||||
def shell(command, opts \\ []) when is_binary(command) do
|
||||
assert_no_null_byte!(command, "System.shell/2")
|
||||
{close_stdin?, opts} = Keyword.pop(opts, :close_stdin, false)
|
||||
|
||||
# Finding shell command logic from :os.cmd in OTP
|
||||
# https://github.com/erlang/otp/blob/8deb96fb1d017307e22d2ab88968b9ef9f1b71d0/lib/kernel/src/os.erl#L184
|
||||
case :os.type() do
|
||||
{:unix, _} ->
|
||||
shell_path = :os.find_executable('sh') || :erlang.error(:enoent, [command, opts])
|
||||
command = "(#{command}\n) </dev/null"
|
||||
shell_path = :os.find_executable(~c"sh") || :erlang.error(:enoent, [command, opts])
|
||||
close_stdin = if close_stdin?, do: " </dev/null", else: ""
|
||||
command = IO.iodata_to_binary(["(", command, "\n)", close_stdin])
|
||||
do_cmd({:spawn_executable, shell_path}, [args: ["-c", command]], opts)
|
||||
|
||||
{:win32, osname} ->
|
||||
|
||||
@@ -648,7 +648,7 @@ defmodule URI do
|
||||
scheme = String.downcase(scheme, :ascii)
|
||||
|
||||
case map do
|
||||
%{port: _} ->
|
||||
%{port: port} when port != :undefined ->
|
||||
%{uri | scheme: scheme}
|
||||
|
||||
%{} ->
|
||||
|
||||
+32
-20
@@ -4,7 +4,8 @@
|
||||
-behaviour(application).
|
||||
-export([start_cli/0,
|
||||
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
|
||||
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3]).
|
||||
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3,
|
||||
eval_quoted/4]).
|
||||
-include("elixir.hrl").
|
||||
-define(system, 'Elixir.System').
|
||||
|
||||
@@ -260,22 +261,27 @@ env_for_eval(Opts) when is_list(Opts) ->
|
||||
|
||||
%% Quoted evaluation
|
||||
|
||||
eval_quoted(Tree, Binding, Opts) when is_list(Opts) ->
|
||||
eval_quoted(Tree, Binding, env_for_eval(Opts));
|
||||
eval_quoted(Tree, Binding, #{line := Line} = E) ->
|
||||
eval_forms(elixir_quote:linify(Line, line, Tree), Binding, E).
|
||||
eval_quoted(Tree, Binding, E) ->
|
||||
eval_quoted(Tree, Binding, E, []).
|
||||
eval_quoted(Tree, Binding, #{line := Line} = E, Opts) ->
|
||||
eval_forms(elixir_quote:linify(Line, line, Tree), Binding, E, Opts).
|
||||
|
||||
eval_forms(Tree, Binding, Opts) when is_list(Opts) ->
|
||||
eval_forms(Tree, Binding, env_for_eval(Opts));
|
||||
eval_forms(Tree, Binding, OrigE) ->
|
||||
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding),
|
||||
eval_forms(Tree, Binding, OrigE, []).
|
||||
eval_forms(Tree, Binding, OrigE, Opts) ->
|
||||
Prune = proplists:get_value(prune_binding, Opts, false),
|
||||
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
|
||||
E = elixir_env:with_vars(OrigE, ExVars),
|
||||
S = elixir_erl_var:from_env(E, ErlVars),
|
||||
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, E, S),
|
||||
ExS = elixir_env:env_to_ex(E),
|
||||
ErlS = elixir_erl_var:from_env(E, ErlVars),
|
||||
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
|
||||
|
||||
case Erl of
|
||||
{atom, _, Atom} ->
|
||||
{Atom, Binding, NewE};
|
||||
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
|
||||
if
|
||||
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
|
||||
true -> {Value, Binding, NewE}
|
||||
end;
|
||||
|
||||
_ ->
|
||||
Exprs =
|
||||
@@ -286,7 +292,12 @@ eval_forms(Tree, Binding, OrigE) ->
|
||||
|
||||
ExternalHandler = eval_external_handler(NewE),
|
||||
{value, Value, NewBinding} = erl_eval:exprs(Exprs, ErlBinding, none, ExternalHandler),
|
||||
{Value, elixir_erl_var:dump_binding(NewBinding, NewExS, NewErlS), NewE}
|
||||
PruneBefore = if Prune -> length(Binding); true -> -1 end,
|
||||
|
||||
{DumpedBinding, DumpedVars} =
|
||||
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
|
||||
|
||||
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
|
||||
end.
|
||||
|
||||
%% TODO: Remove conditional once we require Erlang/OTP 25+.
|
||||
@@ -356,14 +367,15 @@ eval_external_handler(_Env) ->
|
||||
%% Converts a quoted expression to Erlang abstract format
|
||||
|
||||
quoted_to_erl(Quoted, E) ->
|
||||
{_, S} = elixir_erl_var:from_env(E),
|
||||
quoted_to_erl(Quoted, E, S).
|
||||
{_, ErlS} = elixir_erl_var:from_env(E),
|
||||
ExS = elixir_env:env_to_ex(E),
|
||||
quoted_to_erl(Quoted, ErlS, ExS, E).
|
||||
|
||||
quoted_to_erl(Quoted, Env, Scope) ->
|
||||
{Expanded, #elixir_ex{vars={ReadVars, _}} = NewExS, NewEnv} =
|
||||
elixir_expand:expand(Quoted, elixir_env:env_to_ex(Env), Env),
|
||||
{Erl, NewErlS} = elixir_erl_pass:translate(Expanded, erl_anno:new(?key(Env, line)), Scope),
|
||||
{Erl, NewErlS, NewExS, NewEnv#{versioned_vars := ReadVars}}.
|
||||
quoted_to_erl(Quoted, ErlS, ExS, Env) ->
|
||||
{Expanded, NewExS, NewEnv} =
|
||||
elixir_expand:expand(Quoted, ExS, Env),
|
||||
{Erl, NewErlS} = elixir_erl_pass:translate(Expanded, erl_anno:new(?key(Env, line)), ErlS),
|
||||
{Erl, NewErlS, NewExS, NewEnv}.
|
||||
|
||||
%% Converts a given string (charlist) into quote expression
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
'MACRO-defmodule'(_Caller, Alias, [{do, Block}]) ->
|
||||
Escaped = elixir_quote:escape(Block, none, false),
|
||||
Args = [Alias, Escaped, [], env()],
|
||||
Args = [Alias, Escaped, [], false, env()],
|
||||
{{'.', [], [elixir_module, compile]}, [], Args}.
|
||||
|
||||
'__info__'(functions) ->
|
||||
|
||||
@@ -37,7 +37,8 @@ eval_or_compile(Forms, Args, E) ->
|
||||
(not elixir_config:is_bootstrap()) of
|
||||
true -> fast_compile(Forms, E);
|
||||
false -> compile(Forms, Args, E)
|
||||
end.
|
||||
end,
|
||||
ok.
|
||||
|
||||
compile(Quoted, ArgsList, E) ->
|
||||
{Expanded, SE, EE} = elixir_expand:expand(Quoted, elixir_env:env_to_ex(E), E),
|
||||
@@ -47,7 +48,7 @@ compile(Quoted, ArgsList, E) ->
|
||||
elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, E) end),
|
||||
|
||||
Args = list_to_tuple(ArgsList),
|
||||
{dispatch(Module, Fun, Args, Purgeable), EE}.
|
||||
{dispatch(Module, Fun, Args, Purgeable), SE, EE}.
|
||||
|
||||
spawned_compile(ExExprs, #{line := Line, file := File} = E) ->
|
||||
{Vars, S} = elixir_erl_var:from_env(E),
|
||||
@@ -126,7 +127,7 @@ fast_compile({defmodule, Meta, [Mod, [{do, TailBlock}]]}, NoLineE) ->
|
||||
end,
|
||||
|
||||
ContextModules = [Expanded | ?key(E, context_modules)],
|
||||
elixir_module:compile(Expanded, Block, [], E#{context_modules := ContextModules}).
|
||||
elixir_module:compile(Expanded, Block, [], false, E#{context_modules := ContextModules}).
|
||||
|
||||
%% Bootstrapper
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
|
||||
|
||||
check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
|
||||
[elixir_errors:form_warn([{line, Line}], E, ?MODULE, {unused_var, Name, Overridden}) ||
|
||||
{{Name, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
|
||||
{{{Name, nil}, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
|
||||
E.
|
||||
|
||||
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) ->
|
||||
@@ -93,20 +93,18 @@ merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _V
|
||||
|
||||
merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
|
||||
maps:fold(fun
|
||||
({Name, Count} = Key, false, Acc) ->
|
||||
Var = {Name, nil},
|
||||
|
||||
%% The parent knows it, so we have to propagate it was used up.
|
||||
({Var, Count} = Key, false, Acc) ->
|
||||
case Current of
|
||||
#{Var := CurrentCount} when Count =< CurrentCount ->
|
||||
%% The parent knows it, so we have to propagate it was used up.
|
||||
Acc#{Key => false};
|
||||
|
||||
#{} ->
|
||||
Acc
|
||||
end;
|
||||
|
||||
({Name, _Count}, {Line, Overridden}, Acc) ->
|
||||
case is_unused_var(Name) of
|
||||
({{Name, Kind}, _Count}, {Line, Overridden}, Acc) ->
|
||||
case (Kind == nil) andalso is_unused_var(Name) of
|
||||
true ->
|
||||
Warn = {unused_var, Name, Overridden},
|
||||
elixir_errors:form_warn([{line, Line}], E, ?MODULE, Warn);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
-module(elixir_erl_for).
|
||||
-export([translate/4]).
|
||||
-export([translate/3]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
translate(Meta, Args, Return, S) ->
|
||||
translate(Meta, Args, S) ->
|
||||
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
|
||||
|
||||
case lists:keyfind(reduce, 1, Opts) of
|
||||
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
|
||||
false -> translate_into(Meta, Cases, Expr, Opts, Return, S)
|
||||
false -> translate_into(Meta, Cases, Expr, Opts, S)
|
||||
end.
|
||||
|
||||
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
|
||||
@@ -23,14 +23,13 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
|
||||
|
||||
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
|
||||
|
||||
translate_into(Meta, Cases, Expr, Opts, Return, S) ->
|
||||
translate_into(Meta, Cases, Expr, Opts, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
{TInto, SI} =
|
||||
case lists:keyfind(into, 1, Opts) of
|
||||
{into, Into} -> elixir_erl_pass:translate(Into, Ann, S);
|
||||
false when Return -> {{nil, Ann}, S};
|
||||
false -> {false, S}
|
||||
false -> {{nil, Ann}, S}
|
||||
end,
|
||||
|
||||
TUniq = lists:keyfind(uniq, 1, Opts) == {uniq, true},
|
||||
|
||||
@@ -182,7 +182,7 @@ translate({'receive', Meta, [Opts]}, _Ann, S) ->
|
||||
%% Comprehensions and with
|
||||
|
||||
translate({for, Meta, [_ | _] = Args}, _Ann, S) ->
|
||||
elixir_erl_for:translate(Meta, Args, true, S);
|
||||
elixir_erl_for:translate(Meta, Args, S);
|
||||
|
||||
translate({with, Meta, [_ | _] = Args}, _Ann, S) ->
|
||||
{Exprs, [{do, Do} | Opts]} = elixir_utils:split_last(Args),
|
||||
@@ -348,12 +348,6 @@ translate_block([H], Ann, Acc, S) ->
|
||||
translate_block([{'__block__', Meta, Args} | T], Ann, Acc, S) when is_list(Args) ->
|
||||
{TAcc, SA} = translate_block(Args, ?ann(Meta), Acc, S),
|
||||
translate_block(T, Ann, TAcc, SA);
|
||||
translate_block([{for, Meta, [_ | _] = Args} | T], Ann, Acc, S) ->
|
||||
{TH, TS} = elixir_erl_for:translate(Meta, Args, false, S),
|
||||
translate_block(T, Ann, [TH | Acc], TS);
|
||||
translate_block([{'=', _, [{'_', _, Ctx}, {for, Meta, [_ | _] = Args}]} | T], Ann, Acc, S) when is_atom(Ctx) ->
|
||||
{TH, TS} = elixir_erl_for:translate(Meta, Args, false, S),
|
||||
translate_block(T, Ann, [TH | Acc], TS);
|
||||
translate_block([H | T], Ann, Acc, S) ->
|
||||
{TH, TS} = translate(H, Ann, S),
|
||||
translate_block(T, Ann, [TH | Acc], TS).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-module(elixir_erl_var).
|
||||
-export([
|
||||
translate/4, assign/2, build/2,
|
||||
load_binding/1, dump_binding/3,
|
||||
load_binding/2, dump_binding/4,
|
||||
from_env/1, from_env/2
|
||||
]).
|
||||
-include("elixir.hrl").
|
||||
@@ -65,16 +65,16 @@ to_erl_vars([], _Counter) ->
|
||||
to_erl_var(Counter) ->
|
||||
list_to_atom("_@" ++ integer_to_list(Counter)).
|
||||
|
||||
load_binding(Binding) ->
|
||||
load_binding(Binding, #{}, [], [], 0).
|
||||
load_binding(Binding, Prune) ->
|
||||
load_binding(Binding, #{}, [], [], 0, Prune).
|
||||
|
||||
load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter) ->
|
||||
load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter, Prune) ->
|
||||
{Pair, Value} = load_pair(Binding),
|
||||
|
||||
case ExVars of
|
||||
#{Pair := VarCounter} ->
|
||||
ErlVar = to_erl_var(VarCounter),
|
||||
load_binding(NextBindings, ExVars, ErlVars, [{ErlVar, Value} | Normalized], Counter);
|
||||
load_binding(NextBindings, ExVars, ErlVars, [{ErlVar, Value} | Normalized], Counter, Prune);
|
||||
|
||||
#{} ->
|
||||
ErlVar = to_erl_var(Counter),
|
||||
@@ -84,10 +84,13 @@ load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter) ->
|
||||
ExVars#{Pair => Counter},
|
||||
[{Counter, ErlVar} | ErlVars],
|
||||
[{ErlVar, Value} | Normalized],
|
||||
Counter + 1
|
||||
Counter + 1,
|
||||
Prune
|
||||
)
|
||||
end;
|
||||
load_binding([], ExVars, ErlVars, Normalized, _Counter) ->
|
||||
load_binding([], ExVars, ErlVars, Normalized, Counter, true) ->
|
||||
load_binding([{{elixir, prune_binding}, true}], ExVars, ErlVars, Normalized, Counter, false);
|
||||
load_binding([], ExVars, ErlVars, Normalized, _Counter, false) ->
|
||||
%% TODO: Remove me once we require Erlang/OTP 24+
|
||||
%% Also revisit dump_binding below and remove the vars field for simplicity.
|
||||
Mod =
|
||||
@@ -101,9 +104,16 @@ load_binding([], ExVars, ErlVars, Normalized, _Counter) ->
|
||||
load_pair({Key, Value}) when is_atom(Key) -> {{Key, nil}, Value};
|
||||
load_pair({Pair, Value}) -> {Pair, Value}.
|
||||
|
||||
dump_binding(Binding, #elixir_ex{vars={ExVars, _}}, #elixir_erl{var_names=ErlVars}) ->
|
||||
dump_binding(Binding, ErlS, ExS, PruneBefore) ->
|
||||
#elixir_erl{var_names=ErlVars} = ErlS,
|
||||
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
|
||||
|
||||
maps:fold(fun
|
||||
({Var, Kind} = Pair, Version, Acc) when is_atom(Kind) ->
|
||||
({Var, Kind} = Pair, Version, {B, V})
|
||||
when is_atom(Kind),
|
||||
%% If the variable is part of the pruning (usually the input binding)
|
||||
%% and is unused, we removed it from vars.
|
||||
Version > PruneBefore orelse is_map_key({Pair, Version}, Unused) ->
|
||||
Key = case Kind of
|
||||
nil -> Var;
|
||||
_ -> Pair
|
||||
@@ -111,11 +121,14 @@ dump_binding(Binding, #elixir_ex{vars={ExVars, _}}, #elixir_erl{var_names=ErlVar
|
||||
|
||||
ErlName = maps:get(Version, ErlVars),
|
||||
Value = find_binding(ErlName, Binding),
|
||||
[{Key, Value} | Acc];
|
||||
{[{Key, Value} | B], V};
|
||||
|
||||
(Pair, _, {B, V}) when PruneBefore >= 0 ->
|
||||
{B, maps:remove(Pair, V)};
|
||||
|
||||
(_, _, Acc) ->
|
||||
Acc
|
||||
end, [], ExVars).
|
||||
end, {[], ExVars}, ExVars).
|
||||
|
||||
find_binding(ErlName, Binding = #{}) ->
|
||||
case Binding of
|
||||
|
||||
@@ -253,45 +253,8 @@ expand({'try', Meta, [Opts]}, S, E) ->
|
||||
|
||||
%% Comprehensions
|
||||
|
||||
expand({for, Meta, [_ | _] = Args}, S, E) ->
|
||||
assert_no_match_or_guard_scope(Meta, "for", S, E),
|
||||
|
||||
{Cases, Block} =
|
||||
case elixir_utils:split_last(Args) of
|
||||
{OuterCases, OuterOpts} when is_list(OuterOpts) ->
|
||||
case elixir_utils:split_last(OuterCases) of
|
||||
{InnerCases, InnerOpts} when is_list(InnerOpts) ->
|
||||
{InnerCases, InnerOpts ++ OuterOpts};
|
||||
_ ->
|
||||
{OuterCases, OuterOpts}
|
||||
end;
|
||||
_ ->
|
||||
{Args, []}
|
||||
end,
|
||||
|
||||
validate_opts(Meta, for, [do, into, uniq, reduce], Block, E),
|
||||
|
||||
{Expr, Opts} =
|
||||
case lists:keytake(do, 1, Block) of
|
||||
{value, {do, Do}, DoOpts} ->
|
||||
{Do, DoOpts};
|
||||
false ->
|
||||
form_error(Meta, E, ?MODULE, {missing_option, for, [do]})
|
||||
end,
|
||||
|
||||
{EOpts, SO, EO} = expand(Opts, elixir_env:reset_unused_vars(S), E),
|
||||
{ECases, SC, EC} = mapfold(fun expand_for/3, SO, EO, Cases),
|
||||
assert_generator_start(Meta, ECases, E),
|
||||
|
||||
{EExpr, SE, EE} =
|
||||
case validate_for_options(EOpts, false, false, false) of
|
||||
{ok, MaybeReduce} -> expand_for_do_block(Meta, Expr, SC, EC, MaybeReduce);
|
||||
{error, Error} -> form_error(Meta, E, ?MODULE, Error)
|
||||
end,
|
||||
|
||||
{{for, Meta, ECases ++ [[{do, EExpr} | EOpts]]},
|
||||
elixir_env:merge_and_check_unused_vars(SE, S, EE),
|
||||
E};
|
||||
expand({for, _, [_ | _] } = Expr, S, E) ->
|
||||
expand_for(Expr, S, E, true);
|
||||
|
||||
%% With
|
||||
|
||||
@@ -344,7 +307,7 @@ expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_at
|
||||
%% Variable was already overridden
|
||||
#{Pair := VarVersion} when VarVersion >= PrematchVersion ->
|
||||
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E),
|
||||
NewUnused = var_used(Pair, VarVersion, Unused),
|
||||
NewUnused = var_used(Meta, Pair, VarVersion, Unused),
|
||||
Var = {Name, [{version, VarVersion} | Meta], Kind},
|
||||
{Var, S#elixir_ex{unused={NewUnused, Version}}, E};
|
||||
|
||||
@@ -391,7 +354,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
|
||||
{ok, PairVersion} ->
|
||||
maybe_warn_underscored_var_access(Meta, Name, Kind, E),
|
||||
Var = {Name, [{version, PairVersion} | Meta], Kind},
|
||||
{Var, S#elixir_ex{unused={var_used(Pair, PairVersion, Unused), Version}}, E};
|
||||
{Var, S#elixir_ex{unused={var_used(Meta, Pair, PairVersion, Unused), Version}}, E};
|
||||
|
||||
Error ->
|
||||
case lists:keyfind(if_undefined, 1, Meta) of
|
||||
@@ -405,7 +368,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
|
||||
%% TODO: Remove this clause on v2.0
|
||||
_ when Error == warn ->
|
||||
elixir_errors:form_warn(Meta, E, ?MODULE, {unknown_variable, Name}),
|
||||
expand({Name, Meta, []}, S, E);
|
||||
expand({Name, [{if_undefined, warn} | Meta], []}, S, E);
|
||||
|
||||
_ when Error == pin ->
|
||||
form_error(Meta, E, ?MODULE, {undefined_var_pin, Name, Kind});
|
||||
@@ -563,6 +526,12 @@ expand_block([], Acc, _Meta, S, E) ->
|
||||
expand_block([H], Acc, Meta, S, E) ->
|
||||
{EH, SE, EE} = expand(H, S, E),
|
||||
expand_block([], [EH | Acc], Meta, SE, EE);
|
||||
expand_block([{for, _, [_ | _]} = H | T], Acc, Meta, S, E) ->
|
||||
{EH, SE, EE} = expand_for(H, S, E, false),
|
||||
expand_block(T, [EH | Acc], Meta, SE, EE);
|
||||
expand_block([{'=', _, [{'_', _, Ctx}, {for, _, [_ | _]} = H]} | T], Acc, Meta, S, E) when is_atom(Ctx) ->
|
||||
{EH, SE, EE} = expand_for(H, S, E, false),
|
||||
expand_block(T, [EH | Acc], Meta, SE, EE);
|
||||
expand_block([H | T], Acc, Meta, S, E) ->
|
||||
{EH, SE, EE} = expand(H, S, E),
|
||||
|
||||
@@ -638,16 +607,19 @@ mapfold(_Fun, S, E, [], Acc) ->
|
||||
|
||||
%% Match/var helpers
|
||||
|
||||
var_unused({Name, Kind}, Meta, Version, Unused, Override) ->
|
||||
var_unused({_, Kind} = Pair, Meta, Version, Unused, Override) ->
|
||||
case (Kind == nil) andalso should_warn(Meta) of
|
||||
true -> Unused#{{Name, Version} => {?line(Meta), Override}};
|
||||
true -> Unused#{{Pair, Version} => {?line(Meta), Override}};
|
||||
false -> Unused
|
||||
end.
|
||||
|
||||
var_used({Name, Kind}, Version, Unused) ->
|
||||
case Kind of
|
||||
nil -> Unused#{{Name, Version} => false};
|
||||
_ -> Unused
|
||||
var_used(Meta, {_, Kind} = Pair, Version, Unused) ->
|
||||
KeepUnused = lists:keymember(keep_unused, 1, Meta),
|
||||
|
||||
if
|
||||
KeepUnused -> Unused;
|
||||
is_atom(Kind) -> Unused#{{Pair, Version} => false};
|
||||
true -> Unused
|
||||
end.
|
||||
|
||||
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E) ->
|
||||
@@ -741,18 +713,65 @@ generated_case_clauses([{do, Clauses}]) ->
|
||||
|
||||
%% Comprehensions
|
||||
|
||||
validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce) ->
|
||||
validate_for_options(Opts, Pair, Uniq, Reduce);
|
||||
validate_for_options([{uniq, Boolean} = Pair | Opts], Into, _Uniq, Reduce) when is_boolean(Boolean) ->
|
||||
validate_for_options(Opts, Into, Pair, Reduce);
|
||||
validate_for_options([{uniq, Value} | _], _, _, _) ->
|
||||
expand_for({for, Meta, [_ | _] = Args}, S, E, Return) ->
|
||||
assert_no_match_or_guard_scope(Meta, "for", S, E),
|
||||
|
||||
{Cases, Block} =
|
||||
case elixir_utils:split_last(Args) of
|
||||
{OuterCases, OuterOpts} when is_list(OuterOpts) ->
|
||||
case elixir_utils:split_last(OuterCases) of
|
||||
{InnerCases, InnerOpts} when is_list(InnerOpts) ->
|
||||
{InnerCases, InnerOpts ++ OuterOpts};
|
||||
_ ->
|
||||
{OuterCases, OuterOpts}
|
||||
end;
|
||||
_ ->
|
||||
{Args, []}
|
||||
end,
|
||||
|
||||
validate_opts(Meta, for, [do, into, uniq, reduce], Block, E),
|
||||
|
||||
{Expr, Opts} =
|
||||
case lists:keytake(do, 1, Block) of
|
||||
{value, {do, Do}, DoOpts} ->
|
||||
{Do, DoOpts};
|
||||
false ->
|
||||
form_error(Meta, E, ?MODULE, {missing_option, for, [do]})
|
||||
end,
|
||||
|
||||
{EOpts, SO, EO} = expand(Opts, elixir_env:reset_unused_vars(S), E),
|
||||
{ECases, SC, EC} = mapfold(fun expand_for_generator/3, SO, EO, Cases),
|
||||
assert_generator_start(Meta, ECases, E),
|
||||
|
||||
{{EExpr, SE, EE}, NormalizedOpts} =
|
||||
case validate_for_options(EOpts, false, false, false, Return, Meta, E, []) of
|
||||
{ok, MaybeReduce, NOpts} -> {expand_for_do_block(Meta, Expr, SC, EC, MaybeReduce), NOpts};
|
||||
{error, Error} -> {form_error(Meta, E, ?MODULE, Error), EOpts}
|
||||
end,
|
||||
|
||||
{{for, Meta, ECases ++ [[{do, EExpr} | NormalizedOpts]]},
|
||||
elixir_env:merge_and_check_unused_vars(SE, S, EE),
|
||||
E}.
|
||||
|
||||
validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce, Return, Meta, E, Acc) ->
|
||||
validate_for_options(Opts, Pair, Uniq, Reduce, Return, Meta, E, [Pair | Acc]);
|
||||
validate_for_options([{uniq, Boolean} = Pair | Opts], Into, _Uniq, Reduce, Return, Meta, E, Acc) when is_boolean(Boolean) ->
|
||||
validate_for_options(Opts, Into, Pair, Reduce, Return, Meta, E, [Pair | Acc]);
|
||||
validate_for_options([{uniq, Value} | _], _, _, _, _, _, _, _) ->
|
||||
{error, {for_invalid_uniq, Value}};
|
||||
validate_for_options([{reduce, _} = Pair | Opts], Into, Uniq, _Reduce) ->
|
||||
validate_for_options(Opts, Into, Uniq, Pair);
|
||||
validate_for_options([], Into, Uniq, {reduce, _}) when Into /= false; Uniq /= false ->
|
||||
validate_for_options([{reduce, _} = Pair | Opts], Into, Uniq, _Reduce, Return, Meta, E, Acc) ->
|
||||
validate_for_options(Opts, Into, Uniq, Pair, Return, Meta, E, [Pair | Acc]);
|
||||
validate_for_options([], Into, Uniq, {reduce, _}, _Return, _Meta, _E, _Acc) when Into /= false; Uniq /= false ->
|
||||
{error, for_conflicting_reduce_into_uniq};
|
||||
validate_for_options([], _Into, _Uniq, Reduce) ->
|
||||
{ok, Reduce}.
|
||||
validate_for_options([], _Into = false, Uniq, Reduce = false, Return = true, Meta, E, Acc) ->
|
||||
Pair = {into, []},
|
||||
validate_for_options([Pair], Pair, Uniq, Reduce, Return, Meta, E, Acc);
|
||||
validate_for_options([], Into = false, {uniq, true}, Reduce = false, Return = false, Meta, E, Acc) ->
|
||||
elixir_errors:form_warn(Meta, E, ?MODULE, for_with_unused_uniq),
|
||||
AccWithoutUniq = lists:keydelete(uniq, 1, Acc),
|
||||
validate_for_options([], Into, false, Reduce, Return, Meta, E, AccWithoutUniq);
|
||||
validate_for_options([], _Into, _Uniq, Reduce, _Return, _Meta, _E, Acc) ->
|
||||
{ok, Reduce, lists:reverse(Acc)}.
|
||||
|
||||
expand_for_do_block(Meta, [{'->', _, _} | _], _S, E, false) ->
|
||||
form_error(Meta, E, ?MODULE, for_without_reduce_bad_block);
|
||||
@@ -1031,12 +1050,12 @@ expand_aliases({'__aliases__', Meta, _} = Alias, S, E, Report) ->
|
||||
|
||||
%% Comprehensions
|
||||
|
||||
expand_for({'<-', Meta, [Left, Right]}, S, E) ->
|
||||
expand_for_generator({'<-', Meta, [Left, Right]}, S, E) ->
|
||||
{ERight, SR, ER} = expand(Right, S, E),
|
||||
SM = elixir_env:reset_read(SR, S),
|
||||
{[ELeft], SL, EL} = elixir_clauses:head([Left], SM, ER),
|
||||
{{'<-', Meta, [ELeft, ERight]}, SL, EL};
|
||||
expand_for({'<<>>', Meta, Args} = X, S, E) when is_list(Args) ->
|
||||
expand_for_generator({'<<>>', Meta, Args} = X, S, E) when is_list(Args) ->
|
||||
case elixir_utils:split_last(Args) of
|
||||
{LeftStart, {'<-', OpMeta, [LeftEnd, Right]}} ->
|
||||
{ERight, SR, ER} = expand(Right, S, E),
|
||||
@@ -1048,7 +1067,7 @@ expand_for({'<<>>', Meta, Args} = X, S, E) when is_list(Args) ->
|
||||
_ ->
|
||||
expand(X, S, E)
|
||||
end;
|
||||
expand_for(X, S, E) ->
|
||||
expand_for_generator(X, S, E) ->
|
||||
expand(X, S, E).
|
||||
|
||||
assert_generator_start(_, [{'<-', _, [_, _]} | _], _) ->
|
||||
@@ -1170,6 +1189,8 @@ format_error(for_without_reduce_bad_block) ->
|
||||
"the do block was written using acc -> expr clauses but the :reduce option was not given";
|
||||
format_error(for_generator_start) ->
|
||||
"for comprehensions must start with a generator";
|
||||
format_error(for_with_unused_uniq) ->
|
||||
"the :uniq option has no effect since the result of the for comprehension is not used";
|
||||
format_error(unhandled_arrow_op) ->
|
||||
"unhandled operator ->";
|
||||
format_error(as_in_multi_alias_call) ->
|
||||
|
||||
@@ -138,13 +138,19 @@ load_struct(Meta, Name, Args, Keys, E) ->
|
||||
wrapped_load_struct(Meta, Name, Args, Keys, E) ->
|
||||
%% We also include the current module because it won't be present
|
||||
%% in context module in case the module name is defined dynamically.
|
||||
InContext = lists:member(Name, [?key(E, module) | ?key(E, context_modules)]),
|
||||
Module = ?key(E, module),
|
||||
InContext = lists:member(Name, [Module | ?key(E, context_modules)]),
|
||||
|
||||
Arity = length(Args),
|
||||
External = InContext orelse (not(ensure_loaded(Name)) andalso wait_for_struct(Name)),
|
||||
|
||||
try
|
||||
case External andalso elixir_def:external_for(Meta, Name, '__struct__', Arity, [def]) of
|
||||
%% If I am accessing myself and there is no __struct__ function,
|
||||
%% don't invoke the fallback to avoid calling loaded code.
|
||||
false when Module == Name ->
|
||||
error(undef);
|
||||
|
||||
false ->
|
||||
apply(Name, '__struct__', Args);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-module(elixir_module).
|
||||
-export([file/1, data_tables/1, is_open/1, mode/1, delete_definition_attributes/6,
|
||||
compile/4, expand_callback/6, format_error/1, compiler_modules/0,
|
||||
compile/5, expand_callback/6, format_error/1, compiler_modules/0,
|
||||
write_cache/3, read_cache/2, next_counter/1]).
|
||||
-include("elixir.hrl").
|
||||
-define(counter_attr, {elixir, counter}).
|
||||
@@ -64,7 +64,7 @@ next_counter(Module) ->
|
||||
|
||||
%% Compilation hook
|
||||
|
||||
compile(Module, Block, Vars, Env) when is_atom(Module) ->
|
||||
compile(Module, Block, Vars, Prune, Env) when is_atom(Module) ->
|
||||
#{line := Line, function := Function, versioned_vars := OldVerVars} = Env,
|
||||
|
||||
{VerVars, _} =
|
||||
@@ -82,16 +82,16 @@ compile(Module, Block, Vars, Env) when is_atom(Module) ->
|
||||
#{lexical_tracker := nil} ->
|
||||
elixir_lexical:run(
|
||||
MaybeLexEnv,
|
||||
fun(LexEnv) -> compile(Line, Module, Block, Vars, LexEnv) end,
|
||||
fun(LexEnv) -> compile(Line, Module, Block, Vars, Prune, LexEnv) end,
|
||||
fun(_LexEnv) -> ok end
|
||||
);
|
||||
_ ->
|
||||
compile(Line, Module, Block, Vars, MaybeLexEnv)
|
||||
compile(Line, Module, Block, Vars, Prune, MaybeLexEnv)
|
||||
end;
|
||||
compile(Module, _Block, _Vars, #{line := Line, file := File}) ->
|
||||
compile(Module, _Block, _Vars, _Prune, #{line := Line, file := File}) ->
|
||||
elixir_errors:form_error([{line, Line}], File, ?MODULE, {invalid_module, Module}).
|
||||
|
||||
compile(Line, Module, Block, Vars, E) ->
|
||||
compile(Line, Module, Block, Vars, Prune, E) ->
|
||||
File = ?key(E, file),
|
||||
check_module_availability(Line, File, Module),
|
||||
ModuleAsCharlist = validate_module_name(Line, File, Module),
|
||||
@@ -102,7 +102,7 @@ compile(Line, Module, Block, Vars, E) ->
|
||||
|
||||
try
|
||||
put_compiler_modules([Module | CompilerModules]),
|
||||
{Result, NE} = eval_form(Line, Module, DataBag, Block, Vars, E),
|
||||
{Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E),
|
||||
CheckerInfo = checker_info(),
|
||||
|
||||
{Binary, PersistedAttributes, Autoload} =
|
||||
@@ -131,7 +131,10 @@ compile(Line, Module, Block, Vars, E) ->
|
||||
|
||||
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
|
||||
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, File, Line),
|
||||
|
||||
AfterVerify = bag_lookup_element(DataBag, {accumulate, after_verify}, 2),
|
||||
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) ||
|
||||
{VerifyMod, VerifyFun} <- AfterVerify],
|
||||
|
||||
ModuleMap = #{
|
||||
struct => get_struct(DataSet),
|
||||
@@ -155,11 +158,13 @@ compile(Line, Module, Block, Vars, E) ->
|
||||
end),
|
||||
|
||||
Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary),
|
||||
eval_callbacks(Line, DataBag, after_compile, [NE, Binary], NE),
|
||||
elixir_env:trace({on_module, Binary, none}, E),
|
||||
eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE),
|
||||
elixir_env:trace({on_module, Binary, none}, ModuleE),
|
||||
warn_unused_attributes(File, DataSet, DataBag, PersistedAttributes),
|
||||
make_module_available(Module, Binary),
|
||||
(CheckerInfo == undefined) andalso eval_callbacks(Line, DataBag, after_verify, [Module], NE),
|
||||
(CheckerInfo == undefined) andalso
|
||||
[VerifyMod:VerifyFun(Module) ||
|
||||
{VerifyMod, VerifyFun} <- bag_lookup_element(DataBag, {accumulate, after_verify}, 2)],
|
||||
{module, Module, Binary, Result}
|
||||
catch
|
||||
error:undef:Stacktrace ->
|
||||
@@ -370,13 +375,27 @@ build(Line, File, Module) ->
|
||||
|
||||
%% Handles module and callback evaluations.
|
||||
|
||||
eval_form(Line, Module, DataBag, Block, Vars, E) ->
|
||||
{Value, EE} = elixir_compiler:compile(Block, Vars, E),
|
||||
eval_form(Line, Module, DataBag, Block, Vars, Prune, E) ->
|
||||
{Value, ExS, EE} = elixir_compiler:compile(Block, Vars, E),
|
||||
elixir_overridable:store_not_overridden(Module),
|
||||
EV = (elixir_env:reset_vars(EE))#{line := Line},
|
||||
EC = eval_callbacks(Line, DataBag, before_compile, [EV], EV),
|
||||
elixir_overridable:store_not_overridden(Module),
|
||||
{Value, EC}.
|
||||
{Value, maybe_prune_versioned_vars(Prune, Vars, ExS, E), EC}.
|
||||
|
||||
maybe_prune_versioned_vars(false, _Vars, _Exs, E) ->
|
||||
E;
|
||||
maybe_prune_versioned_vars(true, Vars, ExS, E) ->
|
||||
PruneBefore = length(Vars),
|
||||
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
|
||||
|
||||
VersionedVars =
|
||||
maps:filter(fun
|
||||
(Pair, Version) when Version < PruneBefore, not is_map_key({Pair, Version}, Unused) -> false;
|
||||
(_, _) -> true
|
||||
end, ExVars),
|
||||
|
||||
E#{versioned_vars := VersionedVars}.
|
||||
|
||||
eval_callbacks(Line, DataBag, Name, Args, E) ->
|
||||
Callbacks = bag_lookup_element(DataBag, {accumulate, Name}, 2),
|
||||
|
||||
@@ -958,6 +958,15 @@ defmodule DateTimeTest do
|
||||
end
|
||||
|
||||
describe "add" do
|
||||
test "add with invalid time unit" do
|
||||
dt = DateTime.utc_now()
|
||||
|
||||
message =
|
||||
~r/unsupported time unit\. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got "day"/
|
||||
|
||||
assert_raise ArgumentError, message, fn -> DateTime.add(dt, 1, "day") end
|
||||
end
|
||||
|
||||
test "add with non-struct map that conforms to Calendar.datetime" do
|
||||
dt_map = DateTime.from_naive!(~N[2018-08-28 00:00:00], "Etc/UTC") |> Map.from_struct()
|
||||
|
||||
|
||||
@@ -299,6 +299,8 @@ defmodule Code.Formatter.ContainersTest do
|
||||
|
||||
assert_format "<<x::binary()-(13 * 6)-custom>>", "<<x::binary-(13 * 6)-custom()>>"
|
||||
assert_same "<<x::binary-(13 * 6)-custom()>>"
|
||||
assert_same "<<0::size*unit, bytes::binary>>"
|
||||
assert_format "<<0::size*unit, bytes::custom>>", "<<0::size*unit, bytes::custom()>>"
|
||||
|
||||
assert_format "<<0, 1::2-integer() <- x>>", "<<0, 1::2-integer <- x>>"
|
||||
assert_same "<<0, 1::2-integer <- x>>"
|
||||
|
||||
@@ -544,6 +544,12 @@ defmodule Code.Normalizer.QuotedASTTest do
|
||||
end
|
||||
|
||||
test "list in module attribute" do
|
||||
assert quoted_to_string(
|
||||
quote do
|
||||
@foo []
|
||||
end
|
||||
) == "@foo []"
|
||||
|
||||
assert quoted_to_string(
|
||||
quote do
|
||||
@foo [1]
|
||||
@@ -650,6 +656,13 @@ defmodule Code.Normalizer.QuotedASTTest do
|
||||
|
||||
assert quoted_to_string(quote(do: :"one\n\"#{2}\"\nthree")) == ~S[:"one\n\"#{2}\"\nthree"]
|
||||
end
|
||||
|
||||
test ":erlang.binary_to_atom/2 edge cases" do
|
||||
assert quoted_to_string(quote(do: :erlang.binary_to_atom(<<>>, :utf8))) == ~S[:""]
|
||||
|
||||
assert quoted_to_string(quote(do: :erlang.binary_to_atom(<<1>>, :utf8))) ==
|
||||
~S":erlang.binary_to_atom(<<1>>, :utf8)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "quoted_to_algebra/2 does not escape" do
|
||||
|
||||
@@ -48,6 +48,16 @@ defmodule CodeTest do
|
||||
assert Code.eval_string("var!(a, Sample) = 1") == {1, [{{:a, Sample}, 1}]}
|
||||
end
|
||||
|
||||
defmacro hygiene_var do
|
||||
quote do
|
||||
a = 1
|
||||
end
|
||||
end
|
||||
|
||||
test "does not return bindings from macro hygiene" do
|
||||
assert Code.eval_string("require CodeTest; CodeTest.hygiene_var()") == {1, []}
|
||||
end
|
||||
|
||||
test "does not raise on duplicate bindings" do
|
||||
# The order of which values win is not guaranteed, but it should evaluate successfully.
|
||||
assert Code.eval_string("b = String.Chars.to_string(a)", a: 0, a: 1) ==
|
||||
@@ -167,6 +177,72 @@ defmodule CodeTest do
|
||||
assert Macro.Env.vars(env) == [{:x, nil}]
|
||||
end
|
||||
|
||||
test "eval_quoted_with_env/3 with pruning" do
|
||||
env = Code.env_for_eval(__ENV__)
|
||||
|
||||
fun = fn quoted, binding ->
|
||||
{_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
|
||||
{binding, Macro.Env.vars(env)}
|
||||
end
|
||||
|
||||
assert fun.(quote(do: 123), []) == {[], []}
|
||||
assert fun.(quote(do: 123), x: 2, y: 3) == {[], []}
|
||||
|
||||
assert fun.(quote(do: var!(x) = 1), []) == {[x: 1], [x: nil]}
|
||||
assert fun.(quote(do: var!(x) = 1), x: 2, y: 3) == {[x: 1], [x: nil]}
|
||||
|
||||
assert fun.(quote(do: var!(x, :foo) = 1), []) == {[{{:x, :foo}, 1}], [x: :foo]}
|
||||
assert fun.(quote(do: var!(x, :foo) = 1), x: 2, y: 3) == {[{{:x, :foo}, 1}], [x: :foo]}
|
||||
|
||||
assert fun.(quote(do: var!(x, :foo) = 1), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) ==
|
||||
{[{{:x, :foo}, 1}], [x: :foo]}
|
||||
|
||||
assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), []) == {[], []}
|
||||
assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), x: 1, y: 2) == {[], []}
|
||||
|
||||
assert fun.(quote(do: fn -> var!(x) end), x: 2, y: 3) == {[x: 2], [x: nil]}
|
||||
|
||||
assert fun.(quote(do: fn -> var!(x, :foo) end), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) ==
|
||||
{[{{:x, :foo}, 2}], [x: :foo]}
|
||||
end
|
||||
|
||||
defmodule Tracer do
|
||||
def trace(event, env) do
|
||||
send(self(), {:trace, event, env})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
test "eval_quoted_with_env/3 with tracing and pruning" do
|
||||
env = %{Code.env_for_eval(__ENV__) | tracers: [Tracer], function: nil}
|
||||
binding = [x: 1, y: 2, z: 3]
|
||||
|
||||
quoted =
|
||||
quote do
|
||||
defmodule Elixir.CodeTest.TracingPruning do
|
||||
var!(y) = :updated
|
||||
var!(y)
|
||||
var!(x)
|
||||
end
|
||||
end
|
||||
|
||||
{_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
|
||||
assert Enum.sort(binding) == []
|
||||
assert env.versioned_vars == %{}
|
||||
|
||||
assert_receive {:trace, {:on_module, _, _}, %{module: CodeTest.TracingPruning} = trace_env}
|
||||
assert trace_env.versioned_vars == %{{:result, Kernel} => 5, {:x, nil} => 1, {:y, nil} => 4}
|
||||
end
|
||||
|
||||
test "eval_quoted_with_env/3 with defguard" do
|
||||
require Integer
|
||||
env = Code.env_for_eval(__ENV__)
|
||||
quoted = quote do: Integer.is_even(1)
|
||||
{false, binding, env} = Code.eval_quoted_with_env(quoted, [], env, prune_binding: true)
|
||||
assert binding == []
|
||||
assert Macro.Env.vars(env) == []
|
||||
end
|
||||
|
||||
test "compile_file/1" do
|
||||
assert Code.compile_file(fixture_path("code_sample.exs")) == []
|
||||
refute fixture_path("code_sample.exs") in Code.required_files()
|
||||
|
||||
@@ -24,6 +24,7 @@ defmodule FloatTest do
|
||||
assert Float.parse("-12.32453e-10") === {-1.232453e-9, ""}
|
||||
assert Float.parse("0.32453e-10") === {3.2453e-11, ""}
|
||||
assert Float.parse("1.32453e-10") === {1.32453e-10, ""}
|
||||
assert Float.parse("1.7976931348623159e-99999foo") === {0.0, "foo"}
|
||||
assert Float.parse("1.32.45") === {1.32, ".45"}
|
||||
assert Float.parse("1.o") === {1.0, ".o"}
|
||||
assert Float.parse("+12.3E+4") === {1.23e5, ""}
|
||||
@@ -35,10 +36,12 @@ defmodule FloatTest do
|
||||
assert Float.parse("++1.2") === :error
|
||||
assert Float.parse("pi") === :error
|
||||
assert Float.parse("1.7976931348623157e308") === {1.7976931348623157e308, ""}
|
||||
|
||||
assert_raise ArgumentError, fn ->
|
||||
Float.parse("1.7976931348623159e308")
|
||||
end
|
||||
assert Float.parse("1.7976931348623157e308foo") === {1.7976931348623157e308, "foo"}
|
||||
assert Float.parse("1.7976931348623157e+308foo") === {1.7976931348623157e308, "foo"}
|
||||
assert Float.parse("1.7976931348623157e-308foo") === {1.7976931348623155e-308, "foo"}
|
||||
assert Float.parse("1.7976931348623159e308") === :error
|
||||
assert Float.parse("1.7976931348623159e+308") === :error
|
||||
assert Float.parse("9e8363") === :error
|
||||
end
|
||||
|
||||
test "floor/1" do
|
||||
|
||||
@@ -115,6 +115,36 @@ defmodule Kernel.ComprehensionTest do
|
||||
assert for(x <- 1..3, nilly(), do: x * 2) == []
|
||||
end
|
||||
|
||||
test "for comprehensions with unique option where value is not used" do
|
||||
assert capture_io(:stderr, fn ->
|
||||
assert capture_io(fn ->
|
||||
Code.eval_quoted(
|
||||
quote do
|
||||
for x <- [1, 2, 1, 2], uniq: true, do: IO.puts(x)
|
||||
nil
|
||||
end
|
||||
)
|
||||
end) ==
|
||||
"1\n2\n1\n2\n"
|
||||
end) =~
|
||||
"the :uniq option has no effect since the result of the for comprehension is not used"
|
||||
end
|
||||
|
||||
test "for comprehensions with unique option where value is assigned to _" do
|
||||
assert capture_io(:stderr, fn ->
|
||||
assert capture_io(fn ->
|
||||
Code.eval_quoted(
|
||||
quote do
|
||||
_ = for x <- [1, 2, 1, 2], uniq: true, do: IO.puts(x)
|
||||
nil
|
||||
end
|
||||
)
|
||||
end) ==
|
||||
"1\n2\n1\n2\n"
|
||||
end) =~
|
||||
"the :uniq option has no effect since the result of the for comprehension is not used"
|
||||
end
|
||||
|
||||
test "for comprehensions with errors on filters" do
|
||||
assert_raise ArgumentError, fn ->
|
||||
for x <- 1..3, hd(x), do: x * 2
|
||||
|
||||
@@ -204,6 +204,23 @@ defmodule Kernel.ErrorsTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "bad struct on module conflict" do
|
||||
Code.put_compiler_option(:ignore_module_conflict, true)
|
||||
|
||||
assert_eval_raise CompileError, ~r'MissingStructOnReload\.__struct__/1 is undefined', ~c'''
|
||||
defmodule MissingStructOnReload do
|
||||
defstruct [:title]
|
||||
def d(), do: %MissingStructOnReload{}
|
||||
end
|
||||
|
||||
defmodule MissingStructOnReload do
|
||||
def d(), do: %MissingStructOnReload{}
|
||||
end
|
||||
'''
|
||||
after
|
||||
Code.put_compiler_option(:ignore_module_conflict, false)
|
||||
end
|
||||
|
||||
test "missing struct key" do
|
||||
missing_struct_key_error =
|
||||
~r"expected Kernel.ErrorsTest.MissingStructKey.__struct__/(0|1) to return a map.*, got: %\{\}"
|
||||
|
||||
@@ -13,76 +13,6 @@ end
|
||||
defmodule Kernel.ExpansionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
defmacrop var_ver(var, version) do
|
||||
quote do
|
||||
{unquote(var), [version: unquote(version)], __MODULE__}
|
||||
end
|
||||
end
|
||||
|
||||
test "tracks variable version" do
|
||||
assert {:__block__, _, [{:=, _, [var_ver(:x, 0), 0]}, {:=, _, [_, var_ver(:x, 0)]}]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
_ = x
|
||||
end
|
||||
)
|
||||
|
||||
assert {:__block__, _,
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 1), 1]},
|
||||
{:=, _, [_, var_ver(:x, 1)]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
_ = x
|
||||
x = 1
|
||||
_ = x
|
||||
end
|
||||
)
|
||||
|
||||
assert {:__block__, _,
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 3), 3]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
fn x -> x = 2 end
|
||||
_ = x
|
||||
x = 3
|
||||
end
|
||||
)
|
||||
|
||||
assert {:__block__, _,
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 2), 2]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
case(:foo, do: (x -> x))
|
||||
_ = x
|
||||
x = 2
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
defp expand_with_version(expr) do
|
||||
env = :elixir_env.reset_vars(__ENV__)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
expr
|
||||
end
|
||||
|
||||
describe "__block__" do
|
||||
test "expands to nil when empty" do
|
||||
assert expand(quote(do: unquote(:__block__)())) == nil
|
||||
@@ -328,12 +258,30 @@ defmodule Kernel.ExpansionTest do
|
||||
end
|
||||
|
||||
describe "vars" do
|
||||
test "expand to local call" do
|
||||
{output, env} = expand_env(quote(do: a), __ENV__)
|
||||
assert output == quote(do: a())
|
||||
test "expands vars to local call" do
|
||||
{output, env} = expand_env({:a, [], nil}, __ENV__, [])
|
||||
assert output == {:a, [if_undefined: :warn], []}
|
||||
assert Macro.Env.vars(env) == []
|
||||
end
|
||||
|
||||
test "expands vars to local call without warning" do
|
||||
env = __ENV__
|
||||
|
||||
{output, _, env} =
|
||||
:elixir_expand.expand({:a, [if_undefined: :apply], nil}, :elixir_env.env_to_ex(env), env)
|
||||
|
||||
assert output == {:a, [if_undefined: :apply], []}
|
||||
assert Macro.Env.vars(env) == []
|
||||
end
|
||||
|
||||
test "raises when expanding var to local call" do
|
||||
env = __ENV__
|
||||
|
||||
assert_raise CompileError, ~r"undefined variable \"a\"", fn ->
|
||||
:elixir_expand.expand({:a, [if_undefined: :raise], nil}, :elixir_env.env_to_ex(env), env)
|
||||
end
|
||||
end
|
||||
|
||||
test "forces variable to exist" do
|
||||
code =
|
||||
quote do
|
||||
@@ -361,6 +309,76 @@ defmodule Kernel.ExpansionTest do
|
||||
expand(quote(do: {1, 2, _}))
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop var_ver(var, version) do
|
||||
quote do
|
||||
{unquote(var), [version: unquote(version)], __MODULE__}
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_with_version(expr) do
|
||||
env = :elixir_env.reset_vars(__ENV__)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
expr
|
||||
end
|
||||
|
||||
test "tracks variable version" do
|
||||
assert {:__block__, _, [{:=, _, [var_ver(:x, 0), 0]}, {:=, _, [_, var_ver(:x, 0)]}]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
_ = x
|
||||
end
|
||||
)
|
||||
|
||||
assert {:__block__, _,
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 1), 1]},
|
||||
{:=, _, [_, var_ver(:x, 1)]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
_ = x
|
||||
x = 1
|
||||
_ = x
|
||||
end
|
||||
)
|
||||
|
||||
assert {:__block__, _,
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 3), 3]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
fn x -> x = 2 end
|
||||
_ = x
|
||||
x = 3
|
||||
end
|
||||
)
|
||||
|
||||
assert {:__block__, _,
|
||||
[
|
||||
{:=, _, [var_ver(:x, 0), 0]},
|
||||
{:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]},
|
||||
{:=, _, [_, var_ver(:x, 0)]},
|
||||
{:=, _, [var_ver(:x, 2), 2]}
|
||||
]} =
|
||||
expand_with_version(
|
||||
quote do
|
||||
x = 0
|
||||
case(:foo, do: (x -> x))
|
||||
_ = x
|
||||
x = 2
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "^" do
|
||||
@@ -798,7 +816,8 @@ defmodule Kernel.ExpansionTest do
|
||||
c = 3
|
||||
[4]
|
||||
),
|
||||
do: {a(), b, c(), d}
|
||||
do: {a(), b, c(), d},
|
||||
into: []
|
||||
)
|
||||
end
|
||||
|
||||
@@ -807,7 +826,7 @@ defmodule Kernel.ExpansionTest do
|
||||
|
||||
test "variables inside filters are available in blocks" do
|
||||
assert expand(quote(do: for(a <- b, c = a, do: c))) ==
|
||||
quote(do: for(a <- b(), c = a, do: c))
|
||||
quote(do: for(a <- b(), c = a, do: c, into: []))
|
||||
end
|
||||
|
||||
test "variables inside options do not leak" do
|
||||
@@ -2890,15 +2909,13 @@ defmodule Kernel.ExpansionTest do
|
||||
expand_env(expr, __ENV__) |> elem(0)
|
||||
end
|
||||
|
||||
defp expand_env(expr, env) do
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
send(self(), {:expand_env, :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)})
|
||||
end)
|
||||
defp expand_env(expr, env, to_clean \\ [:version, :inferred_bitstring_spec, :if_undefined]) do
|
||||
{{expr, scope, env}, _capture} =
|
||||
ExUnit.CaptureIO.with_io(:stderr, fn ->
|
||||
:elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
end)
|
||||
|
||||
receive do
|
||||
{:expand_env, {expr, scope, env}} ->
|
||||
env = :elixir_env.to_caller({env.line, scope, env})
|
||||
{clean_meta(expr, [:version, :inferred_bitstring_spec]), env}
|
||||
end
|
||||
env = :elixir_env.to_caller({env.line, scope, env})
|
||||
{clean_meta(expr, to_clean), env}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -162,7 +162,7 @@ defmodule Kernel.LexicalTrackerTest do
|
||||
{{compile, _, _, _}, _binding} =
|
||||
Code.eval_string("""
|
||||
defmodule Kernel.LexicalTrackerTest.Attribute1 do
|
||||
@example [String, Enum]
|
||||
@example [String, Enum, 3 + 10]
|
||||
def foo(atom) when atom in @example, do: atom
|
||||
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
|
||||
end |> elem(3)
|
||||
@@ -226,6 +226,18 @@ defmodule Kernel.LexicalTrackerTest do
|
||||
|
||||
assert String in compile
|
||||
assert Enum in compile
|
||||
|
||||
{{compile, _, _, _}, _binding} =
|
||||
Code.eval_string("""
|
||||
defmodule Kernel.LexicalTrackerTest.Attribute6 do
|
||||
@example %{foo: Application.compile_env(:elixir, Enum, String)}
|
||||
def foo(atom) when atom == @example.foo, do: atom
|
||||
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
|
||||
end |> elem(3)
|
||||
""")
|
||||
|
||||
refute String in compile
|
||||
refute Enum in compile
|
||||
end
|
||||
|
||||
test "@compile adds a runtime dependency" do
|
||||
|
||||
@@ -269,9 +269,26 @@ defmodule MacroTest do
|
||||
assert expand_and_clean(quote(do: oror(1, false)), __ENV__) == quoted
|
||||
end
|
||||
|
||||
test "expand_literal" do
|
||||
assert Macro.expand_literal(quote(do: Foo), __ENV__) == Foo
|
||||
assert Macro.expand_literal(quote(do: Foo + Bar), __ENV__) == quote(do: Foo + Bar)
|
||||
test "expand_literals/2" do
|
||||
assert Macro.expand_literals(quote(do: Foo), __ENV__) == Foo
|
||||
assert Macro.expand_literals(quote(do: Foo + Bar), __ENV__) == quote(do: Foo + Bar)
|
||||
assert Macro.expand_literals(quote(do: __MODULE__), __ENV__) == __MODULE__
|
||||
assert Macro.expand_literals(quote(do: __MODULE__.Foo), __ENV__) == __MODULE__.Foo
|
||||
assert Macro.expand_literals(quote(do: [Foo, 1 + 2]), __ENV__) == [Foo, quote(do: 1 + 2)]
|
||||
end
|
||||
|
||||
test "expand_literals/3" do
|
||||
fun = fn node, acc ->
|
||||
expanded = Macro.expand(node, __ENV__)
|
||||
{expanded, [expanded | acc]}
|
||||
end
|
||||
|
||||
assert Macro.expand_literals(quote(do: Foo), [], fun) == {Foo, [Foo]}
|
||||
assert Macro.expand_literals(quote(do: Foo + Bar), [], fun) == {quote(do: Foo + Bar), []}
|
||||
assert Macro.expand_literals(quote(do: __MODULE__), [], fun) == {__MODULE__, [__MODULE__]}
|
||||
|
||||
assert Macro.expand_literals(quote(do: __MODULE__.Foo), [], fun) ==
|
||||
{__MODULE__.Foo, [__MODULE__.Foo, __MODULE__]}
|
||||
end
|
||||
|
||||
test "var/2" do
|
||||
|
||||
@@ -393,6 +393,10 @@ defmodule MapTest do
|
||||
defmodule TestMod do
|
||||
@enforce_keys :foo
|
||||
defstruct [:foo]
|
||||
|
||||
# Verify it remain set afterwards
|
||||
:foo = @enforce_keys
|
||||
|
||||
def foo do
|
||||
%TestMod{}
|
||||
end
|
||||
|
||||
@@ -222,7 +222,6 @@ defmodule Module.Types.PatternTest do
|
||||
|
||||
assert quoted_pattern(x = y = 123) == {:ok, :integer}
|
||||
assert quoted_pattern(x = 123 = y) == {:ok, :integer}
|
||||
assert quoted_pattern(123 = x = y) == {:ok, :integer}
|
||||
|
||||
assert {:error, {:unable_unify, {{:tuple, 1, [var: 0]}, {:var, 0}, _}}} =
|
||||
quoted_pattern({x} = x)
|
||||
@@ -296,15 +295,6 @@ defmodule Module.Types.PatternTest do
|
||||
|
||||
assert quoted_head([x], [is_atom(x) > :foo]) == {:ok, [var: 0]}
|
||||
|
||||
assert quoted_head([x, x = y, y = z], [is_atom(x)]) ==
|
||||
{:ok, [:atom, :atom, :atom]}
|
||||
|
||||
assert quoted_head([x = y, y, y = z], [is_atom(y)]) ==
|
||||
{:ok, [:atom, :atom, :atom]}
|
||||
|
||||
assert quoted_head([x = y, y = z, z], [is_atom(z)]) ==
|
||||
{:ok, [:atom, :atom, :atom]}
|
||||
|
||||
assert quoted_head([x, y], [is_atom(x) or is_integer(y)]) ==
|
||||
{:ok, [{:var, 0}, {:var, 1}]}
|
||||
|
||||
@@ -320,15 +310,6 @@ defmodule Module.Types.PatternTest do
|
||||
assert quoted_head([x, y], [is_atom(y) or is_integer(y)]) ==
|
||||
{:ok, [{:var, 0}, {:union, [:atom, :integer]}]}
|
||||
|
||||
assert quoted_head([x = y], [is_atom(y) or is_integer(y)]) ==
|
||||
{:ok, [{:union, [:atom, :integer]}]}
|
||||
|
||||
assert quoted_head([x = y], [is_atom(x) or is_integer(x)]) ==
|
||||
{:ok, [{:union, [:atom, :integer]}]}
|
||||
|
||||
assert quoted_head([x = y], [is_atom(x) or is_integer(x)]) ==
|
||||
{:ok, [{:union, [:atom, :integer]}]}
|
||||
|
||||
assert quoted_head([x], [true == false or is_integer(x)]) ==
|
||||
{:ok, [var: 0]}
|
||||
|
||||
|
||||
@@ -187,36 +187,6 @@ defmodule Module.Types.TypesTest do
|
||||
"""
|
||||
end
|
||||
|
||||
test "warns on guards with multiple variables" do
|
||||
string = warning([x = y], [is_integer(x) and is_binary(y)], {x, y})
|
||||
|
||||
assert string == """
|
||||
incompatible types:
|
||||
|
||||
integer() !~ binary()
|
||||
|
||||
in expression:
|
||||
|
||||
# types_test.ex:1
|
||||
is_binary(y)
|
||||
|
||||
where "x" was given the same type as "y" in:
|
||||
|
||||
# types_test.ex:1
|
||||
x = y
|
||||
|
||||
where "y" was given the type integer() in:
|
||||
|
||||
# types_test.ex:1
|
||||
is_integer(x)
|
||||
|
||||
where "y" was given the type binary() in:
|
||||
|
||||
# types_test.ex:1
|
||||
is_binary(y)
|
||||
"""
|
||||
end
|
||||
|
||||
test "warns on guards from cases unless generated" do
|
||||
string =
|
||||
warning(
|
||||
@@ -243,36 +213,6 @@ defmodule Module.Types.TypesTest do
|
||||
assert string == :none
|
||||
end
|
||||
|
||||
test "only show relevant traces in warning" do
|
||||
string = warning([x = y, z], [is_integer(x) and is_binary(y) and is_boolean(z)], {x, y, z})
|
||||
|
||||
assert string == """
|
||||
incompatible types:
|
||||
|
||||
integer() !~ binary()
|
||||
|
||||
in expression:
|
||||
|
||||
# types_test.ex:1
|
||||
is_binary(y)
|
||||
|
||||
where "x" was given the same type as "y" in:
|
||||
|
||||
# types_test.ex:1
|
||||
x = y
|
||||
|
||||
where "y" was given the type integer() in:
|
||||
|
||||
# types_test.ex:1
|
||||
is_integer(x)
|
||||
|
||||
where "y" was given the type binary() in:
|
||||
|
||||
# types_test.ex:1
|
||||
is_binary(y)
|
||||
"""
|
||||
end
|
||||
|
||||
test "check body" do
|
||||
string = warning([x], [is_integer(x)], :foo = x)
|
||||
|
||||
@@ -417,6 +357,25 @@ defmodule Module.Types.TypesTest do
|
||||
foo - :bar
|
||||
"""
|
||||
end
|
||||
|
||||
test "rewrite call" do
|
||||
string = warning([foo], [is_map_key(1, foo)], foo)
|
||||
|
||||
assert string == """
|
||||
expected Kernel.is_map_key/2 to have signature:
|
||||
|
||||
integer(), var1 -> dynamic()
|
||||
|
||||
but it has signature:
|
||||
|
||||
%{optional(dynamic()) => dynamic()}, dynamic() -> dynamic()
|
||||
|
||||
in expression:
|
||||
|
||||
# types_test.ex:1
|
||||
is_map_key(1, foo)
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
describe "map warnings" do
|
||||
|
||||
@@ -74,6 +74,14 @@ defmodule ProtocolTest do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ImplStructExplicitFor do
|
||||
defstruct a: 0, b: 0
|
||||
|
||||
defimpl Sample, for: __MODULE__ do
|
||||
def ok(_struct), do: true
|
||||
end
|
||||
end
|
||||
|
||||
test "protocol implementations without any" do
|
||||
assert is_nil(Sample.impl_for(:foo))
|
||||
assert is_nil(Sample.impl_for(fn x -> x end))
|
||||
@@ -91,6 +99,7 @@ defmodule ProtocolTest do
|
||||
assert is_nil(Sample.impl_for(make_ref()))
|
||||
|
||||
assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct
|
||||
assert Sample.impl_for(%ImplStructExplicitFor{}) == Sample.ProtocolTest.ImplStructExplicitFor
|
||||
assert Sample.impl_for(%NoImplStruct{}) == nil
|
||||
end
|
||||
|
||||
|
||||
@@ -107,6 +107,20 @@ defmodule StringTest do
|
||||
assert String.split("x-x-", pattern, parts: 3, trim: true) == ["x", "x"]
|
||||
end
|
||||
|
||||
test "split/2,3 with malformed" do
|
||||
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 1) ==
|
||||
[<<225, 158, 128, 225, 158, 185, 225>>]
|
||||
|
||||
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 2) ==
|
||||
["", <<225, 158, 128, 225, 158, 185, 225>>]
|
||||
|
||||
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 3) ==
|
||||
["", "កឹ", <<225>>]
|
||||
|
||||
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 4) ==
|
||||
["", "កឹ", <<225>>, ""]
|
||||
end
|
||||
|
||||
test "splitter/2,3" do
|
||||
assert String.splitter("a,b,c", ",") |> Enum.to_list() == ["a", "b", "c"]
|
||||
assert String.splitter("a,b", ".") |> Enum.to_list() == ["a,b"]
|
||||
@@ -155,9 +169,18 @@ defmodule StringTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "split_at/2 with invalid guard" do
|
||||
test "split_at/2 with malformed" do
|
||||
assert String.split_at(<<?a, 195, 10, ?a>>, 2) == {<<?a, 195>>, <<10, ?a>>}
|
||||
assert String.split_at(<<107, 205, 135, 184>>, 1) == {<<107, 205, 135>>, <<184>>}
|
||||
|
||||
assert String.split_at(<<225, 158, 128, 225, 158, 185, 225>>, 0) ==
|
||||
{"", <<225, 158, 128, 225, 158, 185, 225>>}
|
||||
|
||||
assert String.split_at(<<225, 158, 128, 225, 158, 185, 225>>, 1) ==
|
||||
{"កឹ", <<225>>}
|
||||
|
||||
assert String.split_at(<<225, 158, 128, 225, 158, 185, 225>>, 2) ==
|
||||
{<<225, 158, 128, 225, 158, 185, 225>>, ""}
|
||||
end
|
||||
|
||||
test "upcase/1" do
|
||||
@@ -457,6 +480,10 @@ defmodule StringTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "with empty string and string replacement with malformed" do
|
||||
assert String.replace(<<225, 158, 128, 225, 158, 185, 225>>, "", ".") == ".កឹ.\xE1."
|
||||
end
|
||||
|
||||
test "with empty pattern list" do
|
||||
assert String.replace("elixir", [], "anything") == "elixir"
|
||||
end
|
||||
@@ -587,6 +614,60 @@ defmodule StringTest do
|
||||
assert String.next_grapheme("") == nil
|
||||
end
|
||||
|
||||
describe "randomized" do
|
||||
test "next_grapheme" do
|
||||
for _ <- 1..10 do
|
||||
bin = :crypto.strong_rand_bytes(20)
|
||||
|
||||
try do
|
||||
bin |> Stream.unfold(&String.next_grapheme/1) |> Enum.to_list()
|
||||
rescue
|
||||
# Ignore malformed pictographic sequences
|
||||
_ -> :ok
|
||||
else
|
||||
list ->
|
||||
assert Enum.all?(list, &is_binary/1), "cannot build graphemes for #{inspect(bin)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "split empty" do
|
||||
for _ <- 1..10 do
|
||||
bin = :crypto.strong_rand_bytes(20)
|
||||
|
||||
try do
|
||||
String.split(bin, "")
|
||||
rescue
|
||||
# Ignore malformed pictographic sequences
|
||||
_ -> :ok
|
||||
else
|
||||
split ->
|
||||
assert Enum.all?(split, &is_binary/1), "cannot split #{inspect(bin)}"
|
||||
assert IO.iodata_to_binary(split) == bin
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "graphemes" do
|
||||
for _ <- 1..10 do
|
||||
bin = :crypto.strong_rand_bytes(20)
|
||||
|
||||
try do
|
||||
String.graphemes(bin)
|
||||
rescue
|
||||
# Ignore malformed pictographic sequences
|
||||
_ -> :ok
|
||||
else
|
||||
graphemes ->
|
||||
assert Enum.all?(graphemes, &is_binary/1),
|
||||
"cannot build graphemes for #{inspect(bin)}"
|
||||
|
||||
assert IO.iodata_to_binary(graphemes) == bin
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "first/1" do
|
||||
assert String.first("elixir") == "e"
|
||||
assert String.first("íelixr") == "í"
|
||||
|
||||
@@ -203,7 +203,7 @@ defmodule SystemTest do
|
||||
|
||||
@tag timeout: 1_000
|
||||
test "shell/1 returns when command awaits input" do
|
||||
assert {"", 0} = System.shell("cat")
|
||||
assert {"", 0} = System.shell("cat", close_stdin: true)
|
||||
end
|
||||
|
||||
test "shell/1 with comment" do
|
||||
|
||||
@@ -279,6 +279,31 @@ defmodule URITest do
|
||||
end
|
||||
end
|
||||
|
||||
test "http://http://http://@http://http://?http://#http://" do
|
||||
assert URI.parse("http://http://http://@http://http://?http://#http://") ==
|
||||
%URI{
|
||||
scheme: "http",
|
||||
authority: "http:",
|
||||
userinfo: nil,
|
||||
host: "http",
|
||||
port: 80,
|
||||
path: "//http://@http://http://",
|
||||
query: "http://",
|
||||
fragment: "http://"
|
||||
}
|
||||
|
||||
assert URI.new!("http://http://http://@http://http://?http://#http://") ==
|
||||
%URI{
|
||||
scheme: "http",
|
||||
userinfo: nil,
|
||||
host: "http",
|
||||
port: 80,
|
||||
path: "//http://@http://http://",
|
||||
query: "http://",
|
||||
fragment: "http://"
|
||||
}
|
||||
end
|
||||
|
||||
test "default_port/1,2" do
|
||||
assert URI.default_port("http") == 80
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
eval(Content) ->
|
||||
{Value, Binding, _} =
|
||||
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
|
||||
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
|
||||
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
|
||||
{Value, Binding}.
|
||||
|
||||
kv([{Key, nil}]) -> Key.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
eval(Content) ->
|
||||
{Value, Binding, _} =
|
||||
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
|
||||
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
|
||||
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
|
||||
{Value, lists:sort(Binding)}.
|
||||
|
||||
function_arg_do_end_test() ->
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
eval(Content) ->
|
||||
{Value, Binding, _} =
|
||||
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
|
||||
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
|
||||
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
|
||||
{Value, Binding}.
|
||||
|
||||
extract_interpolations(String) ->
|
||||
|
||||
@@ -201,7 +201,7 @@ defmodule ExUnit do
|
||||
|
||||
System.at_exit(fn
|
||||
0 ->
|
||||
time = ExUnit.Server.modules_loaded()
|
||||
time = ExUnit.Server.modules_loaded(false)
|
||||
options = persist_defaults(configuration())
|
||||
%{failures: failures} = ExUnit.Runner.run(options, time)
|
||||
|
||||
@@ -373,7 +373,7 @@ defmodule ExUnit do
|
||||
end
|
||||
end
|
||||
|
||||
_ = ExUnit.Server.modules_loaded()
|
||||
_ = ExUnit.Server.modules_loaded(additional_modules != [])
|
||||
options = persist_defaults(configuration())
|
||||
ExUnit.Runner.run(options, nil)
|
||||
end
|
||||
@@ -399,7 +399,7 @@ defmodule ExUnit do
|
||||
@doc since: "1.12.0"
|
||||
@spec await_run(Task.t()) :: suite_result()
|
||||
def await_run(task) do
|
||||
ExUnit.Server.modules_loaded()
|
||||
ExUnit.Server.modules_loaded(false)
|
||||
Task.await(task, :infinity)
|
||||
end
|
||||
|
||||
|
||||
@@ -260,19 +260,21 @@ defmodule ExUnit.Assertions do
|
||||
quote do
|
||||
left = unquote(left)
|
||||
right = unquote(right)
|
||||
expr = unquote(expr)
|
||||
message = unquote(message)
|
||||
|
||||
if ExUnit.Assertions.__equal__?(left, right) do
|
||||
ExUnit.Assertions.assert(false,
|
||||
left: left,
|
||||
expr: unquote(expr),
|
||||
message: unquote(message <> ", both sides are exactly equal")
|
||||
expr: expr,
|
||||
message: message <> ", both sides are exactly equal"
|
||||
)
|
||||
else
|
||||
ExUnit.Assertions.assert(unquote(call),
|
||||
left: left,
|
||||
right: right,
|
||||
expr: unquote(expr),
|
||||
message: unquote(message),
|
||||
expr: expr,
|
||||
message: message,
|
||||
context: unquote(context)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -551,8 +551,11 @@ defmodule ExUnit.Callbacks do
|
||||
@doc """
|
||||
Same as `start_supervised!/2` but links the started process to the test process.
|
||||
|
||||
This means that if the process that was started crashes that crash is propagated to
|
||||
the test process, failing the test and printing the cause of the crash.
|
||||
If the process that was started crashes, the crash is propagated to the test process,
|
||||
failing the test and printing the cause of the crash.
|
||||
|
||||
Note that if the started terminates before it is linked to the test process,
|
||||
this function will exit with reason `:noproc`.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec start_link_supervised!(Supervisor.child_spec() | module | {module, term}, keyword) ::
|
||||
|
||||
@@ -60,6 +60,9 @@ defmodule ExUnit.CaptureLog do
|
||||
capture, for instance, if the log level is set to `:error`, then
|
||||
any message with the lower level will be ignored.
|
||||
The default level is `nil`, which will capture all messages.
|
||||
Note this setting does not override the overall `Logger.level/0` value.
|
||||
Therefore, if `Logger.level/0` is set to a higher level than the one
|
||||
configured in this function, no message will be captured.
|
||||
The behaviour is undetermined if async tests change Logger level.
|
||||
|
||||
The format, metadata and colors can be configured with `:format`,
|
||||
|
||||
@@ -20,6 +20,9 @@ defmodule ExUnit.Case do
|
||||
It should be enabled only if tests do not change any global state.
|
||||
Defaults to `false`.
|
||||
|
||||
* `:register` - when `false`, does not register this module within
|
||||
ExUnit server. This means the module won't run when ExUnit suite runs.
|
||||
|
||||
This module automatically includes all callbacks defined in
|
||||
`ExUnit.Callbacks`. See that module for more information on `setup`,
|
||||
`start_supervised`, `on_exit` and the test process life cycle.
|
||||
@@ -156,6 +159,10 @@ defmodule ExUnit.Case do
|
||||
|
||||
* `:describe_line` - the line the describe block begins on
|
||||
|
||||
* `:doctest` - the module or the file being doctested (if a doctest)
|
||||
|
||||
* `:doctest_line` - the line the doctest was defined (if a doctest)
|
||||
|
||||
The following tags customize how tests behave:
|
||||
|
||||
* `:capture_log` - see the "Log Capture" section below
|
||||
@@ -167,8 +174,8 @@ defmodule ExUnit.Case do
|
||||
|
||||
* `:tmp_dir` - (since v1.11.0) see the "Tmp Dir" section below
|
||||
|
||||
The `:test_type` tag is automatically set by ExUnit, but is **not** reserved.
|
||||
This tag is available for users to customize if they desire.
|
||||
* `:test_type` - the test type used when printing test results.
|
||||
It is set by ExUnit to `:test`, `:doctest` and so on, but is customizable.
|
||||
|
||||
## Filters
|
||||
|
||||
@@ -284,7 +291,7 @@ defmodule ExUnit.Case do
|
||||
raise "you must set @tag, @describetag, and @moduletag after the call to \"use ExUnit.Case\""
|
||||
end
|
||||
|
||||
attributes = [
|
||||
accumulate_attributes = [
|
||||
:ex_unit_tests,
|
||||
:tag,
|
||||
:describetag,
|
||||
@@ -294,24 +301,23 @@ defmodule ExUnit.Case do
|
||||
:ex_unit_registered_module_attributes
|
||||
]
|
||||
|
||||
Enum.each(attributes, &Module.register_attribute(module, &1, accumulate: true))
|
||||
Enum.each(accumulate_attributes, &Module.register_attribute(module, &1, accumulate: true))
|
||||
|
||||
persisted_attributes = [:ex_unit_async]
|
||||
|
||||
Enum.each(persisted_attributes, &Module.register_attribute(module, &1, persist: true))
|
||||
|
||||
attributes = [
|
||||
before_compile: ExUnit.Case,
|
||||
after_compile: ExUnit.Case,
|
||||
ex_unit_async: false
|
||||
]
|
||||
if Keyword.get(opts, :register, true) do
|
||||
Module.put_attribute(module, :after_compile, ExUnit.Case)
|
||||
end
|
||||
|
||||
Enum.each(attributes, fn {k, v} -> Module.put_attribute(module, k, v) end)
|
||||
Module.put_attribute(module, :before_compile, ExUnit.Case)
|
||||
end
|
||||
|
||||
async? = opts[:async]
|
||||
|
||||
if is_boolean(async?) do
|
||||
Module.put_attribute(module, :ex_unit_async, async?)
|
||||
if is_boolean(async?) or not registered? do
|
||||
Module.put_attribute(module, :ex_unit_async, async? || false)
|
||||
end
|
||||
|
||||
registered?
|
||||
@@ -477,10 +483,16 @@ defmodule ExUnit.Case do
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __before_compile__(_) do
|
||||
defmacro __before_compile__(env) do
|
||||
tests =
|
||||
env.module
|
||||
|> Module.get_attribute(:ex_unit_tests)
|
||||
|> Enum.reverse()
|
||||
|> Macro.escape()
|
||||
|
||||
quote do
|
||||
def __ex_unit__ do
|
||||
%ExUnit.TestModule{file: __ENV__.file, name: __MODULE__, tests: @ex_unit_tests}
|
||||
%ExUnit.TestModule{file: __ENV__.file, name: __MODULE__, tests: unquote(tests)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -582,6 +594,7 @@ defmodule ExUnit.Case do
|
||||
This function is deprecated in favor of `register_test/6` which performs
|
||||
better under tight loops by avoiding `__ENV__`.
|
||||
"""
|
||||
# TODO: Deprecate on Elixir v1.17
|
||||
@doc deprecated: "Use register_test/6 instead"
|
||||
def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do
|
||||
register_test(mod, file, line, test_type, name, tags)
|
||||
@@ -759,8 +772,9 @@ defmodule ExUnit.Case do
|
||||
|
||||
defp normalize_tags(tags) do
|
||||
Enum.reduce(Enum.reverse(tags), %{}, fn
|
||||
{key, value}, acc -> Map.put(acc, key, value)
|
||||
tag, acc when is_atom(tag) -> Map.put(acc, tag, true)
|
||||
tag, acc when is_list(tag) -> tag |> Enum.into(acc)
|
||||
tag, acc when is_list(tag) -> Enum.into(tag, acc)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -193,25 +193,27 @@ defmodule ExUnit.DocTest do
|
||||
This macro is auto-imported with every `ExUnit.Case`.
|
||||
"""
|
||||
defmacro doctest(module, opts \\ []) do
|
||||
caller = __CALLER__
|
||||
|
||||
require =
|
||||
if is_atom(Macro.expand(module, __CALLER__)) do
|
||||
if is_atom(Macro.expand(module, caller)) do
|
||||
quote do
|
||||
require unquote(module)
|
||||
end
|
||||
end
|
||||
|
||||
tests =
|
||||
quote bind_quoted: [module: module, opts: opts] do
|
||||
env = __ENV__
|
||||
quote bind_quoted: [
|
||||
module: module,
|
||||
opts: opts,
|
||||
env_line: caller.line,
|
||||
env_file: caller.file
|
||||
] do
|
||||
file = ExUnit.DocTest.__file__(module)
|
||||
|
||||
for {name, test} <- ExUnit.DocTest.__doctests__(module, opts) do
|
||||
if tags = Keyword.get(opts, :tags) do
|
||||
@tag tags
|
||||
end
|
||||
|
||||
for {name, test, tags} <- ExUnit.DocTest.__doctests__(module, opts) do
|
||||
@file file
|
||||
doc = ExUnit.Case.register_test(env, :doctest, name, [])
|
||||
doc = ExUnit.Case.register_test(__MODULE__, env_file, env_line, :doctest, name, tags)
|
||||
def unquote(doc)(_), do: unquote(test)
|
||||
end
|
||||
end
|
||||
@@ -230,13 +232,15 @@ defmodule ExUnit.DocTest do
|
||||
|
||||
@doc false
|
||||
def __doctests__(module, opts) do
|
||||
do_import = Keyword.get(opts, :import, false)
|
||||
tags = [doctest: module] ++ Keyword.get(opts, :tags, [])
|
||||
import = Keyword.get(opts, :import, false)
|
||||
file = module.module_info(:compile)[:source] |> Path.relative_to_cwd()
|
||||
|
||||
extract(module)
|
||||
|> filter_by_opts(module, opts)
|
||||
|> Stream.with_index()
|
||||
|> Stream.with_index(1)
|
||||
|> Enum.map(fn {test, acc} ->
|
||||
compile_test(test, module, do_import, acc + 1)
|
||||
compile_test(test, module, import, acc, file, tags)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -244,14 +248,9 @@ defmodule ExUnit.DocTest do
|
||||
except = Keyword.get(opts, :except, [])
|
||||
|
||||
case Keyword.fetch(opts, :only) do
|
||||
{:ok, []} ->
|
||||
[]
|
||||
|
||||
{:ok, only} ->
|
||||
filter_tests(module, tests, except, only)
|
||||
|
||||
:error ->
|
||||
Stream.reject(tests, &(&1.fun_arity in except))
|
||||
{:ok, []} -> []
|
||||
{:ok, only} -> filter_tests(module, tests, except, only)
|
||||
:error -> Stream.reject(tests, &(&1.fun_arity in except))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -264,7 +263,7 @@ defmodule ExUnit.DocTest do
|
||||
{tests, fun_arities} -> {[test | tests], [test.fun_arity | fun_arities]}
|
||||
end
|
||||
|
||||
case only -- fun_arities do
|
||||
case only -- [:moduledoc | fun_arities] do
|
||||
[] ->
|
||||
filtered_tests
|
||||
|
||||
@@ -287,8 +286,9 @@ defmodule ExUnit.DocTest do
|
||||
|
||||
## Compilation of extracted tests
|
||||
|
||||
defp compile_test(test, module, do_import, n) do
|
||||
{test_name(test, module, n), test_content(test, module, do_import)}
|
||||
defp compile_test(test, module, do_import, n, file, tags) do
|
||||
tags = [doctest_line: test.line] ++ tags
|
||||
{test_name(test, module, n), test_content(test, module, do_import, file), tags}
|
||||
end
|
||||
|
||||
defp test_name(%{fun_arity: :moduledoc}, m, n) do
|
||||
@@ -299,8 +299,7 @@ defmodule ExUnit.DocTest do
|
||||
"#{inspect(m)}.#{f}/#{a} (#{n})"
|
||||
end
|
||||
|
||||
defp test_content(%{exprs: exprs, line: line}, module, do_import) do
|
||||
file = module.module_info(:compile)[:source] |> Path.relative_to_cwd()
|
||||
defp test_content(%{exprs: exprs, line: line}, module, do_import, file) do
|
||||
location = [line: line, file: file]
|
||||
stack = Macro.escape([{module, :__MODULE__, 0, location}])
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ defmodule ExUnit.Formatter do
|
||||
* `{:case_finished, test_module}` -
|
||||
a test module has finished. See `ExUnit.TestModule` for details.
|
||||
|
||||
The full ExUnit configuration is passed as the argument to `c:GenServer.init/1` callback when the
|
||||
formatters are started. If you need to do runtime configuration of a
|
||||
formatter, you can add any configuration needed by using `ExUnit.configure/1`
|
||||
The full ExUnit configuration is passed as the argument to `c:GenServer.init/1`
|
||||
callback when the formatters are started. If you need to do runtime configuration
|
||||
of a formatter, you can add any configuration needed by using `ExUnit.configure/1`
|
||||
or `ExUnit.start/1`, and this will then be included in the options passed to
|
||||
the `c:GenServer.init/1` callback.
|
||||
"""
|
||||
@@ -126,7 +126,7 @@ defmodule ExUnit.Formatter do
|
||||
end
|
||||
end
|
||||
|
||||
# Deprecate me on Elixir v1.16
|
||||
# TODO: Deprecate me on Elixir v1.16
|
||||
@doc false
|
||||
def format_time(run, load) do
|
||||
format_times(%{run: run, load: load, async: nil})
|
||||
@@ -165,43 +165,6 @@ defmodule ExUnit.Formatter do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_assertion_error(%ExUnit.AssertionError{} = struct) do
|
||||
format_exception(%{}, struct, [], :infinity, fn _, msg -> msg end, "") |> elem(0)
|
||||
end
|
||||
|
||||
defp format_exception(test, %ExUnit.AssertionError{} = struct, stack, width, formatter, pad) do
|
||||
label_padding_size = if has_value?(struct.right), do: 7, else: 6
|
||||
padding_size = label_padding_size + byte_size(@counter_padding)
|
||||
|
||||
code_multiline =
|
||||
if struct.doctest != @no_value,
|
||||
do: &pad_multiline(&1, padding_size),
|
||||
else: &code_multiline(&1, padding_size)
|
||||
|
||||
formatted =
|
||||
[
|
||||
note: if_value(struct.message, &format_message(&1, formatter)),
|
||||
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
|
||||
code: if_value(struct.expr, code_multiline),
|
||||
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
|
||||
arguments: if_value(struct.args, &format_args(&1, width))
|
||||
]
|
||||
|> Kernel.++(format_context(struct, formatter, padding_size, width))
|
||||
|> format_meta(formatter, pad, label_padding_size)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
{formatted, stack}
|
||||
end
|
||||
|
||||
defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
|
||||
{blamed, stack} = Exception.blame(:error, struct, stack)
|
||||
banner = Exception.format_banner(:error, struct)
|
||||
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, formatter))
|
||||
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
|
||||
{message <> format_code(test, stack, formatter), stack}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use ExUnit.Formatter.format_test_all_failure/5 instead"
|
||||
def format_test_case_failure(test_case, failures, counter, width, formatter) do
|
||||
@@ -221,6 +184,8 @@ defmodule ExUnit.Formatter do
|
||||
end)
|
||||
end
|
||||
|
||||
## kind/reason formatting
|
||||
|
||||
defp format_kind_reason(test, :error, %mod{} = struct, stack, width, formatter)
|
||||
when mod in @formatter_exceptions do
|
||||
format_exception(test, struct, stack, width, formatter, @counter_padding)
|
||||
@@ -263,129 +228,67 @@ defmodule ExUnit.Formatter do
|
||||
|
||||
defp linked_or_trapped_exit(_kind, _reason), do: :error
|
||||
|
||||
defp format_code(test, stack, formatter) do
|
||||
if snippet = get_code(test, stack) do
|
||||
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"
|
||||
else
|
||||
""
|
||||
end
|
||||
defp format_exception(test, %ExUnit.AssertionError{} = struct, stack, width, formatter, pad) do
|
||||
label_padding_size = if has_value?(struct.right), do: 7, else: 6
|
||||
padding_size = label_padding_size + byte_size(@counter_padding)
|
||||
|
||||
code_multiline =
|
||||
if struct.doctest != @no_value,
|
||||
do: &pad_multiline(&1, padding_size),
|
||||
else: &code_multiline(&1, padding_size)
|
||||
|
||||
formatted =
|
||||
[
|
||||
message: if_value(struct.message, &format_message(&1, formatter)),
|
||||
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
|
||||
code: if_value(struct.expr, code_multiline, fn -> get_code(test, stack) || @no_value end),
|
||||
arguments: if_value(struct.args, &format_args(&1, width))
|
||||
]
|
||||
|> Kernel.++(format_assertion_diff(struct, padding_size, width, formatter))
|
||||
|> format_meta(formatter, pad, label_padding_size)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
{formatted, stack}
|
||||
end
|
||||
|
||||
defp get_code(%{module: module, name: name}, stack) do
|
||||
info =
|
||||
Enum.find_value(stack, fn
|
||||
{^module, ^name, _, info} -> info
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
file = info[:file]
|
||||
line = info[:line]
|
||||
|
||||
if line > 0 && file && File.exists?(file) do
|
||||
file |> File.stream!() |> Enum.at(line - 1) |> String.trim()
|
||||
end
|
||||
rescue
|
||||
_ -> nil
|
||||
defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
|
||||
{blamed, stack} = Exception.blame(:error, struct, stack)
|
||||
banner = Exception.format_banner(:error, struct)
|
||||
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, formatter))
|
||||
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
|
||||
{message <> format_code(test, stack, formatter), stack}
|
||||
end
|
||||
|
||||
defp get_code(%{}, _) do
|
||||
nil
|
||||
## Assertion error and diffing
|
||||
|
||||
@doc false
|
||||
def format_assertion_error(%ExUnit.AssertionError{} = struct) do
|
||||
format_exception(%{}, struct, [], :infinity, fn _, msg -> msg end, "") |> elem(0)
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}, _formatter),
|
||||
do: Macro.to_string(node)
|
||||
@doc """
|
||||
Formats `ExUnit.AssertionError` diff.
|
||||
|
||||
defp blame_match(%{match?: false, node: node}, formatter),
|
||||
do: formatter.(:blame_diff, Macro.to_string(node))
|
||||
It returns a keyword list with diffing information
|
||||
from the left and right side of the assertion, if
|
||||
any exists.
|
||||
|
||||
defp format_meta(fields, formatter, padding, padding_size) do
|
||||
for {label, value} <- fields, has_value?(value) do
|
||||
[padding, format_label(label, formatter, padding_size), value, "\n"]
|
||||
end
|
||||
end
|
||||
It expects the assertion error, the `padding_size`
|
||||
for formatted content, the width (may be `:infinity`),
|
||||
and the formatter callback function.
|
||||
"""
|
||||
def format_assertion_diff(assert_error, padding_size, width, formatter)
|
||||
|
||||
defp if_value(value, fun) do
|
||||
if has_value?(value) do
|
||||
fun.(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
defp unless_value(value, fun) do
|
||||
if has_value?(value) do
|
||||
@no_value
|
||||
else
|
||||
fun.()
|
||||
end
|
||||
end
|
||||
|
||||
defp has_value?(value) do
|
||||
value != @no_value
|
||||
end
|
||||
|
||||
defp format_label(:note, _formatter, _padding_size), do: ""
|
||||
|
||||
defp format_label(label, formatter, padding_size) do
|
||||
formatter.(:extra_info, String.pad_trailing("#{label}:", padding_size))
|
||||
end
|
||||
|
||||
defp format_message(value, formatter) do
|
||||
value = String.replace(value, "\n", "\n" <> @counter_padding)
|
||||
formatter.(:error_info, value)
|
||||
end
|
||||
|
||||
defp format_args(args, width) do
|
||||
entries =
|
||||
for {arg, i} <- Enum.with_index(args, 1) do
|
||||
"""
|
||||
|
||||
# #{i}
|
||||
#{inspect_multiline(arg, 9, width)}
|
||||
"""
|
||||
end
|
||||
|
||||
["\n" | entries]
|
||||
end
|
||||
|
||||
@assertions [
|
||||
:assert,
|
||||
:assert_raise,
|
||||
:assert_receive,
|
||||
:assert_received,
|
||||
:refute,
|
||||
:refute_receive,
|
||||
:refute_received
|
||||
]
|
||||
|
||||
defp code_multiline({fun, _, [expr]}, padding_size) when fun in @assertions do
|
||||
pad_multiline(Atom.to_string(fun) <> " " <> Macro.to_string(expr), padding_size)
|
||||
end
|
||||
|
||||
defp code_multiline(expr, padding_size) do
|
||||
pad_multiline(Macro.to_string(expr), padding_size)
|
||||
end
|
||||
|
||||
defp inspect_multiline(expr, padding_size, width) do
|
||||
width = if width == :infinity, do: width, else: width - padding_size
|
||||
|
||||
expr
|
||||
|> Algebra.to_doc(%Inspect.Opts{width: width})
|
||||
|> Algebra.group()
|
||||
|> Algebra.nest(padding_size)
|
||||
|> Algebra.format(width)
|
||||
end
|
||||
|
||||
defp format_context(%{context: {:mailbox, _pins, []}}, _, _, _) do
|
||||
def format_assertion_diff(%ExUnit.AssertionError{context: {:mailbox, _pins, []}}, _, _, _) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_context(
|
||||
%{left: left, context: {:mailbox, pins, mailbox}},
|
||||
formatter,
|
||||
padding_size,
|
||||
width
|
||||
) do
|
||||
def format_assertion_diff(
|
||||
%ExUnit.AssertionError{left: left, context: {:mailbox, pins, mailbox}},
|
||||
padding_size,
|
||||
width,
|
||||
formatter
|
||||
) do
|
||||
formatted_mailbox =
|
||||
for message <- mailbox do
|
||||
{pattern, value, _warnings} =
|
||||
@@ -413,14 +316,14 @@ defmodule ExUnit.Formatter do
|
||||
[mailbox: Enum.join(formatted_mailbox, "\n")]
|
||||
end
|
||||
|
||||
defp format_context(
|
||||
%{left: left, right: right, context: context},
|
||||
formatter,
|
||||
padding_size,
|
||||
width
|
||||
) do
|
||||
def format_assertion_diff(
|
||||
%ExUnit.AssertionError{left: left, right: right, context: context},
|
||||
padding_size,
|
||||
width,
|
||||
formatter
|
||||
) do
|
||||
{left, right, extras} = format_sides(left, right, context, formatter, padding_size, width)
|
||||
[left: left, right: right] ++ extras
|
||||
for {k, v} <- [left: left, right: right] ++ extras, has_value?(v), do: {k, v}
|
||||
end
|
||||
|
||||
defp format_sides(left, right, context, formatter, padding_size, width) do
|
||||
@@ -488,6 +391,121 @@ defmodule ExUnit.Formatter do
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp format_code(test, stack, formatter) do
|
||||
if snippet = get_code(test, stack) do
|
||||
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
defp get_code(%{module: module, name: name}, stack) do
|
||||
info =
|
||||
Enum.find_value(stack, fn
|
||||
{^module, ^name, _, info} -> info
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
file = info[:file]
|
||||
line = info[:line]
|
||||
|
||||
if line > 0 && file && File.exists?(file) do
|
||||
file |> File.stream!() |> Enum.at(line - 1) |> String.trim()
|
||||
end
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
defp get_code(%{}, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}, _formatter),
|
||||
do: Macro.to_string(node)
|
||||
|
||||
defp blame_match(%{match?: false, node: node}, formatter),
|
||||
do: formatter.(:blame_diff, Macro.to_string(node))
|
||||
|
||||
defp format_meta(fields, formatter, padding, padding_size) do
|
||||
for {label, value} <- fields, has_value?(value) do
|
||||
[padding, format_label(label, formatter, padding_size), value, "\n"]
|
||||
end
|
||||
end
|
||||
|
||||
defp if_value(value, fun) do
|
||||
if has_value?(value) do
|
||||
fun.(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
defp if_value(value, do_fun, else_fun) do
|
||||
if has_value?(value) do
|
||||
do_fun.(value)
|
||||
else
|
||||
else_fun.()
|
||||
end
|
||||
end
|
||||
|
||||
defp has_value?(value) do
|
||||
value != @no_value
|
||||
end
|
||||
|
||||
defp format_label(:message, _formatter, _padding_size), do: ""
|
||||
|
||||
defp format_label(label, formatter, padding_size) do
|
||||
formatter.(:extra_info, String.pad_trailing("#{label}:", padding_size))
|
||||
end
|
||||
|
||||
defp format_message(value, formatter) do
|
||||
value = String.replace(value, "\n", "\n" <> @counter_padding)
|
||||
formatter.(:error_info, value)
|
||||
end
|
||||
|
||||
defp format_args(args, width) do
|
||||
entries =
|
||||
for {arg, i} <- Enum.with_index(args, 1) do
|
||||
"""
|
||||
|
||||
# #{i}
|
||||
#{inspect_multiline(arg, 9, width)}
|
||||
"""
|
||||
end
|
||||
|
||||
["\n" | entries]
|
||||
end
|
||||
|
||||
@assertions [
|
||||
:assert,
|
||||
:assert_raise,
|
||||
:assert_receive,
|
||||
:assert_received,
|
||||
:refute,
|
||||
:refute_receive,
|
||||
:refute_received
|
||||
]
|
||||
|
||||
defp code_multiline({fun, _, [expr]}, padding_size) when fun in @assertions do
|
||||
pad_multiline(Atom.to_string(fun) <> " " <> Macro.to_string(expr), padding_size)
|
||||
end
|
||||
|
||||
defp code_multiline(expr, padding_size) do
|
||||
pad_multiline(Macro.to_string(expr), padding_size)
|
||||
end
|
||||
|
||||
defp inspect_multiline(expr, padding_size, width) do
|
||||
width = if width == :infinity, do: width, else: width - padding_size
|
||||
|
||||
expr
|
||||
|> Algebra.to_doc(%Inspect.Opts{width: width})
|
||||
|> Algebra.group()
|
||||
|> Algebra.nest(padding_size)
|
||||
|> Algebra.format(width)
|
||||
end
|
||||
|
||||
defp format_stacktrace([], _case, _test, _color) do
|
||||
""
|
||||
end
|
||||
|
||||
@@ -571,7 +571,7 @@ defmodule ExUnit.Runner do
|
||||
end
|
||||
|
||||
defp shuffle(%{seed: 0}, list) do
|
||||
Enum.reverse(list)
|
||||
list
|
||||
end
|
||||
|
||||
defp shuffle(%{seed: seed}, list) do
|
||||
|
||||
@@ -22,8 +22,8 @@ defmodule ExUnit.Server do
|
||||
end
|
||||
end
|
||||
|
||||
def modules_loaded do
|
||||
GenServer.call(@name, :modules_loaded, @timeout)
|
||||
def modules_loaded(uniq?) do
|
||||
GenServer.call(@name, {:modules_loaded, uniq?}, @timeout)
|
||||
end
|
||||
|
||||
def take_async_modules(count) do
|
||||
@@ -58,11 +58,23 @@ defmodule ExUnit.Server do
|
||||
{:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}}
|
||||
end
|
||||
|
||||
def handle_call(:modules_loaded, _from, %{loaded: :done} = state) do
|
||||
def handle_call({:modules_loaded, _}, _from, %{loaded: :done} = state) do
|
||||
{:reply, 0, state}
|
||||
end
|
||||
|
||||
def handle_call(:modules_loaded, _from, %{loaded: loaded} = state) when is_integer(loaded) do
|
||||
def handle_call({:modules_loaded, uniq?}, _from, %{loaded: loaded} = state)
|
||||
when is_integer(loaded) do
|
||||
state =
|
||||
if uniq? do
|
||||
%{
|
||||
state
|
||||
| async_modules: Enum.uniq(state.async_modules),
|
||||
sync_modules: Enum.uniq(state.sync_modules)
|
||||
}
|
||||
else
|
||||
state
|
||||
end
|
||||
|
||||
diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond)
|
||||
{:reply, diff, take_modules(%{state | loaded: :done})}
|
||||
end
|
||||
|
||||
@@ -462,7 +462,10 @@ defmodule ExUnit.DocTestTest do
|
||||
# doctest ExUnit.DocTest
|
||||
|
||||
doctest ExUnit.DocTestTest.GoodModule, import: true
|
||||
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [one: 0], import: true
|
||||
|
||||
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly,
|
||||
only: [:moduledoc, one: 0],
|
||||
import: true
|
||||
|
||||
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithExcept,
|
||||
except: [:moduledoc, two: 0],
|
||||
@@ -862,18 +865,17 @@ defmodule ExUnit.DocTestTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "doctests type" do
|
||||
defmodule DoctestType do
|
||||
use ExUnit.Case
|
||||
doctest ExUnit.DocTestTest.NoImport
|
||||
test "doctests built-in tags" do
|
||||
alias ExUnit.DocTestTest.NoImport
|
||||
|
||||
setup test do
|
||||
assert test.test_type == :doctest
|
||||
:ok
|
||||
end
|
||||
defmodule DoctestTags do
|
||||
use ExUnit.Case, register: false
|
||||
doctest NoImport
|
||||
end
|
||||
|
||||
assert capture_io(fn -> ExUnit.run() end) =~ "2 doctests, 0 failures"
|
||||
assert %ExUnit.TestModule{tests: [test1, test2]} = DoctestTags.__ex_unit__()
|
||||
assert %{test_type: :doctest, doctest: NoImport, doctest_line: 129} = test1.tags
|
||||
assert %{test_type: :doctest, doctest: NoImport, doctest_line: 132} = test2.tags
|
||||
end
|
||||
|
||||
test "multiple exceptions in one test case is not supported" do
|
||||
|
||||
@@ -33,6 +33,8 @@ defmodule ExUnit.FormatterTest do
|
||||
defp diff_formatter(:diff_enabled?, _default), do: true
|
||||
defp diff_formatter(_key, value), do: value
|
||||
|
||||
defp kw_to_string(kw), do: for({k, v} <- kw, do: {k, IO.iodata_to_binary(v)})
|
||||
|
||||
test "formats test case filters" do
|
||||
filters = [run: true, slow: false]
|
||||
assert format_filters(filters, :exclude) =~ "Excluding tags: [run: true, slow: false]"
|
||||
@@ -242,7 +244,8 @@ defmodule ExUnit.FormatterTest do
|
||||
end
|
||||
|
||||
test "formats assertions" do
|
||||
failure = [{:error, catch_assertion(assert ExUnit.FormatterTest.falsy()), []}]
|
||||
assertion_error = catch_assertion(assert ExUnit.FormatterTest.falsy())
|
||||
failure = [{:error, assertion_error, []}]
|
||||
|
||||
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
|
||||
1) world (Hello)
|
||||
@@ -250,10 +253,13 @@ defmodule ExUnit.FormatterTest do
|
||||
Expected truthy, got false
|
||||
code: assert ExUnit.FormatterTest.falsy()
|
||||
"""
|
||||
|
||||
assert format_assertion_diff(assertion_error, 0, :infinity, &formatter/2) == []
|
||||
end
|
||||
|
||||
test "formats assertions with patterns and values" do
|
||||
failure = [{:error, catch_assertion(assert {1, 2, 3} > {1, 2, 3}), []}]
|
||||
assertion_error = catch_assertion(assert {1, 2, 3} > {1, 2, 3})
|
||||
failure = [{:error, assertion_error, []}]
|
||||
|
||||
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
|
||||
1) world (Hello)
|
||||
@@ -263,7 +269,12 @@ defmodule ExUnit.FormatterTest do
|
||||
left: {1, 2, 3}
|
||||
"""
|
||||
|
||||
failure = [{:error, catch_assertion(assert {3, 2, 1} = {1, 2, 3}), []}]
|
||||
assert format_assertion_diff(assertion_error, 0, :infinity, &diff_formatter/2)
|
||||
|> kw_to_string() ==
|
||||
[left: "{1, 2, 3}"]
|
||||
|
||||
assertion_error = catch_assertion(assert {3, 2, 1} = {1, 2, 3})
|
||||
failure = [{:error, assertion_error, []}]
|
||||
|
||||
assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """
|
||||
1) world (Hello)
|
||||
@@ -273,13 +284,18 @@ defmodule ExUnit.FormatterTest do
|
||||
left: {3, 2, 1}
|
||||
right: {1, 2, 3}
|
||||
"""
|
||||
|
||||
assert format_assertion_diff(assertion_error, 0, :infinity, &diff_formatter/2)
|
||||
|> kw_to_string() ==
|
||||
[{:left, "{3, 2, 1}"}, {:right, "{1, 2, 3}"}]
|
||||
end
|
||||
|
||||
nfc_hello = String.normalize("héllo", :nfc)
|
||||
nfd_hello = String.normalize("héllo", :nfd)
|
||||
|
||||
test "formats assertions with hints" do
|
||||
failure = [{:error, catch_assertion(assert unquote(nfc_hello) == unquote(nfd_hello)), []}]
|
||||
assertion_error = catch_assertion(assert unquote(nfc_hello) == unquote(nfd_hello))
|
||||
failure = [{:error, assertion_error, []}]
|
||||
|
||||
assert format_test_failure(test(), failure, 1, 80, &diff_formatter/2) =~ """
|
||||
1) world (Hello)
|
||||
@@ -290,6 +306,15 @@ defmodule ExUnit.FormatterTest do
|
||||
right: "#{unquote(nfd_hello)}"
|
||||
hint: you are comparing strings that have the same visual representation but are made of different Unicode codepoints
|
||||
"""
|
||||
|
||||
assert format_assertion_diff(assertion_error, 0, :infinity, &diff_formatter/2)
|
||||
|> kw_to_string() ==
|
||||
[
|
||||
left: inspect(unquote(nfc_hello)),
|
||||
right: inspect(unquote(nfd_hello)),
|
||||
hint:
|
||||
"you are comparing strings that have the same visual representation but are made of different Unicode codepoints"
|
||||
]
|
||||
end
|
||||
|
||||
test "formats match error between pinned struct type and a non-struct" do
|
||||
|
||||
@@ -24,7 +24,7 @@ defmodule ExUnitTest do
|
||||
assert ExUnit.run() == %{failures: 2, skipped: 0, total: 2, excluded: 0}
|
||||
end) =~ "\n2 tests, 2 failures\n"
|
||||
|
||||
ExUnit.Server.modules_loaded()
|
||||
ExUnit.Server.modules_loaded(false)
|
||||
|
||||
assert capture_io(fn ->
|
||||
assert ExUnit.async_run() |> ExUnit.await_run() ==
|
||||
@@ -34,7 +34,7 @@ defmodule ExUnitTest do
|
||||
|
||||
test "supports rerunning given modules" do
|
||||
defmodule SampleAsyncTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: true, register: false
|
||||
|
||||
test "true" do
|
||||
assert false
|
||||
@@ -42,7 +42,7 @@ defmodule ExUnitTest do
|
||||
end
|
||||
|
||||
defmodule SampleSyncTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, register: false
|
||||
|
||||
test "true" do
|
||||
assert false
|
||||
@@ -61,15 +61,26 @@ defmodule ExUnitTest do
|
||||
|
||||
assert capture_io(fn ->
|
||||
assert ExUnit.run() == %{
|
||||
failures: 3,
|
||||
failures: 1,
|
||||
skipped: 0,
|
||||
total: 3,
|
||||
total: 1,
|
||||
excluded: 0
|
||||
}
|
||||
end) =~ "\n3 tests, 3 failures\n"
|
||||
end) =~ "\n1 test, 1 failure\n"
|
||||
|
||||
sample = [SampleSyncTest, SampleAsyncTest]
|
||||
|
||||
assert capture_io(fn ->
|
||||
assert ExUnit.run([SampleSyncTest, SampleAsyncTest]) == %{
|
||||
assert ExUnit.run(sample) == %{
|
||||
failures: 2,
|
||||
skipped: 0,
|
||||
total: 2,
|
||||
excluded: 0
|
||||
}
|
||||
end) =~ "\n2 tests, 2 failures\n"
|
||||
|
||||
assert capture_io(fn ->
|
||||
assert ExUnit.run(sample ++ sample) == %{
|
||||
failures: 2,
|
||||
skipped: 0,
|
||||
total: 2,
|
||||
@@ -899,7 +910,7 @@ defmodule ExUnitTest do
|
||||
|
||||
defp run_with_filter(filters, cases) do
|
||||
Enum.each(cases, &ExUnit.Server.add_sync_module/1)
|
||||
ExUnit.Server.modules_loaded()
|
||||
ExUnit.Server.modules_loaded(false)
|
||||
|
||||
opts =
|
||||
ExUnit.configuration()
|
||||
|
||||
+15
-13
@@ -10,27 +10,29 @@ defmodule IEx.Broker do
|
||||
## Shell API
|
||||
|
||||
@doc """
|
||||
Finds the IEx server running inside `:user_drv`, on this node exclusively.
|
||||
Finds the IEx server.
|
||||
"""
|
||||
@spec shell :: shell()
|
||||
# TODO: Use shell:whereis_shell() from Erlang/OTP 26+.
|
||||
def shell() do
|
||||
# Locate top group leader when using the "new shell".
|
||||
if user = Process.whereis(:user) do
|
||||
case :group.interfaces(user) do
|
||||
# Old or no shell
|
||||
[] ->
|
||||
nil
|
||||
|
||||
# Get current group from user_drv
|
||||
[user_drv: user_drv] ->
|
||||
case :user_drv.interfaces(user_drv) do
|
||||
[] -> nil
|
||||
[current_group: group] -> :group.interfaces(group)[:shell]
|
||||
end
|
||||
if user_drv = get_from_dict(user, :user_drv) do
|
||||
if group = get_from_dict(user_drv, :current_group) do
|
||||
get_from_dict(group, :shell)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_from_dict(pid, key) do
|
||||
with {:dictionary, dictionary} <- Process.info(pid, :dictionary),
|
||||
{^key, value} <- List.keyfind(dictionary, key, 0) do
|
||||
value
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Finds the evaluator and server running inside `:user_drv`, on this node exclusively.
|
||||
"""
|
||||
|
||||
+22
-14
@@ -37,6 +37,8 @@
|
||||
defmodule IEx.CLI do
|
||||
@moduledoc false
|
||||
|
||||
@compile {:no_warn_undefined, {:user, :start, 0}}
|
||||
|
||||
@doc """
|
||||
In order to work properly, IEx needs to be set as the
|
||||
proper `-user` when starting the Erlang VM and we do so
|
||||
@@ -50,22 +52,28 @@ defmodule IEx.CLI do
|
||||
a dumb terminal version is started instead.
|
||||
"""
|
||||
def start do
|
||||
if tty_works?() do
|
||||
:user_drv.start([:"tty_sl -c -e", tty_args()])
|
||||
else
|
||||
if get_remsh(:init.get_plain_arguments()) do
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"warning: the --remsh option will be ignored because IEx is running on limited shell"
|
||||
)
|
||||
end
|
||||
# TODO: We can remove the -user callback on Erlang/OTP 26+ in favor of eval
|
||||
cond do
|
||||
Code.ensure_loaded?(:prim_tty) ->
|
||||
:user_drv.start(%{initial_shell: tty_args()})
|
||||
|
||||
:user.start()
|
||||
tty_works?() ->
|
||||
:user_drv.start([:"tty_sl -c -e", tty_args()])
|
||||
|
||||
# IEx.Broker is capable of considering all groups under user_drv but
|
||||
# when we use :user.start(), we need to explicitly register it instead.
|
||||
# If we don't register, pry doesn't work.
|
||||
IEx.start([register: true] ++ options(), {:elixir, :start_cli, []})
|
||||
true ->
|
||||
if get_remsh(:init.get_plain_arguments()) do
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"warning: the --remsh option will be ignored because IEx is running on limited shell"
|
||||
)
|
||||
end
|
||||
|
||||
:user.start()
|
||||
|
||||
# IEx.Broker is capable of considering all groups under user_drv but
|
||||
# when we use :user.start(), we need to explicitly register it instead.
|
||||
# If we don't register, pry doesn't work.
|
||||
IEx.start([register: true] ++ options(), {:elixir, :start_cli, []})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ defmodule IEx.Pry do
|
||||
|
||||
def pry(binding, opts) when is_list(opts) do
|
||||
vars = for {k, _} when is_atom(k) <- binding, do: {k, nil}
|
||||
pry(binding, opts |> :elixir.env_for_eval() |> :elixir_env.with_vars(vars))
|
||||
pry(binding, opts |> Code.env_for_eval() |> :elixir_env.with_vars(vars))
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
+31
-2
@@ -615,6 +615,17 @@ defmodule Mix do
|
||||
Hexpm.Repo.query!("SELECT COUNT(1) from packages")
|
||||
#=> ...
|
||||
|
||||
The example above can be simplified by passing the application
|
||||
name as an atom for `:config_path` and `:lockfile`:
|
||||
|
||||
Mix.install(
|
||||
[
|
||||
{:hexpm, path: "/tmp/hexpm", env: :dev},
|
||||
],
|
||||
config_path: :hexpm,
|
||||
lockfile: :hexpm
|
||||
)
|
||||
|
||||
## Limitations
|
||||
|
||||
There is one limitation to `Mix.install/2`, which is actually an Elixir
|
||||
@@ -682,7 +693,7 @@ defmodule Mix do
|
||||
end)
|
||||
|
||||
config = Keyword.get(opts, :config, [])
|
||||
config_path = opts[:config_path] && Path.expand(opts[:config_path])
|
||||
config_path = expand_path(opts[:config_path], deps, :config_path, "config/config.exs")
|
||||
system_env = Keyword.get(opts, :system_env, [])
|
||||
consolidate_protocols? = Keyword.get(opts, :consolidate_protocols, true)
|
||||
|
||||
@@ -729,7 +740,7 @@ defmodule Mix do
|
||||
:ok = Mix.Local.append_archives()
|
||||
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
|
||||
build_dir = Path.join(install_dir, "_build")
|
||||
external_lockfile = opts[:lockfile] && Path.expand(opts[:lockfile])
|
||||
external_lockfile = expand_path(opts[:lockfile], deps, :lockfile, "mix.lock")
|
||||
|
||||
try do
|
||||
first_build? = not File.dir?(build_dir)
|
||||
@@ -800,6 +811,24 @@ defmodule Mix do
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_path(_path = nil, _deps, _key, _), do: nil
|
||||
defp expand_path(path, _deps, _key, _) when is_binary(path), do: Path.expand(path)
|
||||
|
||||
defp expand_path(app_name, deps, key, relative_path) when is_atom(app_name) do
|
||||
app_dir =
|
||||
case List.keyfind(deps, app_name, 0) do
|
||||
{_, _, opts} when is_list(opts) -> opts[:path]
|
||||
{_, opts} when is_list(opts) -> opts[:path]
|
||||
_ -> Mix.raise("unknown dependency #{inspect(app_name)} given to #{inspect(key)}")
|
||||
end
|
||||
|
||||
unless app_dir do
|
||||
Mix.raise("#{inspect(app_name)} given to #{inspect(key)} must be a path dependency")
|
||||
end
|
||||
|
||||
Path.join(app_dir, relative_path)
|
||||
end
|
||||
|
||||
defp install_dir(cache_id) do
|
||||
install_root =
|
||||
System.get_env("MIX_INSTALL_DIR") ||
|
||||
|
||||
@@ -32,6 +32,8 @@ defmodule Mix.Compilers.Test do
|
||||
{matched_test_files, nil, []}
|
||||
end
|
||||
|
||||
Application.ensure_all_started(:ex_unit)
|
||||
|
||||
cond do
|
||||
test_files == [] ->
|
||||
# Make sure we run the after_suite callbacks but with no feedback
|
||||
|
||||
@@ -106,6 +106,10 @@ defmodule Mix.Task.Compiler do
|
||||
it receives a tuple with current status and the list
|
||||
of diagnostic. It must return the updated status and
|
||||
diagnostics.
|
||||
|
||||
If the given compiler does not run (for instance,
|
||||
because an earlier compiler in the stack has aborted),
|
||||
the callback will not be executed.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec after_compiler(atom, ({status, [Diagnostic.t()]} -> {status, [Diagnostic.t()]})) :: :ok
|
||||
|
||||
@@ -56,7 +56,6 @@ defmodule Mix.Tasks.Compile.Elixir do
|
||||
* `--long-compilation-threshold N` - sets the "long compilation" threshold
|
||||
(in seconds) to `N` (see the docs for `Kernel.ParallelCompiler.compile/2`)
|
||||
* `--profile` - if set to `time`, outputs timing information of compilation steps
|
||||
* `--all-warnings` - prints warnings even from files that do not need to be recompiled
|
||||
* `--tracer` - adds a compiler tracer in addition to any specified in the `mix.exs` file
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -46,6 +46,7 @@ defmodule Mix.Tasks.Compile do
|
||||
|
||||
## Command line options
|
||||
|
||||
* `--all-warnings` - prints warnings even from files that do not need to be recompiled
|
||||
* `--erl-config` - path to an Erlang term file that will be loaded as Mix config
|
||||
* `--force` - forces compilation
|
||||
* `--list` - lists all enabled compilers
|
||||
|
||||
@@ -8,8 +8,8 @@ defmodule Mix.Tasks.Format do
|
||||
|
||||
mix format mix.exs "lib/**/*.{ex,exs}" "test/**/*.{ex,exs}"
|
||||
|
||||
If any of the files is `-`, then the output is read from stdin
|
||||
and written to stdout.
|
||||
If any of the files is `-`, then the input is read from stdin and the output
|
||||
is written to stdout.
|
||||
|
||||
## Formatting options
|
||||
|
||||
@@ -176,7 +176,7 @@ defmodule Mix.Tasks.Format do
|
||||
]
|
||||
|
||||
@manifest "cached_dot_formatter"
|
||||
@manifest_vsn 1
|
||||
@manifest_vsn 2
|
||||
|
||||
@doc """
|
||||
Returns which features this plugin should plug into.
|
||||
@@ -304,10 +304,21 @@ defmodule Mix.Tasks.Format do
|
||||
else
|
||||
manifest = Path.join(Mix.Project.manifest_path(), @manifest)
|
||||
|
||||
maybe_cache_in_manifest(dot_formatter, manifest, fn ->
|
||||
{subdirectories, sources} = eval_subs_opts(subs, prefix, sources)
|
||||
{{eval_deps_opts(formatter_opts, deps), subdirectories}, sources}
|
||||
end)
|
||||
{{locals_without_parens, subdirectories}, sources} =
|
||||
maybe_cache_in_manifest(dot_formatter, manifest, fn ->
|
||||
{subdirectories, sources} = eval_subs_opts(subs, prefix, sources)
|
||||
{{eval_deps_opts(deps), subdirectories}, sources}
|
||||
end)
|
||||
|
||||
formatter_opts =
|
||||
Keyword.update(
|
||||
formatter_opts,
|
||||
:locals_without_parens,
|
||||
locals_without_parens,
|
||||
&(locals_without_parens ++ &1)
|
||||
)
|
||||
|
||||
{{formatter_opts, subdirectories}, sources}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -342,29 +353,21 @@ defmodule Mix.Tasks.Format do
|
||||
{entry, sources}
|
||||
end
|
||||
|
||||
defp eval_deps_opts(formatter_opts, []) do
|
||||
formatter_opts
|
||||
defp eval_deps_opts([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp eval_deps_opts(formatter_opts, deps) do
|
||||
defp eval_deps_opts(deps) do
|
||||
deps_paths = Mix.Project.deps_paths()
|
||||
|
||||
parenless_calls =
|
||||
for dep <- deps,
|
||||
dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths),
|
||||
dep_dot_formatter = Path.join(dep_path, ".formatter.exs"),
|
||||
File.regular?(dep_dot_formatter),
|
||||
dep_opts = eval_file_with_keyword_list(dep_dot_formatter),
|
||||
parenless_call <- dep_opts[:export][:locals_without_parens] || [],
|
||||
uniq: true,
|
||||
do: parenless_call
|
||||
|
||||
Keyword.update(
|
||||
formatter_opts,
|
||||
:locals_without_parens,
|
||||
parenless_calls,
|
||||
&(&1 ++ parenless_calls)
|
||||
)
|
||||
for dep <- deps,
|
||||
dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths),
|
||||
dep_dot_formatter = Path.join(dep_path, ".formatter.exs"),
|
||||
File.regular?(dep_dot_formatter),
|
||||
dep_opts = eval_file_with_keyword_list(dep_dot_formatter),
|
||||
parenless_call <- dep_opts[:export][:locals_without_parens] || [],
|
||||
uniq: true,
|
||||
do: parenless_call
|
||||
end
|
||||
|
||||
defp eval_subs_opts(subs, prefix, sources) do
|
||||
|
||||
@@ -107,6 +107,8 @@ defmodule Mix.Tasks.Test do
|
||||
|
||||
## Command line options
|
||||
|
||||
* `--all-warnings` - prints warnings even from files that do not need to be recompiled
|
||||
|
||||
* `--color` - enables color in the output
|
||||
|
||||
* `--cover` - runs coverage tool. See "Coverage" section below
|
||||
@@ -389,6 +391,7 @@ defmodule Mix.Tasks.Test do
|
||||
"""
|
||||
|
||||
@switches [
|
||||
all_warnings: :boolean,
|
||||
force: :boolean,
|
||||
color: :boolean,
|
||||
cover: :boolean,
|
||||
|
||||
@@ -178,6 +178,37 @@ defmodule Mix.Tasks.FormatTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not cache inputs from .formatter.exs", context do
|
||||
in_tmp(context.test, fn ->
|
||||
File.write!(".formatter.exs", """
|
||||
[
|
||||
inputs: Path.wildcard("{a,b}.ex"),
|
||||
locals_without_parens: [foo: 1]
|
||||
]
|
||||
""")
|
||||
|
||||
File.write!("a.ex", """
|
||||
foo bar baz
|
||||
""")
|
||||
|
||||
Mix.Tasks.Format.run([])
|
||||
|
||||
assert File.read!("a.ex") == """
|
||||
foo bar(baz)
|
||||
"""
|
||||
|
||||
File.write!("b.ex", """
|
||||
bar baz bat
|
||||
""")
|
||||
|
||||
Mix.Tasks.Format.run([])
|
||||
|
||||
assert File.read!("b.ex") == """
|
||||
bar(baz(bat))
|
||||
"""
|
||||
end)
|
||||
end
|
||||
|
||||
test "expands patterns in inputs from .formatter.exs", context do
|
||||
in_tmp(context.test, fn ->
|
||||
File.write!(".formatter.exs", """
|
||||
@@ -343,7 +374,7 @@ defmodule Mix.Tasks.FormatTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "can read exported configuration from subdirectories", context do
|
||||
test "reads exported configuration from subdirectories", context do
|
||||
in_tmp(context.test, fn ->
|
||||
File.write!(".formatter.exs", """
|
||||
[subdirectories: ["lib"]]
|
||||
@@ -396,7 +427,7 @@ defmodule Mix.Tasks.FormatTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "can read exported configuration from dependencies", context do
|
||||
test "reads exported configuration from dependencies", context do
|
||||
in_tmp(context.test, fn ->
|
||||
Mix.Project.push(__MODULE__.FormatWithDepsApp)
|
||||
|
||||
@@ -434,7 +465,7 @@ defmodule Mix.Tasks.FormatTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "can read exported configuration from dependencies and subdirectories", context do
|
||||
test "reads exported configuration from dependencies and subdirectories", context do
|
||||
in_tmp(context.test, fn ->
|
||||
Mix.Project.push(__MODULE__.FormatWithDepsApp)
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ defmodule Mix.Tasks.TestTest do
|
||||
end)
|
||||
end
|
||||
|
||||
@tag :unix
|
||||
test "does not exit on compilation failure" do
|
||||
in_fixture("test_stale", fn ->
|
||||
File.write!("lib/b.ex", """
|
||||
@@ -410,9 +411,10 @@ defmodule Mix.Tasks.TestTest do
|
||||
test "logs test absence for a project with no test paths" do
|
||||
in_fixture("test_stale", fn ->
|
||||
File.rm_rf!("test")
|
||||
assert_run_output("There are no tests to run")
|
||||
|
||||
File.mkdir_p!("test")
|
||||
File.write!("test/test_helper.exs", "ExUnit.start()")
|
||||
|
||||
assert_run_output("There are no tests to run")
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -268,6 +268,35 @@ defmodule Mix.Tasks.XrefTest do
|
||||
assert_trace("lib/b.ex", files, output)
|
||||
end
|
||||
|
||||
test "shows traces for module callbacks" do
|
||||
files = %{
|
||||
"lib/a.ex" => ~S"""
|
||||
defmodule A do
|
||||
@before_compile :"Elixir.B"
|
||||
@after_compile :"Elixir.B"
|
||||
@after_verify :"Elixir.B"
|
||||
end
|
||||
""",
|
||||
"lib/b.ex" => ~S"""
|
||||
defmodule B do
|
||||
defmacro __before_compile__(_env), do: :ok
|
||||
defmacro __after_compile__(_env, _binary), do: :ok
|
||||
def __after_verify__(_module), do: :ok
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
output = """
|
||||
Compiling 2 files (.ex)
|
||||
Generated sample app
|
||||
lib/a.ex:1: call B.__after_compile__/2 (compile)
|
||||
lib/a.ex:1: call B.__after_verify__/1 (compile)
|
||||
lib/a.ex:1: call B.__before_compile__/1 (compile)
|
||||
"""
|
||||
|
||||
assert_trace("lib/a.ex", files, output)
|
||||
end
|
||||
|
||||
test "filters per label" do
|
||||
files = %{
|
||||
"lib/a.ex" => ~S"""
|
||||
|
||||
@@ -153,6 +153,27 @@ defmodule MixTest do
|
||||
Application.delete_env(:unknown_app, :foo, persistent: true)
|
||||
end
|
||||
|
||||
test ":config_path with application name", %{tmp_dir: tmp_dir} do
|
||||
config_path = Path.join(tmp_dir, "install_test/config/config.exs")
|
||||
config_path |> Path.dirname() |> File.mkdir_p!()
|
||||
|
||||
File.write!(config_path, """
|
||||
import Config
|
||||
config :myapp, :foo, 1
|
||||
""")
|
||||
|
||||
Mix.install(
|
||||
[
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")}
|
||||
],
|
||||
config_path: :install_test
|
||||
)
|
||||
|
||||
assert Application.fetch_env!(:myapp, :foo) == 1
|
||||
after
|
||||
Application.delete_env(:myapp, :foo)
|
||||
end
|
||||
|
||||
test ":config_path", %{tmp_dir: tmp_dir} do
|
||||
config_path = Path.join(tmp_dir, "config.exs")
|
||||
|
||||
@@ -271,6 +292,30 @@ defmodule MixTest do
|
||||
purge([GitRepo, GitRepo.MixProject])
|
||||
end
|
||||
|
||||
test ":lockfile with application name", %{tmp_dir: tmp_dir} do
|
||||
lockfile = Path.join(tmp_dir, "install_test/mix.lock")
|
||||
lockfile |> Path.dirname() |> File.mkdir_p!()
|
||||
Mix.Project.push(GitApp)
|
||||
[_latest_rev, rev | _] = get_git_repo_revs("git_repo")
|
||||
Mix.Dep.Lock.write(lockfile, %{git_repo: {:git, fixture_path("git_repo"), rev, []}})
|
||||
Mix.ProjectStack.pop()
|
||||
|
||||
Mix.install(
|
||||
[
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")},
|
||||
{:git_repo, git: fixture_path("git_repo")}
|
||||
],
|
||||
lockfile: :install_test,
|
||||
verbose: true
|
||||
)
|
||||
|
||||
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
|
||||
assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]}
|
||||
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev
|
||||
after
|
||||
purge([GitRepo, GitRepo.MixProject])
|
||||
end
|
||||
|
||||
test ":lockfile that does not exist" do
|
||||
assert_raise File.Error, ~r/bad": no such file or directory/, fn ->
|
||||
Mix.install([], lockfile: "bad")
|
||||
|
||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user