Compare commits

...
61 Commits
Author SHA1 Message Date
José Valim 0909940b04 Release v1.14.2 2022-11-11 12:32:30 +01:00
José Valim d061cb96ef Improve docs for tracer events 2022-11-09 19:13:29 +01:00
José Valim d1f7457907 Document register 2022-11-09 19:13:29 +01:00
José Valim 123034569f Generate unique vars in defguard 2022-11-09 13:02:04 +01:00
José Valim bdea27c1e3 Include [if_undefined: :warn] on var metadata 2022-11-08 13:37:34 +01:00
José Valim 7f70f64a01 Do not include module variables in prune_binding 2022-11-08 11:36:38 +01:00
José Valim 2acd0949dc Improvements to prune binding 2022-11-08 00:33:19 +01:00
José Valim 7996c0a966 mix format 2022-11-03 11:57:31 +01:00
José Valim c01658b12d Add doctest metadata as tags 2022-11-03 11:56:44 +01:00
José Valim fdf96ad48a Prune binding from env and on literals 2022-11-03 09:57:33 +01:00
José Valim c80f8b6698 Ensure proper error message on mix test when there is no test directory 2022-11-01 18:38:18 +01:00
José Valim e65dae9f54 Improvements to ExUnit
* Do not run duplicate cases on ExUnit.run/1
* Allow test cases to not be registered on use
* Expose ExUnit.Formatter.format_assertion_diff/4
2022-10-31 15:31:25 +01:00
José Valim 6c5d7bfe3f Fix backported test 2022-10-29 21:14:32 +02:00
José Valim 804d03431a Add prune_binding to Code.eval_quoted_with_env/4 2022-10-29 20:43:59 +02:00
José Valim 29a2bb49d0 Add a test for hygienic vars on eval 2022-10-29 20:43:18 +02:00
sabiwara eb43a6a444 Mix.install accepts atoms as paths (#12204) 2022-10-16 11:32:01 +02:00
sabiwara fcb7b69cb5 Fix formatter: size*unit shortcut in bitstring (#12201) 2022-10-14 21:49:48 +02:00
José Valim 32c2a2db62 Expand :for in protocols with the appropriate env 2022-10-11 14:51:27 +02:00
José Valim a285e5876b Release v1.14.1 2022-10-10 16:28:46 +02:00
José Valim bc936f77c7 Add note about after_compiler, see #12159 2022-10-10 16:19:39 +02:00
José Valim ee602cd33e Skip unreliable stdin test on Windows 2022-10-08 22:21:12 +02:00
Michał Łępicki b5941d2f6b Expand default compile_env argument in expand literals (#12181) 2022-10-08 22:21:12 +02:00
José Valim 5f20a94895 expand_literal -> expand_literals 2022-10-07 11:57:32 +02:00
José Valim fd97c2f00f Update CHANGELOG 2022-10-07 11:54:45 +02:00
José Valim 3d2c3101c3 Add Macro.expand_literals/2 and do partial literal expansion in @
Closes #12174.
2022-10-07 11:44:10 +02:00
sabiwara 278eba5767 Handle unused :uniq comprehensions (#12116) 2022-10-05 10:43:17 +02:00
Sam Lin b2130136fb Fix range first and last description in module doc (#12169) 2022-10-04 16:23:19 +02:00
José Valim 7e76039434 More tests for malformed sequences 2022-10-04 16:14:26 +02:00
José Valim 375f2bd1cd Don't warn if :user is not defined in Erlang/OTP master (#12168) 2022-10-04 14:58:29 +02:00
José Valim f6f4f817f9 Improve coverage of malformed sequences in String 2022-10-03 12:42:21 +02:00
José Valim c2426ce98a Ignore malformed pictographic sequences, closes #12165 2022-10-03 12:42:06 +02:00
sabiwara e62a2d74ca Fix algebra edge case on :erlang.binary_to_atom/2 (#12163) 2022-10-02 23:32:57 +02:00
José Valim 61b2601488 mix format 2022-09-30 18:54:39 +02:00
José Valim 87dc7929fd Properly handle list output of :unicode_util.gc/1, closes #12160 2022-09-30 18:48:52 +02:00
Simon Prévost 78d6b88e33 Add complete list of time units on DateTime.add/3 argument error (#12152) 2022-09-23 17:26:15 +02:00
José Valim 7f494e6293 Fix signature order in type checkings messages, closes #11744 2022-09-23 13:38:25 +02:00
José Valim 41fa1db031 Do not recursively traverse vars to simplify checker, closes #12149 2022-09-23 13:38:25 +02:00
José Valim 887e2182dc Fix checker on missing debug info, closes #12140 2022-09-16 10:06:57 +02:00
José Valim 1c391417d4 Raise error message on missing metadata 2022-09-15 20:18:45 +02:00
José Valim 3f2ded08a5 Handle :port :undefined on URI.new 2022-09-15 19:40:12 +02:00
José Valim d60040636e Add :close_stdin to System.shell/2
It defaults to false as that was the behaviour
in past Elixir versions.

Closes #12135.
2022-09-15 00:09:56 +02:00
José Valim d84f88954c Fix bootstrap 2022-09-09 09:36:48 +02:00
José Valim d11d619c20 Do not delete enforce_keys on defstruct, closes #12126 2022-09-09 09:24:51 +02:00
José Valim f0943e74a7 Accept --all-warnings on mix test 2022-09-07 23:01:36 +02:00
José Valim d1055330c2 Do not crash when moduledoc and other functions are listed in only, closes #12120 2022-09-07 16:20:00 +02:00
José Valim efcb90bf10 Document noproc behaviour in start_link_supervised!, see #11737 2022-09-07 11:43:03 +02:00
José Valim 1457728e54 Make shell compatible with OTP 26 (#12114) 2022-09-06 16:33:31 +02:00
José Valim bd63670765 Do not cache inputs from .formatter.exs 2022-09-05 13:46:43 +02:00
José Valim b78b480587 Raise on missing struct for module conflict, closes #12113 2022-09-04 16:02:58 +02:00
José Valim 62ec71a489 Fix argument count for --no-pry 2022-09-03 00:31:41 +02:00
José Valim cfb51e3be9 Fix date 2022-09-01 20:18:19 +02:00
José Valim c5151e6890 Release v1.14.0 2022-09-01 20:06:00 +02:00
Vicente Merloandantedeguemon b0153d5db4 Fixes wording (#12105)
Co-authored-by: antedeguemon <antedeguemon@users.noreply.github.com>
2022-08-31 18:21:12 +02:00
José Valim d522f96fa5 List compile-time deps for after_verify 2022-08-29 23:37:25 +02:00
José Valim e281427cb8 @after_verify should define compile-time modules 2022-08-29 10:57:12 +02:00
José Valim f1b8f8ce30 Mention IEx behaviour in dbg docs 2022-08-29 08:46:19 +02:00
Andrea Leopardi 8f1296964d Don't raise on overflowing floats in Float.parse/1 (#12099)
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 and return an error.
2022-08-27 11:46:55 +02:00
Melody Horn ba03b8ec05 Clarify CaptureLog level vs Logger level (#12067) 2022-08-26 08:56:53 +02:00
José Valim a3f4d0aecc Fix module attribute with empty list in Macro.to_string, closes #12097 2022-08-26 08:56:32 +02:00
watsy0007 391c3a9097 Update docs slicing with steps (#12084) 2022-08-17 14:19:56 +02:00
José Valim ed076eb2fe Fix path on notification script 2022-08-15 12:51:05 +02:00
82 changed files with 1546 additions and 785 deletions
+1 -1
View File
@@ -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
View File
@@ -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`
+1 -1
View File
@@ -1 +1 @@
1.14.0-rc.1
1.14.2
+3 -3
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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))
+6
View File
@@ -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
View File
@@ -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`.
+11 -8
View File
@@ -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
+10 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+12 -5
View File
@@ -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
View File
@@ -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(&quoted_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.
+6 -2
View File
@@ -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 """
+20 -8
View File
@@ -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
+11 -7
View File
@@ -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 ",
+10 -6
View File
@@ -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
+6 -31
View File
@@ -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
+2 -1
View File
@@ -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"
+1 -1
View File
@@ -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`,
+3 -2
View File
@@ -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}
+10 -3
View File
@@ -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} ->
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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) ->
+4 -3
View File
@@ -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
+5 -7
View File
@@ -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);
+5 -6
View File
@@ -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},
+1 -7
View File
@@ -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).
+24 -11
View File
@@ -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
+82 -61
View File
@@ -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) ->
+7 -1
View File
@@ -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);
+32 -13
View File
@@ -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
+76
View File
@@ -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()
+7 -4
View File
@@ -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: %\{\}"
+101 -84
View File
@@ -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
+20 -3
View File
@@ -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
+4
View File
@@ -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
+9
View File
@@ -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
+82 -1
View File
@@ -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") == "í"
+1 -1
View File
@@ -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
+25
View File
@@ -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
+2 -2
View File
@@ -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 -2
View File
@@ -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() ->
+2 -2
View File
@@ -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) ->
+3 -3
View File
@@ -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
+6 -4
View File
@@ -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
+5 -2
View File
@@ -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) ::
+3
View File
@@ -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`,
+29 -15
View File
@@ -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
+24 -25
View File
@@ -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}])
+177 -159
View File
@@ -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
+1 -1
View File
@@ -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
+16 -4
View File
@@ -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
+12 -10
View File
@@ -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
+29 -4
View File
@@ -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
+19 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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") ||
+2
View File
@@ -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
+4
View File
@@ -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
-1
View File
@@ -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
+1
View File
@@ -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
+29 -26
View File
@@ -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
+3
View File
@@ -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,
+34 -3
View File
@@ -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)
+3 -1
View File
@@ -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
+29
View File
@@ -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"""
+45
View File
@@ -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")