Compare commits

...
21 Commits
Author SHA1 Message Date
José Valim 6730d669fb Release v1.14.3 2023-01-14 16:22:53 +01:00
José Valim fb4ccfa656 Properly handle Macro inputs and output types in expansion
Closes #12299
Closes #12324
2023-01-14 16:00:45 +01:00
sabiwara ff2ea69710 Fix Macro.to_string/1 for large negative integers (#12326) 2023-01-10 10:50:51 +01:00
José Valim 2146bb449f Remove unecessary :beam_lib.info/1 call, closes #12317 2023-01-06 12:12:36 +01:00
José Valim 5a583c753b Consistently merge precisions in Calendar subsecond operations, closes #12303 2022-12-18 12:18:23 +01:00
José Valim ec31cc2331 Escape maps when expanding __ENV__ 2022-12-12 19:31:13 +01:00
Alex NaserandJosé Valim 6549d209d9 Precompile runtime module paths (#12294)
Co-authored-by: José Valim <jose.valim@dashbit.co>
2022-12-08 19:37:01 +01:00
José Valim 71d27919ad Revert "Ensure all runtime_modules loaded before mapping their paths (#12287)"
We don't want to load all modules because some may be
specified as `@compile {:autoload, false}`.

This reverts commit a8aea1c763.
2022-12-08 06:38:31 +01:00
Alex NaserandAlex Naser a8aea1c763 Ensure all runtime_modules loaded before mapping their paths (#12287)
Co-authored-by: Alex Naser <alex.naser@remote.com>
2022-12-05 20:46:27 +01:00
José Valim a72f18b38b Fix Range.disjoint?/2 2022-12-05 16:20:44 +01:00
José Valim a1a37c67b6 Optimize range membership implementation, closes #12282 2022-12-04 13:02:56 +01:00
José Valim ff73830103 Return values from running doctests and order tests 2022-12-01 13:22:03 +01:00
Thanabodee Charoenpiriyakij 950bcc0704 Fix Calendar.strftime/2 format negative year incorrectly (#12268)
Closes #12266
2022-11-24 09:37:01 +01:00
José Valim 80564fc954 Do not quote doctest excerpt 2022-11-23 18:01:31 +01:00
José Valim 94e0e386ce Do not crash for missing line info on type warnings
Closes #12263.
2022-11-23 05:35:09 +01:00
José Valim bfa799b31f Do not warn on captured underscored vars from defmodule
Closes #12262
2022-11-23 05:19:58 +01:00
Marcus Kruse abe2af477b Fix wildcard edge case (#12255) 2022-11-20 11:06:15 +01:00
sabiwara 2a82c35e39 Fix unused for optimization (#12252) 2022-11-19 10:27:40 +01:00
José Valim 1f14211a5e Handle false positive on List.to_charlist/1 formatting, closes #12248 2022-11-17 20:12:59 +01:00
José Valim 1ae36fa079 Keep expression when normalizing is_struct in blame
Closes #12243.
2022-11-15 23:03:53 +01:00
José Valim e3fc947b92 Pass the relevant module when generating unique vars for guards 2022-11-11 12:59:36 +01:00
31 changed files with 345 additions and 185 deletions
+31
View File
@@ -241,6 +241,37 @@ 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.3 (2023-01-14)
### 1. Enhancements
#### Elixir
* [Kernel] Speed up loading of runtime modules in the parallel compiler
* [Range] Optimize range membership implementation
#### ExUnit
* [ExUnit] Return values from running doctests and make their order consistent
### 2. Bug fixes
#### Elixir
* [Calendar] Fix handling of negative years in `Calendar.strftime/2`
* [Exception] Improve blaming of FunctionClauseError with `is_struct/2` guards
* [Kernel] Fix invalid variable scoping in `defguard` expansion
* [Kernel] Do not warn on captured underscored vars from `defmodule`
* [Kernel] Do not crash for missing line info on type warnings
* [Macro] Fix `Macro.to_string/1` for large negative integers
* [Macro] Properly type and escape expansion of `__ENV__` in macros
* [Path] Make sure `Path.wildcard/2` expands `..` symlinks accordingly
* [Range] Address corner cases in `Range.disjoint?/2` implementation
#### ExUnit
* [ExUnit.DocTest] Remove unnecessary literal quotes from error message on reports
## v1.14.2 (2022-11-11)
### 1. Enhancements
+1 -1
View File
@@ -1 +1 @@
1.14.2
1.14.3
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.14.2
ELIXIR_VERSION=1.14.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -1,6 +1,6 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
set ELIXIR_VERSION=1.14.2
set ELIXIR_VERSION=1.14.3
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
+8 -1
View File
@@ -791,7 +791,14 @@ defmodule Calendar do
# Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> Integer.to_string() |> pad_leading(width, pad)
{sign, year} =
if datetime.year < 0 do
{?-, -datetime.year}
else
{[], datetime.year}
end
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
parse(rest, datetime, format_options, [result | acc])
end
+8
View File
@@ -1493,6 +1493,13 @@ defmodule DateTime do
iex> dt |> DateTime.add(1, :day, FakeTimeZoneDatabase)
#DateTime<2019-04-01 02:00:00+02:00 CEST Europe/Copenhagen>
This operation merges the precision of the naive date time with the given unit:
iex> result = DateTime.add(~U[2014-10-02 00:29:10Z], 21, :millisecond)
~U[2014-10-02 00:29:10.021Z]
iex> result.microsecond
{21000, 3}
"""
@doc since: "1.8.0"
@spec add(
@@ -1537,6 +1544,7 @@ defmodule DateTime do
ppd = System.convert_time_unit(86400, :second, unit)
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
result =
datetime
+11
View File
@@ -247,6 +247,17 @@ defmodule Calendar.ISO do
defguardp is_utc_offset(offset) when is_integer(offset)
defguardp is_std_offset(offset) when is_integer(offset)
@doc """
Converts a `t:System.time_unit/0` to precision.
Integer-based time units always get maximum precision.
"""
def time_unit_to_precision(:nanosecond), do: 6
def time_unit_to_precision(:microsecond), do: 6
def time_unit_to_precision(:millisecond), do: 3
def time_unit_to_precision(:second), do: 0
def time_unit_to_precision(_), do: 6
@doc """
Parses a time `string` in the `:extended` format.
+7 -10
View File
@@ -379,7 +379,7 @@ defmodule NaiveDateTime do
It can also work with subsecond precisions:
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10], 2_000, :millisecond)
~N[2014-10-02 00:29:12]
~N[2014-10-02 00:29:12.000]
As well as days/hours/minutes:
@@ -390,16 +390,12 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(~N[2015-02-28 00:29:10], 60, :minute)
~N[2015-02-28 01:29:10]
This operation keeps the precision of the naive date time:
This operation merges the precision of the naive date time with the given unit:
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10.021], 21, :second)
~N[2014-10-02 00:29:31.021]
And ignores any changes below the precision:
iex> hidden = NaiveDateTime.add(~N[2014-10-02 00:29:10], 21, :millisecond)
iex> hidden.microsecond # ~N[2014-10-02 00:29:10]
{21000, 0}
iex> result = NaiveDateTime.add(~N[2014-10-02 00:29:10], 21, :millisecond)
~N[2014-10-02 00:29:10.021]
iex> result.microsecond
{21000, 3}
Operations on top of gregorian seconds or the Unix epoch are optimized:
@@ -440,6 +436,7 @@ defmodule NaiveDateTime do
)
when is_integer(amount_to_add) do
ppd = System.convert_time_unit(86400, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
naive_datetime
|> to_iso_days()
+10 -2
View File
@@ -493,6 +493,13 @@ defmodule Time do
iex> Time.add(~T[17:10:05], 30, :minute)
~T[17:40:05]
This operation merges the precision of the time with the given unit:
iex> result = Time.add(~T[00:29:10], 21, :millisecond)
~T[00:29:10.021]
iex> result.microsecond
{21000, 3}
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -511,6 +518,7 @@ defmodule Time do
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
total = time_to_microseconds(time) + amount_to_add
parts = Integer.mod(total, @parts_per_day)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
@@ -736,14 +744,14 @@ defmodule Time do
hour: hour1,
minute: minute1,
second: second1,
microsecond: {microsecond1, @parts_per_day}
microsecond: {microsecond1, _}
},
%{
calendar: Calendar.ISO,
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, @parts_per_day}
microsecond: {microsecond2, _}
},
unit
) do
+4
View File
@@ -1583,6 +1583,10 @@ defmodule Code.Formatter do
insert_underscores(int_part) <> "." <> decimal_part
end
defp insert_underscores("-" <> digits) do
"-" <> insert_underscores(digits)
end
defp insert_underscores(digits) do
cond do
digits =~ "_" ->
+30 -23
View File
@@ -70,15 +70,15 @@ defmodule Code.Normalizer do
# Atoms with interpolations
defp do_normalize(
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta,
[{:<<>>, _, args} = string, :utf8]},
[{:<<>>, _, parts} = string, :utf8]},
state
)
when is_list(args) do
when is_list(parts) 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
if parts == [] or binary_interpolated?(parts) do
# a non-normalized :utf8 atom signals an atom interpolation
:utf8
else
@@ -96,23 +96,27 @@ defmodule Code.Normalizer do
end
# Charlists with interpolations
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}, state) do
parts =
Enum.map(parts, fn
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args} ->
args = normalize_args(args, state)
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
Enum.map(parts, fn
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args} ->
args = normalize_args(args, state)
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args}
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args}
part ->
if state.escape do
maybe_escape_literal(part, state)
else
part
end
end)
part when is_binary(part) ->
if state.escape do
maybe_escape_literal(part, state)
else
part
end
end)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
end
end
# Don't normalize the `Access` atom in access syntax
@@ -389,11 +393,11 @@ defmodule Code.Normalizer do
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts} = quoted, state, escape_interpolation \\ false) do
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
meta = patch_meta_line(meta, state.parent_meta)
parts =
if interpolated?(quoted) do
if binary_interpolated?(parts) do
normalize_interpolation_parts(parts, %{state | parent_meta: meta}, escape_interpolation)
else
state = %{state | parent_meta: meta}
@@ -543,8 +547,7 @@ defmodule Code.Normalizer do
term
end
# Check if we have an interpolated string.
defp interpolated?({:<<>>, _, [_ | _] = parts}) do
defp binary_interpolated?(parts) do
Enum.all?(parts, fn
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true
binary when is_binary(binary) -> true
@@ -552,8 +555,12 @@ defmodule Code.Normalizer do
end)
end
defp interpolated?(_) do
false
defp list_interpolated?(parts) do
Enum.all?(parts, fn
{{:., _, [Kernel, :to_string]}, _, [_]} -> true
binary when is_binary(binary) -> true
_ -> false
end)
end
defp patch_meta_line(meta, parent_meta) do
+3 -5
View File
@@ -294,13 +294,11 @@ defmodule Exception do
end
end
defp undo_is_struct_guard(
{:and, meta, [_, %{node: {_, _, [{_, _, [_, {struct, _, _}]} | optional]}}]}
) do
defp undo_is_struct_guard({:and, meta, [_, %{node: {_, _, [{_, _, [_, arg]} | optional]}}]}) do
args =
case optional do
[] -> [{struct, meta, nil}]
[module] -> [{struct, meta, nil}, module]
[] -> [arg]
[module] -> [arg, module]
end
%{match?: meta[:value], node: {:is_struct, meta, args}}
+1 -1
View File
@@ -4806,7 +4806,7 @@ defmodule Kernel do
var_meta =
case prune do
true -> [generated: true, keep_unused: true]
false -> [generated: false]
false -> [generated: true]
end
module_vars = :lists.map(&module_var(&1, var_meta), :maps.keys(versioned_vars))
@@ -271,12 +271,6 @@ defmodule Kernel.ParallelCompiler do
for {{:module, module}, _} <- result,
do: module
runtime_modules =
for module <- runtime_modules,
path = :code.which(module),
is_list(path) and path != [],
do: {module, path}
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(checker, runtime_modules)
end)
+3 -3
View File
@@ -331,7 +331,7 @@ defmodule Kernel.Utils do
quote do
case Macro.Env.in_guard?(__CALLER__) do
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
end
end
end
@@ -361,7 +361,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs) do
defp unquote_refs_once(guard, refs, module) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -375,7 +375,7 @@ defmodule Kernel.Utils do
%{} ->
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
new_var = Macro.unique_var(generated, Elixir)
new_var = Macro.unique_var(generated, module)
{new_var, Map.put(acc, pair, {new_var, var})}
end
+11 -6
View File
@@ -1763,7 +1763,7 @@ defmodule Macro do
end
"""
@spec expand_once(t(), Macro.Env.t()) :: t()
@spec expand_once(input(), Macro.Env.t()) :: output()
def expand_once(ast, env) do
elem(do_expand_once(ast, env), 0)
end
@@ -1795,13 +1795,15 @@ defmodule Macro do
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
do: {:filename.dirname(env.file), true}
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom),
do: {{:%{}, [], Map.to_list(env)}, true}
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
env = update_in(env.versioned_vars, &maybe_escape_map/1)
{maybe_escape_map(env), true}
end
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
when is_atom(atom) and is_atom(field) do
if Map.has_key?(env, field) do
{Map.get(env, field), true}
{maybe_escape_map(Map.get(env, field)), true}
else
{original, false}
end
@@ -1884,6 +1886,9 @@ defmodule Macro do
# Anything else is just returned
defp do_expand_once(other, _env), do: {other, false}
defp maybe_escape_map(map) when is_map(map), do: {:%{}, [], Map.to_list(map)}
defp maybe_escape_map(other), do: other
@doc """
Returns `true` if the given name and arity is a special form.
"""
@@ -1995,7 +2000,7 @@ defmodule Macro do
when they change.
"""
@doc since: "1.14.1"
@spec expand_literals(t(), Macro.Env.t()) :: t()
@spec expand_literals(input(), Macro.Env.t()) :: output()
def expand_literals(ast, env) do
{ast, :ok} = expand_literals(ast, :ok, fn node, :ok -> {expand(node, env), :ok} end)
ast
@@ -2075,7 +2080,7 @@ defmodule Macro do
This function uses `expand_once/2` under the hood. Check
it out for more information and examples.
"""
@spec expand(t(), Macro.Env.t()) :: t()
@spec expand(input(), Macro.Env.t()) :: output()
def expand(ast, env) do
expand_until({ast, true}, env)
end
+6 -4
View File
@@ -62,7 +62,10 @@ defmodule Module.ParallelChecker do
if is_map(info) do
info
else
info |> File.read!() |> maybe_module_map(module)
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
end
module_map && cache_from_module_map(ets, module_map)
@@ -136,7 +139,7 @@ defmodule Module.ParallelChecker do
the modules and adds the ExCk chunk to the binaries. Returns the updated
list of warnings from the verification.
"""
@spec verify(pid(), [{module(), binary()}]) :: [warning()]
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
def verify(checker, runtime_files) do
for {module, file} <- runtime_files do
spawn({self(), checker}, module, file)
@@ -324,8 +327,7 @@ defmodule Module.ParallelChecker do
defp cache_from_chunk(ets, module) do
with {^module, binary, _filename} <- :code.get_object_code(module),
{:ok, ^module} <- binary |> :beam_lib.info() |> Keyword.fetch(:module),
{:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
cache_chunk(ets, module, contents.exports)
true
+4 -4
View File
@@ -135,8 +135,7 @@ defmodule Module.Types do
# Collect relevant information from context and traces to report error
def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do
{fun, arity} = context.function
line = get_meta(stack.last_expr)[:line]
location = {context.file, line, {context.module, fun, arity}}
location = {context.file, get_line(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[signature | args], traces} = lift_all_types([signature | args], traces, context)
@@ -146,8 +145,7 @@ defmodule Module.Types do
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
line = get_meta(stack.last_expr)[:line]
location = {context.file, line, {context.module, fun, arity}}
location = {context.file, get_line(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[left, right], traces} = lift_all_types([left, right], traces, context)
@@ -155,6 +153,8 @@ defmodule Module.Types do
{Module.Types, error, location}
end
defp get_line(stack), do: stack.last_expr |> get_meta() |> Keyword.get(:line, 0)
# Collect relevant traces from context.traces using stack.unify_stack
defp type_traces(stack, context) do
# TODO: Do we need the unify_stack or is enough to only get the last variable
+4
View File
@@ -603,6 +603,10 @@ defmodule Path do
:file.read_link_info(file)
end
def read_file_info(file) do
:file.read_file_info(file)
end
def list_dir(dir) do
case :file.list_dir(dir) do
{:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
+31 -27
View File
@@ -294,22 +294,24 @@ defmodule Range do
iex> Range.disjoint?(1..10//2, 2..10//2)
true
# First element in common in all below is 29
iex> Range.disjoint?(2..100//3, 9..100//5)
false
iex> Range.disjoint?(101..2//-3, 99..9//-5)
false
# First element in common is 29
iex> Range.disjoint?(1..100//14, 8..100//21)
false
iex> Range.disjoint?(57..-1//-14, 8..100//21)
false
iex> Range.disjoint?(1..100//14, 51..8//-21)
iex> Range.disjoint?(1..100//14, 50..8//-21)
false
# If 29 is out of range
iex> Range.disjoint?(1..28//14, 8..28//21)
true
# First element in common is 14
iex> Range.disjoint?(2..28//3, 9..28//5)
false
iex> Range.disjoint?(26..2//-3, 29..9//-5)
false
# Starting from the back without alignment
iex> Range.disjoint?(27..11//-3, 30..0//-7)
true
"""
@@ -334,21 +336,28 @@ defmodule Range do
# progressions and see if they belong within the ranges
# https://math.stackexchange.com/questions/1656120/formula-to-find-the-first-intersection-of-two-arithmetic-progressions
{gcd, u, v} = Integer.extended_gcd(-step1, step2)
c = first1 - first2 + step2 - step1
t1 = -c / step1 * u
t2 = -c / step2 * v
t = max(floor(t1) + 1, floor(t2) + 1)
x = div(c * u + t * step2, gcd) - 1
y = div(c * v + t * step1, gcd) - 1
x < 0 or first1 + x * step1 > last1 or
y < 0 or first2 + y * step2 > last2
if rem(first2 - first1, gcd) == 0 do
c = first1 - first2 + step2 - step1
t1 = -c / step2 * u
t2 = -c / step1 * v
t = max(floor(t1) + 1, floor(t2) + 1)
x = div(c * u + t * step2, gcd) - 1
y = div(c * v + t * step1, gcd) - 1
x < 0 or first1 + x * step1 > last1 or
y < 0 or first2 + y * step2 > last2
else
true
end
end
end
end
@compile inline: [normalize: 3]
defp normalize(first, last, step) when first > last, do: {last, first, -step}
defp normalize(first, last, step) when first > last,
do: {first - abs(div(first - last, step) * step), first, -step}
defp normalize(first, last, step), do: {first, last, step}
@doc false
@@ -387,16 +396,11 @@ defimpl Enumerable, for: Range do
{:done, acc}
end
def member?(first..last//step = range, value) when is_integer(value) do
cond do
Range.size(range) == 0 ->
{:ok, false}
first <= last ->
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
true ->
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
def member?(first..last//step, value) when is_integer(value) do
if step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
else
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
end
+1 -1
View File
@@ -29,7 +29,7 @@ translate_into(Meta, Cases, Expr, Opts, S) ->
{TInto, SI} =
case lists:keyfind(into, 1, Opts) of
{into, Into} -> elixir_erl_pass:translate(Into, Ann, S);
false -> {{nil, Ann}, S}
false -> {false, S}
end,
TUniq = lists:keyfind(uniq, 1, Opts) == {uniq, true},
+7
View File
@@ -364,5 +364,12 @@ defmodule CalendarTest do
Calendar.strftime(~D[2019-08-15], "%D", unknown: "option")
end
end
test "zero padding for negative year" do
assert Calendar.strftime(Date.new!(-1, 1, 1), "%Y") == "-0001"
assert Calendar.strftime(Date.new!(-11, 1, 1), "%Y") == "-0011"
assert Calendar.strftime(Date.new!(-111, 1, 1), "%Y") == "-0111"
assert Calendar.strftime(Date.new!(-1111, 1, 1), "%Y") == "-1111"
end
end
end
@@ -515,6 +515,17 @@ defmodule Code.Normalizer.QuotedASTTest do
test "charlist" do
assert quoted_to_string(quote(do: [])) == "[]"
assert quoted_to_string(quote(do: 'abc')) == "'abc'"
# False positive
assert quoted_to_string(
quote do
:"Elixir.List".to_charlist([
case var do
var -> var
end
])
end
) =~ "List.to_charlist([\n case var do\n var -> var\n end\n])"
end
test "string" do
+15
View File
@@ -426,6 +426,8 @@ defmodule ExceptionTest do
when is_binary(url) or (is_struct(url) and is_struct(url, url_module) and false) do
url
end
def sub_get!(url) when is_struct(url.sub, URI), do: url.sub
end
)
@@ -475,6 +477,19 @@ defmodule ExceptionTest do
def get!(url) when -is_binary(url)- or is_struct(url) and is_struct(url, URI) and -false-
"""
assert blame_message(Req, & &1.sub_get!(%{})) =~ """
no function clause matching in ExceptionTest.Req.sub_get!/1
The following arguments were given to ExceptionTest.Req.sub_get!/1:
# 1
%{}
Attempted function clauses (showing 1 out of 1):
def sub_get!(url) when -is_struct(url.sub, URI)-
"""
end
test "annotates badarg on apply" do
+20 -2
View File
@@ -172,10 +172,18 @@ defmodule MacroTest do
end
test "env" do
env = %{__ENV__ | line: 0}
assert Macro.expand_once(quote(do: __ENV__), env) == {:%{}, [], Map.to_list(env)}
env = %{__ENV__ | line: 0, lexical_tracker: self()}
expanded = Macro.expand_once(quote(do: __ENV__), env)
assert Macro.validate(expanded) == :ok
assert Code.eval_quoted(expanded) == {env, []}
assert Macro.expand_once(quote(do: __ENV__.file), env) == env.file
assert Macro.expand_once(quote(do: __ENV__.unknown), env) == quote(do: __ENV__.unknown)
expanded = Macro.expand_once(quote(do: __ENV__.versioned_vars), env)
assert Macro.validate(expanded) == :ok
assert Code.eval_quoted(expanded) == {env.versioned_vars, []}
end
defmacro local_macro(), do: raise("ignored")
@@ -421,6 +429,16 @@ defmodule MacroTest do
test "converts quoted to string" do
assert Macro.to_string(quote do: hello(world)) == "hello(world)"
end
test "large number literals" do
# with quote
assert Macro.to_string(quote do: 576_460_752_303_423_455) == "576_460_752_303_423_455"
assert Macro.to_string(quote do: -576_460_752_303_423_455) == "-576_460_752_303_423_455"
# without quote
assert Macro.to_string(576_460_752_303_423_455) == "576_460_752_303_423_455"
assert Macro.to_string(-576_460_752_303_423_455) == "-576_460_752_303_423_455"
end
end
describe "to_string/2" do
@@ -562,6 +562,22 @@ defmodule Module.Types.IntegrationTest do
end
end
describe "regressions" do
test "handle missing location info from quoted" do
assert capture_io(:stderr, fn ->
quote do
defmodule X do
def f() do
x = %{}
%{x | key: :value}
end
end
end
|> Code.compile_quoted()
end) =~ "warning:"
end
end
describe "after_verify" do
test "reports functions" do
files = %{
+7
View File
@@ -332,6 +332,13 @@ defmodule ModuleTest do
assert Elixir.ModuleTest.NonAtomAlias.hello() == :world
end
test "does not warn on captured underscored vars" do
_unused = 123
defmodule __MODULE__.NoVarWarning do
end
end
@compile {:no_warn_undefined, ModuleCreateSample}
test "create" do
+15
View File
@@ -38,6 +38,21 @@ defmodule PathTest do
File.rm_rf(Path.join(config.tmp_dir, "wildcard"))
end
@tag :tmp_dir
test "wildcard/2 follows ..", config do
hello = Path.join(config.tmp_dir, "wildcard/hello")
world = Path.join(config.tmp_dir, "wildcard/world")
File.mkdir_p(hello)
File.touch(world)
assert Path.wildcard(Path.join(config.tmp_dir, "wildcard/w*/../h*")) == []
assert Path.wildcard(Path.join(config.tmp_dir, "wildcard/h*/../w*")) ==
[Path.join(config.tmp_dir, "wildcard/hello/../world")]
after
File.rm_rf(Path.join(config.tmp_dir, "wildcard"))
end
test "wildcard/2 raises on null byte" do
assert_raise ArgumentError, ~r/null byte/, fn -> Path.wildcard("foo\0bar") end
end
+27 -33
View File
@@ -238,9 +238,9 @@ defmodule ExUnit.DocTest do
extract(module)
|> filter_by_opts(module, opts)
|> Stream.with_index(1)
|> Enum.map(fn {test, acc} ->
compile_test(test, module, import, acc, file, tags)
|> Enum.sort_by(& &1.line)
|> Enum.with_index(fn test, index ->
compile_test(test, module, import, index + 1, file, tags)
end)
end
@@ -313,8 +313,8 @@ defmodule ExUnit.DocTest do
end
tests =
Enum.map(exprs, fn {expr, expected, formatted} ->
test_case_content(expr, expected, location, stack, formatted)
Enum.map(exprs, fn {expr, expected, doctest} ->
test_case_content(expr, expected, location, stack, doctest)
end)
{:__block__, [], test_import(module, do_import) ++ tests}
@@ -327,12 +327,11 @@ defmodule ExUnit.DocTest do
end) > 1
end
defp test_case_content(expr, :test, location, stack, formatted) do
string_to_quoted(location, stack, expr, "\n" <> formatted) |> insert_assertions()
defp test_case_content(expr, :test, location, stack, doctest) do
string_to_quoted(location, stack, expr, doctest) |> insert_assertions()
end
defp test_case_content(expr, {:test, expected}, location, stack, formatted) do
doctest = "\n" <> formatted <> "\n" <> expected
defp test_case_content(expr, {:test, expected}, location, stack, doctest) do
expr_ast = string_to_quoted(location, stack, expr, doctest) |> insert_assertions()
expected_ast = string_to_quoted(location, stack, expected, doctest)
last_expr = Macro.to_string(last_expr(expr_ast))
@@ -340,56 +339,53 @@ defmodule ExUnit.DocTest do
quote do
value = unquote(expr_ast)
expected = unquote(expected_ast)
formatted = unquote(formatted)
doctest = unquote(doctest)
last_expr = unquote(last_expr)
expected_expr = unquote(expected)
stack = unquote(stack)
ExUnit.DocTest.__test__(value, expected, formatted, last_expr, expected_expr, stack)
ExUnit.DocTest.__test__(value, expected, doctest, last_expr, expected_expr, stack)
end
end
defp test_case_content(expr, {:inspect, expected}, location, stack, formatted) do
doctest = "\n" <> formatted <> "\n" <> expected
defp test_case_content(expr, {:inspect, expected}, location, stack, doctest) do
expr_ast = string_to_quoted(location, stack, expr, doctest) |> insert_assertions()
expected_ast = string_to_quoted(location, stack, expected, doctest)
last_expr = Macro.to_string(last_expr(expr_ast))
quote do
value = unquote(expr_ast)
expected = unquote(expected_ast)
formatted = unquote(formatted)
expected = unquote(expected)
doctest = unquote(doctest)
last_expr = unquote(last_expr)
expected_expr = unquote(expected)
expected_expr = unquote(inspect(expected))
stack = unquote(stack)
ExUnit.DocTest.__inspect__(value, expected, formatted, last_expr, expected_expr, stack)
ExUnit.DocTest.__inspect__(value, expected, doctest, last_expr, expected_expr, stack)
end
end
defp test_case_content(expr, {:error, exception, message}, location, stack, formatted) do
doctest = "\n" <> formatted <> "\n** (#{inspect(exception)}) #{inspect(message)}"
defp test_case_content(expr, {:error, exception, message}, location, stack, doctest) do
expr_ast = string_to_quoted(location, stack, expr, doctest)
quote do
stack = unquote(stack)
message = unquote(message)
formatted = unquote(formatted)
doctest = unquote(doctest)
exception = unquote(exception)
ExUnit.DocTest.__error__(fn -> unquote(expr_ast) end, message, exception, formatted, stack)
ExUnit.DocTest.__error__(fn -> unquote(expr_ast) end, message, exception, doctest, stack)
end
end
@doc false
def __test__(value, expected, formatted, last_expr, expected_expr, stack) do
def __test__(value, expected, doctest, last_expr, expected_expr, stack) do
case value do
^expected ->
:ok
{:ok, value}
_ ->
error = [
message: "Doctest failed",
doctest: "\n" <> formatted <> "\n" <> expected_expr,
doctest: doctest,
expr: "#{last_expr} === #{String.trim(expected_expr)}",
left: value,
right: expected
@@ -400,7 +396,7 @@ defmodule ExUnit.DocTest do
end
@doc false
def __inspect__(value, expected, formatted, last_expr, expected_expr, parent_stack) do
def __inspect__(value, expected, doctest, last_expr, expected_expr, parent_stack) do
result =
try do
inspect(value, safe: false)
@@ -415,10 +411,9 @@ defmodule ExUnit.DocTest do
case result do
:ok ->
:ok
{:ok, value}
{extra, stack} ->
doctest = "\n" <> formatted <> "\n" <> expected_expr
expr = "inspect(#{last_expr}) === #{String.trim(expected_expr)}"
error = [doctest: doctest, expr: expr] ++ extra
reraise ExUnit.AssertionError, error, stack ++ parent_stack
@@ -426,7 +421,7 @@ defmodule ExUnit.DocTest do
end
@doc false
def __error__(fun, message, exception, formatted, stack) do
def __error__(fun, message, exception, doctest, stack) do
try do
fun.()
rescue
@@ -451,12 +446,10 @@ defmodule ExUnit.DocTest do
end
if failed do
doctest = "\n" <> formatted <> "\n** (#{inspect(exception)}) #{inspect(message)}"
reraise ExUnit.AssertionError, [message: failed, doctest: doctest], stack
end
else
_ ->
doctest = "\n" <> formatted <> "\n** (#{inspect(exception)}) #{inspect(message)}"
failed = "Doctest failed: expected exception #{inspect(exception)} but nothing was raised"
error = [message: failed, doctest: doctest]
reraise ExUnit.AssertionError, error, stack
@@ -862,7 +855,8 @@ defmodule ExUnit.DocTest do
end
defp add_expr(%{exprs: exprs} = test, expr, expected, formatted) do
%{test | exprs: [{expr, tag_expected(expected), formatted} | exprs]}
doctest = "\n" <> formatted <> "\n" <> expected
%{test | exprs: [{expr, tag_expected(expected), doctest} | exprs]}
end
defp tag_expected(string) do
@@ -876,7 +870,7 @@ defmodule ExUnit.DocTest do
_ ->
if inspectable?(string) do
{:inspect, inspect(string)}
{:inspect, string}
else
{:test, string}
end
+46 -53
View File
@@ -577,7 +577,7 @@ defmodule ExUnit.DocTestTest do
Doctest failed
doctest:
iex> :oops
"#Inspect<[]>"
#Inspect<[]>
code: inspect(:oops) === "#Inspect<[]>"
left: ":oops"
right: "#Inspect<[]>"
@@ -600,7 +600,7 @@ defmodule ExUnit.DocTestTest do
Doctest failed: expected exception WhatIsThis but got RuntimeError with message "oops"
doctest:
iex> raise "oops"
** (WhatIsThis) "oops"
** (WhatIsThis) oops
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 18}: ExUnit.DocTestTest.Invalid (module)
"""
@@ -615,7 +615,7 @@ defmodule ExUnit.DocTestTest do
"oops"
doctest:
iex> raise "oops"
** (RuntimeError) "hello"
** (RuntimeError) hello
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 21}: ExUnit.DocTestTest.Invalid (module)
"""
@@ -635,18 +635,17 @@ defmodule ExUnit.DocTestTest do
"""
assert output =~ """
9) doctest ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (9) (ExUnit.DocTestTest.ActuallyCompiled)
9) doctest ExUnit.DocTestTest.Invalid.b/0 (9) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 57}:5: unexpected token: "`" (column 5, code point U+0060)
#{line_placeholder(starting_line + 57)} |
#{starting_line + 57} | ```
#{line_placeholder(starting_line + 57)} | ^
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 33}:6: syntax error before: '*'
#{line_placeholder(starting_line + 33)} |
#{starting_line + 33} | 1 + * 1
#{line_placeholder(starting_line + 33)} | ^
doctest:
iex> 1 + 2
3
```
iex> 1 + * 1
1
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 56}: ExUnit.DocTestTest.Invalid (module)
test/ex_unit/doc_test_test.exs:#{starting_line + 33}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ """
@@ -680,7 +679,22 @@ defmodule ExUnit.DocTestTest do
"""
assert output =~ """
12) doctest ExUnit.DocTestTest.Invalid.invalid_utf8/0 (12) (ExUnit.DocTestTest.ActuallyCompiled)
12) doctest ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (12) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 57}:5: unexpected token: "`" (column 5, code point U+0060)
#{line_placeholder(starting_line + 57)} |
#{starting_line + 57} | ```
#{line_placeholder(starting_line + 57)} | ^
doctest:
iex> 1 + 2
3
```
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 56}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ """
13) doctest ExUnit.DocTestTest.Invalid.invalid_utf8/0 (13) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (UnicodeConversionError) invalid encoding starting at <<255, 34, 41>>
stacktrace:
@@ -688,7 +702,7 @@ defmodule ExUnit.DocTestTest do
"""
assert output =~ """
13) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (13) (ExUnit.DocTestTest.ActuallyCompiled)
14) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (14) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (TokenMissingError) test/ex_unit/doc_test_test.exs:#{starting_line + 69}:7: missing terminator: } (for "{" starting at line #{starting_line + 69})
#{line_placeholder(starting_line + 69)} |
@@ -702,28 +716,8 @@ defmodule ExUnit.DocTestTest do
test/ex_unit/doc_test_test.exs:#{starting_line + 69}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ "14) doctest ExUnit.DocTestTest.Invalid.raising_inspect/0"
assert output =~ "iex> ExUnit.DocTestTest.Haiku.new(:this, :is, {:not, :a, :haiku})"
assert output =~
"test/ex_unit/doc_test_test.exs:#{starting_line + 82}: ExUnit.DocTestTest.Invalid (module)"
assert output =~ """
15) doctest ExUnit.DocTestTest.Invalid.b/0 (15) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 33}:6: syntax error before: '*'
#{line_placeholder(starting_line + 33)} |
#{starting_line + 33} | 1 + * 1
#{line_placeholder(starting_line + 33)} | ^
doctest:
iex> 1 + * 1
1
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 33}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ """
16) doctest ExUnit.DocTestTest.Invalid.t/0 (16) (ExUnit.DocTestTest.ActuallyCompiled)
15) doctest ExUnit.DocTestTest.Invalid.t/0 (15) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 75}:6: syntax error before: '*'
#{line_placeholder(starting_line + 75)} |
@@ -735,6 +729,12 @@ defmodule ExUnit.DocTestTest do
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 75}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ "16) doctest ExUnit.DocTestTest.Invalid.raising_inspect/0"
assert output =~ "iex> ExUnit.DocTestTest.Haiku.new(:this, :is, {:not, :a, :haiku})"
assert output =~
"test/ex_unit/doc_test_test.exs:#{starting_line + 82}: ExUnit.DocTestTest.Invalid (module)"
end
test "pattern matching assertions in doctests" do
@@ -900,24 +900,7 @@ defmodule ExUnit.DocTestTest do
end
end
test "fails when testing single function not found" do
message = """
test/ex_unit/doc_test_test\.exs: undefined or private function given to doctest:
ExUnit.DocTestTest.SomewhatGoodModuleWithOnly.three/0
"""
assert_raise ExUnit.DocTest.Error, message, fn ->
defmodule NeverCompiled do
import ExUnit.DocTest
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [three: 0], import: true
end
end
end
test "fails when testing multiple functions not found" do
test "fails when testing functions are not found" do
message = """
test/ex_unit/doc_test_test\.exs: undefined or private functions given to doctest:
@@ -989,6 +972,16 @@ defmodule ExUnit.DocTestTest do
end
end
test "doctest direct invocation" do
defmodule Direct do
use ExUnit.Case, register: false
doctest ExUnit.DocTestTest.GoodModule, import: true
end
[head | _] = Direct.__ex_unit__().tests
assert apply(Direct, head.name, [%{}]) == {:ok, 2}
end
defp line_placeholder(line_number) do
digits =
line_number
+5 -1
View File
@@ -465,7 +465,11 @@ defmodule Mix.Compilers.Elixir do
modules_set = Map.from_keys(modules, true)
{_, runtime_modules} = fixpoint_runtime_modules(sources, modules_set)
{:runtime, runtime_modules, warnings}
runtime_paths =
Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")})
{:runtime, runtime_paths, warnings}
else
Mix.Utils.compiling_n(length(changed), :ex)