diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83716404c1f..978f36a2f9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,16 +18,16 @@ permissions: jobs: test_linux: - name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }} + name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }} strategy: fail-fast: false matrix: include: + - otp_version: "27.1" + deterministic: true - otp_version: "27.1" otp_latest: true - erlc_opts: "warnings_as_errors" - otp_version: "27.0" - erlc_opts: "warnings_as_errors" - otp_version: "26.0" - otp_version: "25.3" - otp_version: "25.0" @@ -36,11 +36,6 @@ jobs: - otp_version: maint development: true runs-on: ubuntu-24.04 - # Earlier Erlang/OTP versions ignored compiler directives - # when using warnings_as_errors. So we only set ERLC_OPTS - # from Erlang/OTP 27+. - env: - ERLC_OPTS: ${{ matrix.erlc_opts || '' }} steps: - uses: actions/checkout@v4 with: @@ -48,6 +43,9 @@ jobs: - uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp_version }} + - name: Set ERL_COMPILER_OPTIONS + if: ${{ matrix.deterministic }} + run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV - name: Compile Elixir run: | make compile @@ -72,12 +70,12 @@ jobs: cd ../elixir/ make docs - name: Check reproducible builds + if: ${{ matrix.deterministic }} run: | rm -rf .git # Recompile System without .git cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd - taskset 1 make check_reproducible - if: ${{ matrix.otp_latest }} test_windows: name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d6ece4dcda..fb9c043c14d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ You may also prefer to write using guards: * [Kernel] Track the type of tuples in patterns and inside `elem/2` * [Kernel] Perform validation of root AST nodes in `unquote` and `unquote_splicing` to catch bugs earlier * [Kernel] Add source, behaviour, and record information to Docs chunk metadata + * [Kernel] Support deterministic builds in tandem with Erlang by setting `ERL_COMPILER_OPTIONS=deterministic`. Keep in mind deterministic builds strip source and other compile time information, which may be relevant for programs * [List] Add `List.ends_with?/2` * [Macro] Improve `dbg` handling of `if/2`, `with/1` and of code blocks * [Macro] Add `Macro.struct_info!/2` to return struct information mirroring `mod.__info__(:struct)` @@ -156,6 +157,7 @@ You may also prefer to write using guards: #### Elixir + * [Code] Setting `:warnings_as_errors` is deprecated via `Code.put_compiler_option/2`. This must not affect developers, as the `:warnings_as_errors` option is managed by Mix tasks, and not directly used via the `Code` module * [Enumerable] Deprecate returning a two-arity function in `Enumerable.slice/1` * [List] `List.zip/1` is deprecated in favor of `Enum.zip/1` * [Module] Deprecate `Module.eval_quoted/3` in favor of `Code.eval_quoted/3` diff --git a/Makefile b/Makefile index 761d5f1c1dc..9efa58d4bd7 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ MAN_PREFIX ?= $(SHARE_PREFIX)/man CANONICAL := main/ ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ERLC := erlc -I lib/elixir/include -ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi +ERL_MAKE := erl -make ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript VERSION := $(strip $(shell cat VERSION)) diff --git a/README.md b/README.md index 93cdfd2b58d..9ec9a9631ca 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,9 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows). In case you want to use this Elixir version as your system version, you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable). -Additionally, you may choose to run the test suite with `make clean test`. +When updating the repository, you may want to run `make clean` before +recompiling. For deterministic builds, you should set the environment +variable `ERL_COMPILER_OPTIONS=deterministic`. ## Contributing diff --git a/lib/elixir/Emakefile b/lib/elixir/Emakefile index 84b5a49d23b..33cc5994ca5 100644 --- a/lib/elixir/Emakefile +++ b/lib/elixir/Emakefile @@ -9,8 +9,8 @@ warn_deprecated_function, warn_obsolete_guard, warn_exported_vars, - %% warn_missing_spec, - %% warn_untyped_record, + %% Enable this when we require Erlang/OTP 27+ + %% warnings_as_errors, debug_info, {outdir, "ebin/"} ]}. diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index f058b4203df..ecb2c857b2b 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -249,8 +249,8 @@ defmodule Code do :debug_info, :ignore_already_consolidated, :ignore_module_conflict, - :relative_paths, - :warnings_as_errors + :infer_signatures, + :relative_paths ] @list_compiler_options [:no_warn_undefined, :tracers, :parser_options] @@ -1562,8 +1562,8 @@ defmodule Code do ## Examples - Code.compiler_options(warnings_as_errors: true) - #=> %{warnings_as_errors: false} + Code.compiler_options(infer_signatures: false) + #=> %{infer_signatures: true} """ @spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term} @@ -1592,6 +1592,12 @@ defmodule Code do :elixir_config.get(key) end + # TODO: Remove me in Elixir v2.0 + def get_compiler_option(:warnings_as_errors) do + IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1") + :ok + end + @doc """ Returns a list with all available compiler options. @@ -1620,16 +1626,19 @@ defmodule Code do Defaults to `true`. * `:debug_info` - when `true`, retains debug information in the compiled - module. Defaults to `true`. - This enables static analysis tools as it allows developers to - partially reconstruct the original source code. Therefore, disabling + module. This option can also be overridden per module using the `@compile` + directive. Defaults to `true`. + + This enables tooling to partially reconstruct the original source code, + for instance, to perform static analysis of 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. - Additionally, `mix test` disables it via the `:test_elixirc_options` - project configuration option. - This option can also be overridden per module using the `@compile` directive. + + Other environments, such as `mix test`, automatically disables this + via the `:test_elixirc_options` project configuration, as there is + typically no need to store debug chunks for test files. * `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn when a protocol has already been consolidated and a new implementation is added. @@ -1638,13 +1647,19 @@ defmodule Code do * `:ignore_module_conflict` - when `true`, does not warn when a module has already been defined. Defaults to `false`. + * `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local + signature inference used when type checking remote calls to the compiled + module. Type checking will be executed regardless of this value of this option. + Defaults to `true`. + + `mix test` automatically disables this option via the `:test_elixirc_options` + project configuration, as there is typically no need to store infer signatures + for test files. + * `: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`. - * `:warnings_as_errors` - causes compilation to fail when warnings are - generated. Defaults to `false`. - * `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}` tuples that will not emit warnings that the module or function does not exist at compilation time. Pass atom `:all` to skip warning for all undefined @@ -1690,6 +1705,16 @@ defmodule Code do :ok end + # TODO: Remove me in Elixir v2.0 + def put_compiler_option(:warnings_as_errors, _value) do + IO.warn( + ":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <> + "pass it as option to Kernel.ParallelCompiler instead" + ) + + :ok + end + def put_compiler_option(:no_warn_undefined, value) do if value != :all and not is_list(value) do raise "compiler option :no_warn_undefined should be a list or the atom :all, " <> @@ -1719,7 +1744,7 @@ defmodule Code do :ok end - # TODO: Make this option have no effect on Elixir v2.0 + # TODO: Remove this option on Elixir v2.0 # TODO: Warn if mode is :warn on Elixir v1.19 def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do :elixir_config.put(:on_undefined_variable, value) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index b9b0fd1fcbd..8a38fba5e48 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -9,6 +9,7 @@ defmodule Kernel.CLI do compile: [], no_halt: false, compiler_options: [], + warnings_as_errors: false, errors: [], verbose_compile: false, profile: nil, @@ -315,8 +316,7 @@ defmodule Kernel.CLI do end defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do - compiler_options = [{:warnings_as_errors, true} | config.compiler_options] - parse_argv(t, %{config | compiler_options: compiler_options}) + parse_argv(t, %{config | warnings_as_errors: true}) end defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do @@ -499,15 +499,11 @@ defmodule Kernel.CLI do ] end - profile_opts = - if config.profile do - [profile: config.profile] - else - [] - end - output = IO.chardata_to_string(config.output) - opts = verbose_opts ++ profile_opts + + opts = + verbose_opts ++ + [profile: config.profile, warnings_as_errors: config.warnings_as_errors] case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do {:ok, _, _} -> :ok diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 684de1d43d1..ee6809eaeb0 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -258,7 +258,7 @@ defmodule Kernel.ParallelCompiler do {status, modules_or_errors, info} = try do outcome = spawn_workers(schedulers, checker, files, output, options) - {outcome, Code.get_compiler_option(:warnings_as_errors)} + {outcome, Keyword.get(options, :warnings_as_errors, false)} else {{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true} when r_warnings != [] or c_warnings != [] -> diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 312c697c66b..117cc4bee9a 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -854,7 +854,7 @@ defmodule Macro do @spec struct_info!(module(), Macro.Env.t()) :: [%{field: atom(), required: boolean(), default: term()}] def struct_info!(module, env) when is_atom(module) do - case :elixir_map.maybe_load_struct_info([line: env.line], module, [], env) do + case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do {:ok, info} -> info {:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc)) end diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 1ef6dcfc54f..ef24485d02c 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -191,7 +191,8 @@ defmodule Module.ParallelChecker do or if the function does not exist return `{:error, :function}`. """ @spec fetch_export(cache(), module(), atom(), arity()) :: - {:ok, mode(), kind(), binary() | nil} | {:error, :function | :module} + {:ok, mode(), binary() | nil, {:infer, [term()]} | :none} + | {:error, :function | :module} def fetch_export({server, ets}, module, fun, arity) do case :ets.lookup(ets, module) do [] -> @@ -203,7 +204,7 @@ defmodule Module.ParallelChecker do [{_key, mode}] -> case :ets.lookup(ets, {module, {fun, arity}}) do - [{_key, reason}] -> {:ok, mode, reason} + [{_key, reason, signature}] -> {:ok, mode, reason, signature} [] -> {:error, :function} end end @@ -369,13 +370,13 @@ defmodule Module.ParallelChecker do true -> {mode, exports} = info_exports(module) deprecated = info_deprecated(module) - cache_info(ets, module, exports, deprecated, mode) + cache_info(ets, module, exports, deprecated, %{}, mode) false -> # Or load exports from chunk with {^module, binary, _filename} <- object_code, {:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do - cache_info(ets, module, exports, %{}, :erlang) + cache_info(ets, module, exports, %{}, %{}, :erlang) else _ -> :ets.insert(ets, {module, false}) @@ -417,14 +418,13 @@ defmodule Module.ParallelChecker do behaviour_exports(map) ++ for({function, :def, _meta, _clauses} <- map.definitions, do: function) - deprecated = Map.new(map.deprecated) - cache_info(ets, map.module, exports, deprecated, :elixir) + cache_info(ets, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir) end - defp cache_info(ets, module, exports, deprecated, mode) do - Enum.each(exports, fn {fun, arity} -> - reason = Map.get(deprecated, {fun, arity}) - :ets.insert(ets, {{module, {fun, arity}}, reason}) + defp cache_info(ets, module, exports, deprecated, sigs, mode) do + Enum.each(exports, fn fa -> + reason = Map.get(deprecated, fa) + :ets.insert(ets, {{module, fa}, reason, Map.get(sigs, fa, :none)}) end) :ets.insert(ets, {module, mode}) @@ -432,10 +432,14 @@ defmodule Module.ParallelChecker do defp cache_chunk(ets, module, exports) do Enum.each(exports, fn {{fun, arity}, info} -> - :ets.insert(ets, {{module, {fun, arity}}, Map.get(info, :deprecated)}) + # TODO: Match on signature directly in Elixir v1.22+ + :ets.insert( + ets, + {{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)} + ) end) - :ets.insert(ets, {{module, {:__info__, 1}}, nil}) + :ets.insert(ets, {{module, {:__info__, 1}}, nil, :none}) :ets.insert(ets, {module, :elixir}) end diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index e6e4ff646f6..5257a784c0e 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -1,37 +1,69 @@ defmodule Module.Types do @moduledoc false - alias Module.Types.{Expr, Pattern} + alias Module.Types.{Descr, Expr, Pattern} + + # These functions are not inferred because they are added/managed by the compiler + @no_infer [__protocol__: 1, behaviour_info: 1] @doc false - def warnings(module, file, defs, no_warn_undefined, cache) do + def infer(module, file, defs, env) do context = context() - Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} -> - stack = - stack(:dynamic, with_file_meta(meta, file), module, function, no_warn_undefined, cache) + for {{fun, arity}, :def, _meta, clauses} <- defs, + {fun, arity} not in @no_infer, + into: %{} do + stack = stack(:infer, file, module, {fun, arity}, :all, env) + expected = List.duplicate(Descr.dynamic(), arity) + + pair_types = + Enum.reduce(clauses, [], fn {meta, args, guards, body}, inferred -> + try do + {args, context} = + Pattern.of_head(args, guards, expected, :default, meta, stack, context) + + {return, _context} = Expr.of_expr(body, stack, context) + add_inferred(inferred, args, return, []) + rescue + e -> internal_error!(e, __STACKTRACE__, :def, meta, module, fun, args, guards, body) + end + end) + + # TODO: Reuse context from patterns and guards + {{fun, arity}, {:infer, Enum.reverse(pair_types)}} + end + end - Enum.flat_map(clauses, fn {meta, args, guards, body} -> - try do - warnings_from_clause(meta, args, guards, body, stack, context) - rescue - e -> - def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]} + # We check for term equality of types as an optimization + # to reduce the amount of check we do at runtime. + defp add_inferred([{args, existing_return} | tail], args, return, acc), + do: Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail]) - error = - RuntimeError.exception(""" - found error while checking types for #{Exception.format_mfa(module, fun, arity)}: + defp add_inferred([head | tail], args, return, acc), + do: add_inferred(tail, args, return, [head | acc]) - #{Exception.format_banner(:error, e, __STACKTRACE__)}\ + defp add_inferred([], args, return, acc), + do: [{args, return} | Enum.reverse(acc)] - The exception happened while checking this code: + @doc false + def warnings(module, file, defs, no_warn_undefined, cache) do + context = context() - #{Macro.to_string(def_expr)} + Enum.flat_map(defs, fn {{fun, arity}, kind, meta, clauses} -> + file = with_file_meta(meta, file) + stack = stack(:dynamic, file, module, {fun, arity}, no_warn_undefined, cache) + expected = List.duplicate(Descr.dynamic(), arity) - Please report this bug at: https://github.com/elixir-lang/elixir/issues - """) + Enum.flat_map(clauses, fn {meta, args, guards, body} -> + try do + {_types, context} = + Pattern.of_head(args, guards, expected, :default, meta, stack, context) - reraise error, __STACKTRACE__ + {_type, context} = Expr.of_expr(body, stack, context) + context.warnings + rescue + e -> + internal_error!(e, __STACKTRACE__, kind, meta, module, fun, args, guards, body) end end) end) @@ -44,6 +76,25 @@ defmodule Module.Types do end end + defp internal_error!(e, stack, kind, meta, module, fun, args, guards, body) do + def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]} + + exception = + RuntimeError.exception(""" + found error while checking types for #{Exception.format_mfa(module, fun, length(args))}: + + #{Exception.format_banner(:error, e, stack)}\ + + The exception happened while checking this code: + + #{Macro.to_string(def_expr)} + + Please report this bug at: https://github.com/elixir-lang/elixir/issues + """) + + reraise exception, stack + end + defp guards_to_expr([], left) do left end @@ -52,14 +103,6 @@ defmodule Module.Types do guards_to_expr(guards, {:when, [], [left, guard]}) end - defp warnings_from_clause(meta, args, guards, body, stack, context) do - dynamic = Module.Types.Descr.dynamic() - expected = Enum.map(args, fn _ -> dynamic end) - {_types, context} = Pattern.of_head(args, guards, expected, :default, meta, stack, context) - {_type, context} = Expr.of_expr(body, stack, context) - context.warnings - end - @doc false def stack(mode, file, module, function, no_warn_undefined, cache) when mode in [:static, :dynamic, :infer] do @@ -72,9 +115,9 @@ defmodule Module.Types do module: module, # Current function function: function, - # List of calls to not warn on as undefined + # List of calls to not warn on as undefined or :all no_warn_undefined: no_warn_undefined, - # A list of cached modules received from the parallel compiler + # A tuple with cache information or a Macro.Env struct indicating no remote traversals cache: cache, # The mode controls what happens on function application when # there are gradual arguments. Non-gradual arguments always diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 6efc243ace7..74200c4cf8f 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1145,9 +1145,17 @@ defmodule Module.Types.Descr do defp dynamic_to_quoted(descr) do cond do - term_type?(descr) -> [{:dynamic, [], []}] - single = indivisible_bitmap(descr) -> [single] - true -> [{:dynamic, [], [to_quoted(descr)]}] + term_type?(descr) -> + [{:dynamic, [], []}] + + single = indivisible_bitmap(descr) -> + [single] + + true -> + case to_quoted(descr) do + {:none, _meta, []} = none -> [none] + descr -> [{:dynamic, [], [descr]}] + end end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index f3c78adbcec..51362ce0c4e 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -202,6 +202,15 @@ defmodule Module.Types.Helpers do |> Macro.to_string() end + defp erl_to_ex( + :erlang, + :error, + [expr, :none, [error_info: {:%{}, _, [module: Exception]}]], + meta + ) do + {:raise, meta, [expr]} + end + defp erl_to_ex(mod, fun, args, meta) do case :elixir_rewrite.erl_to_ex(mod, fun, args) do {Kernel, fun, args, _} -> {fun, meta, args} diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index b0fb198f44d..03a7c12ca72 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -207,13 +207,22 @@ defmodule Module.Types.Of do Returns `__info__(:struct)` information about a struct. """ def struct_info(struct, meta, stack, context) do - {_, context} = remote(struct, :__struct__, 0, meta, stack, context) + case stack.cache do + %Macro.Env{} = env -> + case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do + {:ok, info} -> {info, context} + {:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc)) + end + + _ -> + {_, context} = export(struct, :__struct__, 0, meta, stack, context) - info = - struct.__info__(:struct) || - raise "expected #{inspect(struct)} to return struct metadata, but got none" + info = + struct.__info__(:struct) || + raise "expected #{inspect(struct)} to return struct metadata, but got none" - {info, context} + {info, context} + end end @doc """ @@ -384,7 +393,13 @@ defmodule Module.Types.Of do struct: list(closed_map(default: term(), field: atom(), required: boolean())) |> union(atom([nil])) - ] ++ shared_info + ] ++ shared_info, + __protocol__: [ + module: atom(), + functions: fas, + consolidated?: boolean(), + impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())])) + ] } for {name, clauses} <- infos do @@ -803,6 +818,21 @@ defmodule Module.Types.Of do end end + defp apply_remote({:infer, clauses}, args_types, _stack) do + case for({expected, return} <- clauses, zip_not_disjoint?(args_types, expected), do: return) do + [] -> + domain = + clauses + |> Enum.map(fn {args, _} -> args end) + |> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end) + + {:error, domain, clauses} + + returns -> + {:ok, returns |> Enum.reduce(&union/2) |> dynamic()} + end + end + defp zip_compatible_or_only_gradual?([actual | actuals], [expected | expecteds]) do (only_gradual?(actual) or compatible?(actual, expected)) and zip_compatible_or_only_gradual?(actuals, expecteds) @@ -826,11 +856,15 @@ defmodule Module.Types.Of do {remote(:module_info, arity), context} end + defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do + {:none, context} + end + defp export(module, fun, arity, meta, stack, context) do case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do - {:ok, mode, reason} -> - {remote(fun, arity), - check_deprecated(mode, module, fun, arity, reason, meta, stack, context)} + {:ok, mode, reason, info} -> + info = if info == :none, do: remote(fun, arity), else: info + {info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)} {:error, type} -> context = @@ -1051,6 +1085,21 @@ defmodule Module.Types.Of do {{:., _, [mod, fun]}, _, args} = expr {mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args) + explanation = + if i = Enum.find_index(args_types, &empty?/1) do + """ + the #{integer_to_ordinal(i + 1)} argument is empty (often represented as none()), \ + most likely because it is the result of an expression that always fails, such as \ + a `raise` or a previous invalid call. This causes any function called with this \ + value to fail + """ + else + """ + but expected one of: + #{clauses_args_to_quoted_string(clauses, converter)} + """ + end + %{ details: %{typing_traces: traces}, message: @@ -1064,9 +1113,8 @@ defmodule Module.Types.Of do #{args_to_quoted_string(args_types, domain, converter) |> indent(4)} - but expected one of: - #{clauses_args_to_quoted_string(clauses, converter)} """, + explanation, format_traces(traces) ]) } @@ -1253,4 +1301,13 @@ defmodule Module.Types.Of do single_line -> binary_slice(single_line, 1..-2//1) end end + + defp integer_to_ordinal(i) do + case rem(i, 10) do + 1 when rem(i, 100) != 11 -> "#{i}st" + 2 when rem(i, 100) != 12 -> "#{i}nd" + 3 when rem(i, 100) != 13 -> "#{i}rd" + _ -> "#{i}th" + end + end end diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 3a81ecf69a4..68eeb57e62f 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -912,7 +912,7 @@ defmodule Protocol do @doc false @spec __protocol__(:module) :: __MODULE__ - @spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__)) + @spec __protocol__(:functions) :: [{atom(), non_neg_integer()}] @spec __protocol__(:consolidated?) :: boolean @spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]} Kernel.def(__protocol__(:module), do: __MODULE__) @@ -922,12 +922,6 @@ defmodule Protocol do end end - @doc false - def __functions_spec__([]), do: [] - - def __functions_spec__([head | tail]), - do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)] - @doc false def __impl__(protocol, opts, do_block, env) do opts = Keyword.merge(opts, do_block) diff --git a/lib/elixir/pages/references/gradual-set-theoretic-types.md b/lib/elixir/pages/references/gradual-set-theoretic-types.md index 5abf2f4e598..bfbe65473e1 100644 --- a/lib/elixir/pages/references/gradual-set-theoretic-types.md +++ b/lib/elixir/pages/references/gradual-set-theoretic-types.md @@ -8,19 +8,23 @@ Elixir is in the process of incorporating set-theoretic types into the compiler. * **set-theoretic** - the types are described, implemented, and composed using basic set operations: unions, intersections, and negation -The current milestone aims to infer types from patterns and guards and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391). +The current milestone aims to infer types from patterns and guards and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. User provided type signatures are planned for future releases. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391). ## Supported types -At the moment, Elixir developers interact with set-theoretic types through warnings found by the type system. All data types in the language are modelled: +At the moment, Elixir developers interact with set-theoretic types through warnings found by the type system. These warnings will represent tyoes using the following notation: * `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()` - these types are indivisible. This means both `1` and `13` get the same `integer()` type. * `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types. + * `tuple()` - it represents all tuples. Tuples may also be written using the curly brackets syntax, such as `{:ok, binary()}`. A `...` at the end of the tuple means the overall size of the tuple is unknown. For example, the following tuple has at least two elements: `{:ok, binary(), ...}`. + + * `list(type)` - it represents a list of `type`. More precisely, it can be written as `empty_list() or non_empty_list(type, empty_list())`. Improper lists, which are lists which do not end with an empty list, such as `[1, 2 | 3]`, can be written as `list(integer(), integer())`. + * `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key. - * `tuple()`, `list()`, and `function()` - represent their respective composite types + * `function()` - it represents anonymous functions (which may be closures) ## Set operations @@ -39,9 +43,9 @@ If you give it an integer, it negates it. If you give it a boolean, it negates i We can say this function has the type `(integer() -> integer())` because it is capable of receiving an integer and returning an integer. In this case, `(integer() -> integer())` is a set that represents all functions that can receive an integer and return an integer. Even though this function can receive other arguments and return other values, it is still part of the `(integer() -> integer())` set. -This function also has the type `(boolean() -> boolean())`, because it receives the booleans and returns booleans. Therefore, we can say the overall type of the function is `(integer() -> integer()) and (boolean() -> boolean())`. The intersection means the function belongs to both sets. +This function also has the type `(boolean() -> boolean())`, because it receives booleans and returns booleans. Therefore, we can say the overall type of the function is `(integer() -> integer()) and (boolean() -> boolean())`. The intersection means the function belongs to both sets. -At this point, some may ask, why not a union? As a real-world example, take a t-shirt with green and yellow stripes. We can say the t-shirt belongs to the set of "t-shirts with green color". We can also say the t-shirt belongs to the set of "t-shirts with yellow color". Let's see the difference between unions and intersections: +At this point, you may ask, why not a union? As a real-world example, take a t-shirt with green and yellow stripes. We can say the t-shirt belongs to the set of "t-shirts with green color". We can also say the t-shirt belongs to the set of "t-shirts with yellow color". Let's see the difference between unions and intersections: * `(t_shirts_with_green() or t_shirts_with_yellow())` - contains t-shirts with either green or yellow, such as green, green and red, green and yellow, yellow, yellow and red, etc. @@ -70,29 +74,33 @@ However, by intersecting a type with `dynamic()`, we make the type gradual and t Compared to other gradually typed languages, the `dynamic()` type in Elixir is quite powerful: it restricts our program to certain types, via intersections, while still emitting warnings once it is certain the code will fail. This makes `dynamic()` an excellent tool for typing existing Elixir code with meaningful warnings. -Once Elixir introduces typed function signatures (see "Roadmap"), typed Elixir programs will behave as a statically typed code, unless the `dynamic()` type is used. This brings us to one last remark about dynamic types in Elixir: dynamic types are always at the root. For example, when you write a tuple of type `{:ok, dynamic()}`, Elixir will rewrite it to `dynamic({:ok, term()})`. While this has the downside that you cannot make part of a tuple/map/list gradual, only the whole tuple/map/list, it comes with the upside that dynamic is always explicitly at the root, making it harder to accidentally sneak `dynamic()` in a statically typed program. +If the user provides their own types, and those types are not `dynamic()`, then Elixir's type system behaves as a statically typed one. This brings us to one last property of dynamic types in Elixir: dynamic types are always at the root. For example, when you write a tuple of type `{:ok, dynamic()}`, Elixir will rewrite it to `dynamic({:ok, term()})`. While this has the downside that you cannot make part of a tuple/map/list gradual, only the whole tuple/map/list, it comes with the upside that dynamic is always explicitly at the root, making it harder to accidentally sneak `dynamic()` in a statically typed program. ## Type inference -Type inference (or reconstruction) is the ability of a type system automatically deduce, either partially or fully, the type of an expression at compile time. Type inference may happen at different levels. For example, many programming languages can automatically infer the types of variables, also known "local type inference", but not all can infer type signatures (in other words, they cannot reconstruct the arguments types and return types of a function). +Type inference (or reconstruction) is the ability of a type system automatically deduce, either partially or fully, the type of an expression at compile time. Type inference may occur at different levels. For example, many programming languages can automatically infer the types of variables, also known "local type inference", but not all can infer type signatures. In other words, they may not reconstruct the arguments types and return types of a function. Inferring type signatures comes with a series of trade-offs: - * speed - type inference algorithms are more expensive than type checking algorithms + * Speed - type inference algorithms are often more computationally intensive than type checking algorithms. + + * Expressiveness - in any given type system, the constructs that support inference are always a subset of those that can be type-checked. Therefore, if a programming language is restricted to fully reconstructed types, it is less expressive than a solely type checked counterpart. + + * Incremental compilation - type inference complicates incremental compilation. If module A depends on module B, which depends on module C, a change to C may require the type signature in B to be reconstructed, which may then require A to be recomputed (and so on). This dependency chain may require large projects to explicitly add type signatures for stability and compilation efficiency. - * expressiveness - for any given type system, the constructs that support inference are always a subset of the constructs that can be type checked. Therefore, if a programming language restricts itself to be fully reconstructed, it is then less expressive than its type checked counterparts + * Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths. - * incremental compilation - typing inference complicates incremental compilation. If module A depends on module B, the type signatures from module A must be reconstructed whenever the inferred signatures from B changes, and so on +On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir’s type system provides the following type reconstruction capabilities: -In order to balance these trade-offs, Elixir's type system provides the following type reconstruction capabilities: + * Local type inference - the type system automatically infer the types of variables, at the place those variables are defined. - * Local type inference - the type system automatically infer the types of variables, at the time those variables are defined + * Type inference of patterns (and guards in future releases) - the argument types of a function are automatically inferred based on patterns and guards, which capture and narrow types based on common Elixir constructs. - * Type inference of patterns (and guards in future releases) - the argument types of a function is automatically inferred based on its patterns and guards + * Module-local inference of return types - the gradual return types of functions are computed considering all of the functions within the module itself. Any call to a function in another module is conservatively assumed to return `dynamic()`. - * Module-local inference of return types - the gradual return types of a function is computed considering all of the functions within the module itself (any call to another module is considered to return `dynamic()`) +The last two items offer gradual reconstruction of type signatures. Our goal is to provide an efficient type reconstruction algorithm that can detect definite bugs in dynamic codebases, even in the absence of explicit type annotations. The gradual system focuses on proving cases where all combinations of a type *will* fail, rather than issuing warnings for cases where some combinations *might* error. -The last two items offer gradual reconstruction of type signatures, where the inferred types are broadly dynamic. Our goal is to provide an efficient type reconstruction algorithm for dynamic programs, which can help the type system spot definite bugs, even in the absence of type signatures. Once Elixir introduces typed function signatures (see "Roadmap"), then no type inference of function signatures is perform, and the functions will be type checked according to the user provided signature. +Once Elixir introduces typed function signatures (see "Roadmap"), any function with an explicit type signature will be checked against the user-provided type, as in other statically typed languages, without performing type inference of the function signature. ## Roadmap diff --git a/lib/elixir/scripts/diff.exs b/lib/elixir/scripts/diff.exs index 04e19a9930f..53e688632e6 100644 --- a/lib/elixir/scripts/diff.exs +++ b/lib/elixir/scripts/diff.exs @@ -3,20 +3,31 @@ defmodule Diff do Utilities for comparing build artifacts. """ - @known_chunks ~w( - abstract_code - debug_info + @atom_chunks ~w( + atoms attributes compile_info + debug_info exports labeled_exports imports indexed_imports locals labeled_locals - atoms )a + @binary_chunks ~w( + Attr + AtU8 + CInf + Dbgi + Docs + ExCk + ExpT + ImpT + LocT + )c + @doc """ Compares the build artifacts of two build directories. """ @@ -81,24 +92,9 @@ defmodule Diff do end defp beam_diff(file1, content1, file2, content2) do - with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks), - {:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks), - true <- chunks1 != chunks2 do - for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do - tmp_file1 = - chunk1 - |> inspect(pretty: true, limit: :infinity) - |> write_tmp() - - tmp_file2 = - chunk2 - |> inspect(pretty: true, limit: :infinity) - |> write_tmp() - - file_diff(tmp_file1, tmp_file2) - end - else - _ -> + chunk_diff(content1, content2, @atom_chunks, &inspect(&1, pretty: true, limit: :infinity)) || + chunk_diff(content1, content2, @binary_chunks, &(&1 |> write_tmp() |> xxd_dump())) || + ( tmp_file1 = file1 |> xxd_dump() @@ -110,6 +106,30 @@ defmodule Diff do |> write_tmp() file_diff(tmp_file1, tmp_file2) + ) + end + + defp chunk_diff(content1, content2, names, formatter) do + with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, names), + {:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, names), + true <- chunks1 != chunks2 do + if length(chunks1) != length(chunks2) do + """ + Different chunks: + * #{inspect(chunks1)} + * #{inspect(chunks2)} + """ + else + for {{name1, chunk1}, {name2, chunk2}} <- Enum.zip(chunks1, chunks2), + true = name1 == name2, + chunk1 != chunk2 do + tmp_file1 = chunk1 |> formatter.() |> write_tmp() + tmp_file2 = chunk2 |> formatter.() |> write_tmp() + [name1, ?\n, file_diff(tmp_file1, tmp_file2)] + end + end + else + _ -> nil end end @@ -137,18 +157,16 @@ defmodule Diff do end defp write_tmp(content) do - filename = generate_tmp_filename() + filename = "tmp-#{System.unique_integer([:positive])}" File.mkdir_p!("tmp") File.write!(Path.join("tmp", filename), content) Path.join("tmp", filename) end +end - defp generate_tmp_filename do - sec = :os.system_time(:second) - rand = :rand.uniform(999_999_999) - scheduler_id = :erlang.system_info(:scheduler_id) - "tmp-#{sec}-#{rand}-#{scheduler_id}" - end +if :deterministic not in :compile.env_compiler_options() do + IO.puts("Cannot validate if reproducible without setting ERL_COMPILER_OPTIONS=deterministic") + System.halt(1) end case System.argv() do diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 70ad268fa65..c6042be8094 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -88,12 +88,12 @@ start(_Type, _Args) -> %% Compiler options {docs, true}, + {infer_signatures, true}, {ignore_already_consolidated, false}, {ignore_module_conflict, false}, {on_undefined_variable, raise}, {parser_options, [{columns, true}]}, {debug_info, true}, - {warnings_as_errors, false}, {relative_paths, true}, {no_warn_undefined, []}, {tracers, []} diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index a9267d08e26..5a12007ade6 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -143,11 +143,12 @@ bootstrap() -> {ok, _} = application:ensure_all_started(elixir), elixir_config:static(#{bootstrap => true}), elixir_config:put(docs, false), - elixir_config:put(relative_paths, false), elixir_config:put(ignore_module_conflict, true), + elixir_config:put(infer_signatures, false), elixir_config:put(on_undefined_variable, raise), - elixir_config:put(tracers, []), elixir_config:put(parser_options, []), + elixir_config:put(relative_paths, false), + elixir_config:put(tracers, []), {Init, Main} = bootstrap_files(), {ok, Cwd} = file:get_cwd(), Lib = filename:join(Cwd, "lib/elixir/lib"), diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 1768f842735..58800a2a966 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -124,7 +124,13 @@ consolidate(Map, TypeSpecs, Chunks) -> %% Dynamic compilation hook, used in regular compiler -compile(#{module := Module, anno := Anno} = Map) -> +compile(#{module := Module, anno := Anno} = BaseMap) -> + Map = + case elixir_erl_compiler:env_compiler_options() of + [] -> BaseMap; + EnvOptions -> BaseMap#{compile_opts := ?key(BaseMap, compile_opts) ++ EnvOptions} + end, + {Set, Bag} = elixir_module:data_tables(Module), TranslatedTypespecs = @@ -137,8 +143,14 @@ compile(#{module := Module, anno := Anno} = Map) -> {Prefix, Forms, Def, Defmacro, Macros} = dynamic_form(Map), {Types, Callbacks, TypeSpecs} = typespecs_form(Map, TranslatedTypespecs, Macros), - DocsChunk = docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks), - CheckerChunk = checker_chunk(Def, Map), + ChunkOpts = + case lists:member(deterministic, ?key(Map, compile_opts)) of + true -> [deterministic]; + false -> [] + end, + + DocsChunk = docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts), + CheckerChunk = checker_chunk(Map, Def, ChunkOpts), load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk). dynamic_form(#{module := Module, relative_file := RelativeFile, @@ -482,7 +494,7 @@ attributes_form(Line, Attributes, Forms) -> load_form(#{file := File, compile_opts := Opts} = Map, Prefix, Forms, Specs, Chunks) -> CompileOpts = extra_chunks_opts(Chunks, debug_opts(Map, Specs, Opts)), - {_, Binary} = elixir_erl_compiler:forms(Prefix ++ Specs ++ Forms, File, CompileOpts), + {_, Binary} = elixir_erl_compiler:noenv_forms(Prefix ++ Specs ++ Forms, File, CompileOpts), Binary. debug_opts(Map, Specs, Opts) -> @@ -501,7 +513,7 @@ take_debug_opts(Opts) -> extra_chunks_opts([], Opts) -> Opts; extra_chunks_opts(Chunks, Opts) -> [{extra_chunks, Chunks} | Opts]. -docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks) -> +docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts) -> #{file := File, attributes := Attributes} = Map, case elixir_config:get(docs) of @@ -526,7 +538,7 @@ docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks) -> ModuleDoc, ModuleMeta, FunctionDocs ++ MacroDocs ++ CallbackDocs ++ TypeDocs - }, [deterministic, compressed]), + }, [compressed | ChunkOpts]), [{<<"Docs">>, DocsChunkData}]; @@ -612,14 +624,16 @@ signature_to_binary(_, Name, Signature) -> Doc = 'Elixir.Inspect.Algebra':format('Elixir.Code':quoted_to_algebra(Quoted), infinity), 'Elixir.IO':iodata_to_binary(Doc). -checker_chunk(Def, #{deprecated := Deprecated, defines_behaviour := DefinesBehaviour}) -> +checker_chunk(Map, Def, ChunkOpts) -> + #{deprecated := Deprecated, defines_behaviour := DefinesBehaviour, signatures := Signatures} = Map, DeprecatedMap = maps:from_list(Deprecated), Exports = [begin + Signature = maps:get(FA, Signatures, none), Info = case DeprecatedMap of - #{FA := Reason} -> #{deprecated => Reason}; - #{} -> #{} + #{FA := Reason} -> #{deprecated => Reason, sig => Signature}; + #{} -> #{sig => Signature} end, {FA, Info} end || {FA, _Meta} <- prepend_behaviour_info(DefinesBehaviour, Def)], @@ -628,7 +642,7 @@ checker_chunk(Def, #{deprecated := Deprecated, defines_behaviour := DefinesBehav exports => Exports }, - [{<<"ExCk">>, term_to_binary({elixir_checker_v1, Contents}, [deterministic])}]. + [{<<"ExCk">>, term_to_binary({elixir_checker_v1, Contents}, ChunkOpts)}]. prepend_behaviour_info(true, Def) -> [{{behaviour_info, 1}, []} | Def]; prepend_behaviour_info(false, Def) -> Def. diff --git a/lib/elixir/src/elixir_erl_compiler.erl b/lib/elixir/src/elixir_erl_compiler.erl index ea2eb7c8245..90848fb3b48 100644 --- a/lib/elixir/src/elixir_erl_compiler.erl +++ b/lib/elixir/src/elixir_erl_compiler.erl @@ -1,5 +1,5 @@ -module(elixir_erl_compiler). --export([spawn/1, forms/3, noenv_forms/3, erl_to_core/2]). +-export([spawn/1, noenv_forms/3, erl_to_core/2, env_compiler_options/0]). -include("elixir.hrl"). spawn(Fun) -> @@ -41,11 +41,16 @@ copy_diagnostics({Head, _}) -> {Tail, Log} -> put(elixir_code_diagnostics, {Head ++ Tail, Log}) end. -forms(Forms, File, Opts) -> - compile(Forms, File, Opts ++ compile:env_compiler_options()). +env_compiler_options() -> + case persistent_term:get(?MODULE, undefined) of + undefined -> + Options = compile:env_compiler_options() -- [warnings_as_errors], + persistent_term:put(?MODULE, Options), + Options; -noenv_forms(Forms, File, Opts) -> - compile(Forms, File, Opts). + Options -> + Options + end. erl_to_core(Forms, Opts) -> %% TODO: Remove parse transform handling on Elixir v2.0 @@ -59,7 +64,7 @@ erl_to_core(Forms, Opts) -> end end. -compile(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) -> +noenv_forms(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) -> Source = elixir_utils:characters_to_list(File), case erl_to_core(Forms, Opts) of @@ -73,19 +78,18 @@ compile(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) - format_warnings(Opts, Warnings), {Module, Binary}; - {ok, Module, _Binary, _Warnings} -> - Message = io_lib:format( - "could not compile module ~ts. We expected the compiler to return a .beam binary but " - "got something else. This usually happens because ERL_COMPILER_OPTIONS or @compile " - "was set to change the compilation outcome in a way that is incompatible with Elixir", - [elixir_aliases:inspect(Module)] - ), + {ok, Module, _, _} -> + incompatible_options("could not compile module ~ts", [elixir_aliases:inspect(Module)], File); - elixir_errors:compile_error([], File, Message); + {ok, Module, _} -> + incompatible_options("could not compile module ~ts", [elixir_aliases:inspect(Module)], File); {error, Errors, Warnings} -> format_warnings(Opts, Warnings), - format_errors(Errors) + format_errors(Errors); + + _ -> + incompatible_options("could not compile module", [], File) end; {error, CoreErrors, CoreWarnings} -> @@ -93,6 +97,16 @@ compile(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) - format_errors(CoreErrors) end. +incompatible_options(Prefix, Args, File) -> + Message = io_lib:format( + Prefix ++ ". We expected the compiler to return a .beam binary but " + "got something else. This usually happens because ERL_COMPILER_OPTIONS or @compile " + "was set to change the compilation outcome in a way that is incompatible with Elixir", + Args + ), + + elixir_errors:compile_error([], File, Message). + format_errors([]) -> exit({nocompile, "compilation failed but no error was raised"}); format_errors(Errors) -> diff --git a/lib/elixir/src/elixir_map.erl b/lib/elixir/src/elixir_map.erl index 4b81002f5cc..d2b0fdac573 100644 --- a/lib/elixir/src/elixir_map.erl +++ b/lib/elixir/src/elixir_map.erl @@ -1,5 +1,5 @@ -module(elixir_map). --export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/4]). +-export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/5]). -import(elixir_errors, [function_error/4, file_error/4, file_warn/4]). -include("elixir.hrl"). @@ -128,12 +128,12 @@ validate_struct(_, _) -> false. load_struct_info(Meta, Name, Assocs, E) -> assert_struct_assocs(Meta, Assocs, E), - case maybe_load_struct_info(Meta, Name, Assocs, E) of + case maybe_load_struct_info(Meta, Name, Assocs, true, E) of {ok, Info} -> Info; {error, Desc} -> file_error(Meta, E, ?MODULE, Desc) end. -maybe_load_struct_info(Meta, Name, Assocs, E) -> +maybe_load_struct_info(Meta, Name, Assocs, Trace, E) -> try case is_open(Name, E) andalso lookup_struct_info_from_data_tables(Name) of %% If I am accessing myself and there is no attribute, @@ -152,7 +152,7 @@ maybe_load_struct_info(Meta, Name, Assocs, E) -> function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key}), Key end || {Key, _} <- Assocs], - elixir_env:trace({struct_expansion, Meta, Name, Keys}, E), + Trace andalso elixir_env:trace({struct_expansion, Meta, Name, Keys}, E), {ok, Info} catch error:undef -> {error, detail_undef(Name, E)} diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index f21be266296..95d737d5d59 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -167,6 +167,17 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> [elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) || {VerifyMod, VerifyFun} <- AfterVerify], + %% Compute signatures only if the module is valid. + case ets:member(DataSet, {elixir, taint}) of + true -> elixir_errors:compile_error(E); + false -> ok + end, + + Signatures = case elixir_config:get(infer_signatures) of + true -> 'Elixir.Module.Types':infer(Module, File, AllDefinitions, CallbackE); + false -> #{} + end, + ModuleMap = #{ struct => get_struct(DataSet), module => Module, @@ -180,14 +191,10 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> compile_opts => CompileOpts, deprecated => get_deprecated(DataBag), defines_behaviour => defines_behaviour(DataBag), - impls => Impls + impls => Impls, + signatures => Signatures }, - case ets:member(DataSet, {elixir, taint}) of - true -> elixir_errors:compile_error(E); - false -> ok - end, - Binary = elixir_erl:compile(ModuleMap), Autoload = proplists:get_value(autoload, CompileOpts, true), spawn_parallel_checker(CheckerInfo, Module, ModuleMap), diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index ec927bd17ae..de766611758 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -439,15 +439,11 @@ defmodule CodeTest do assert meta[:end_column] == 3 end + @tag :requires_source test "compile source" do assert __MODULE__.__info__(:compile)[:source] == String.to_charlist(__ENV__.file) end - test "compile info returned with source accessible through keyword module" do - compile = __MODULE__.__info__(:compile) - assert Keyword.get(compile, :source) != nil - end - describe "compile_string/1" do test "compiles the given string" do assert [{CompileStringSample, _}] = diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index 6dad07664d0..95b5ae32b9e 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -422,8 +422,6 @@ defmodule Kernel.ParallelCompilerTest do end test "supports warnings as errors" do - warnings_as_errors = Code.get_compiler_option(:warnings_as_errors) - [fixture] = write_tmp( "warnings_as_errors", @@ -438,12 +436,12 @@ defmodule Kernel.ParallelCompilerTest do output = tmp_path("not_to_be_used") try do - Code.compiler_options(warnings_as_errors: true) - msg = capture_io(:stderr, fn -> assert {:error, [error], []} = - Kernel.ParallelCompiler.compile_to_path([fixture], output) + Kernel.ParallelCompiler.compile_to_path([fixture], output, + warnings_as_errors: true + ) assert {^fixture, {3, 7}, "this clause " <> _} = error end) @@ -451,7 +449,6 @@ defmodule Kernel.ParallelCompilerTest do assert msg =~ "Compilation failed due to warnings while using the --warnings-as-errors option\n" after - Code.compiler_options(warnings_as_errors: warnings_as_errors) purge([WarningsSample]) end @@ -592,8 +589,6 @@ defmodule Kernel.ParallelCompilerTest do end test "supports warnings as errors" do - warnings_as_errors = Code.get_compiler_option(:warnings_as_errors) - [fixture] = write_tmp( "warnings_as_errors", @@ -606,11 +601,10 @@ defmodule Kernel.ParallelCompilerTest do ) try do - Code.compiler_options(warnings_as_errors: true) - msg = capture_io(:stderr, fn -> - assert {:error, [error], []} = Kernel.ParallelCompiler.require([fixture]) + assert {:error, [error], []} = + Kernel.ParallelCompiler.require([fixture], warnings_as_errors: true) assert {^fixture, {3, 7}, "this clause " <> _} = error end) @@ -618,7 +612,6 @@ defmodule Kernel.ParallelCompilerTest do assert msg =~ "Compilation failed due to warnings while using the --warnings-as-errors option\n" after - Code.compiler_options(warnings_as_errors: warnings_as_errors) purge([WarningsSample]) end end diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index db23899758d..c97adc21519 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -992,10 +992,6 @@ defmodule KernelTest do assert get_in(map, ["fruits", by_index(0)]) == "banana" assert get_in(map, ["fruits", by_index(3)]) == nil assert get_in(map, ["unknown", by_index(3)]) == nil - - assert_raise FunctionClauseError, fn -> - get_in(users, []) - end end test "put_in/3" do @@ -1003,10 +999,6 @@ defmodule KernelTest do assert put_in(users, ["john", :age], 28) == %{"john" => %{age: 28}, "meg" => %{age: 23}} - assert_raise FunctionClauseError, fn -> - put_in(users, [], %{}) - end - assert_raise ArgumentError, "could not put/update key \"john\" on a nil value", fn -> put_in(nil, ["john", :age], 28) end @@ -1039,10 +1031,6 @@ defmodule KernelTest do assert update_in(users, ["john", :age], &(&1 + 1)) == %{"john" => %{age: 28}, "meg" => %{age: 23}} - assert_raise FunctionClauseError, fn -> - update_in(users, [], fn _ -> %{} end) - end - assert_raise ArgumentError, "could not put/update key \"john\" on a nil value", fn -> update_in(nil, ["john", :age], fn _ -> %{} end) end @@ -1091,10 +1079,6 @@ defmodule KernelTest do assert get_and_update_in(map, ["unknown", by_index(3)], &{&1, []}) == {:oops, %{"fruits" => ["banana", "apple", "orange"], "unknown" => []}} - - assert_raise FunctionClauseError, fn -> - update_in(users, [], fn _ -> %{} end) - end end test "get_and_update_in/2" do @@ -1124,19 +1108,14 @@ defmodule KernelTest do test "pop_in/2" do users = %{"john" => %{age: 27}, "meg" => %{age: 23}} - assert pop_in(users, ["john", :age]) == {27, %{"john" => %{}, "meg" => %{age: 23}}} - - assert pop_in(users, ["bob", :age]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}} + assert pop_in(users, ["john", :age]) == + {27, %{"john" => %{}, "meg" => %{age: 23}}} - assert pop_in([], [:foo, :bar]) == {nil, []} + assert pop_in(users, ["bob", :age]) == + {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}} - assert_raise FunctionClauseError, fn -> - pop_in(users, []) - end - - assert_raise FunctionClauseError, "no function clause matching in Kernel.pop_in/2", fn -> - pop_in(users, :not_a_list) - end + assert pop_in([], [:foo, :bar]) == + {nil, []} end test "pop_in/2 with paths" do diff --git a/lib/elixir/test/elixir/list_test.exs b/lib/elixir/test/elixir/list_test.exs index 12676c126f1..a8c4984560e 100644 --- a/lib/elixir/test/elixir/list_test.exs +++ b/lib/elixir/test/elixir/list_test.exs @@ -276,12 +276,6 @@ defmodule ListTest do assert_raise FunctionClauseError, message, fn -> List.starts_with?([1 | 2], [1 | 2]) end - - message = "no function clause matching in List.starts_with?/2" - - assert_raise FunctionClauseError, message, fn -> - List.starts_with?([1, 2], 1) - end end end @@ -313,10 +307,6 @@ defmodule ListTest do assert_raise ArgumentError, ~r/not a list/, fn -> List.ends_with?([1 | 2], [1 | 2]) end - - assert_raise ArgumentError, ~r/not a list/, fn -> - List.ends_with?([1, 2], 1) - end end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 2a3f8e92776..3e74d5d2aae 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1163,6 +1163,7 @@ defmodule Module.Types.DescrTest do test "none" do assert none() |> to_quoted_string() == "none()" + assert dynamic(none()) |> to_quoted_string() == "none()" end test "negation" do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 0b12ca701af..7e5499cde56 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -165,6 +165,24 @@ defmodule Module.Types.ExprTest do {union(dynamic(), binary()), "GenServer.to_string/1 is undefined or private"} end + test "calling a function with none()" do + assert typeerror!(Integer.to_string(raise "oops")) |> strip_ansi() == + ~l""" + incompatible types given to Integer.to_string/1: + + Integer.to_string(raise RuntimeError.exception("oops")) + + given types: + + none() + + the 1st argument is empty (often represented as none()), \ + most likely because it is the result of an expression that \ + always fails, such as a `raise` or a previous invalid call. \ + This causes any function called with this value to fail + """ + end + test "calling a nullary function on non atoms" do assert typeerror!([<>], x.foo_bar()) == ~l""" diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs new file mode 100644 index 00000000000..ce7c51f2dc8 --- /dev/null +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -0,0 +1,64 @@ +Code.require_file("type_helper.exs", __DIR__) + +defmodule Module.Types.InferTest do + use ExUnit.Case, async: true + + import Module.Types.Descr + + defmacro infer(config, do: block) do + quote do + defmodule unquote(config).test do + unquote(block) + end + |> runtime_infer() + end + end + + defp runtime_infer({:module, module, binary, _result}) do + {:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info]) + {:debug_info_v1, backend, data} = chunk + {:ok, %{signatures: signatures}} = backend.debug_info(:elixir_v1, module, data, []) + signatures + end + + test "infer types from patterns", config do + types = + infer config do + def fun1(%y{}, %x{}, x = y, x = Point), do: :ok + def fun2(%x{}, %y{}, x = y, x = Point), do: :ok + def fun3(%y{}, %x{}, x = y, y = Point), do: :ok + def fun4(%x{}, %y{}, x = y, y = Point), do: :ok + end + + args = [ + dynamic(open_map(__struct__: atom([Point]))), + dynamic(open_map(__struct__: atom([Point]))), + dynamic(atom([Point])), + dynamic(atom([Point])) + ] + + assert types[{:fun1, 4}] == {:infer, [{args, atom([:ok])}]} + assert types[{:fun2, 4}] == {:infer, [{args, atom([:ok])}]} + assert types[{:fun3, 4}] == {:infer, [{args, atom([:ok])}]} + assert types[{:fun4, 4}] == {:infer, [{args, atom([:ok])}]} + end + + test "merges patterns", config do + types = + infer config do + def fun(:ok), do: :one + def fun("two"), do: :two + def fun("three"), do: :three + def fun("four"), do: :four + def fun(:error), do: :five + end + + assert types[{:fun, 1}] == + {:infer, + [ + {[dynamic(atom([:ok]))], atom([:one])}, + {[dynamic(binary())], atom([:two, :three, :four])}, + {[dynamic(atom([:error]))], atom([:five])} + ]} + end +end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index b088ba5b9ca..4c6e1e79a6c 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -38,19 +38,69 @@ defmodule Module.Types.IntegrationTest do modules = compile(files) - assert read_chunk(modules[A]).exports == [ + assert [ {{:c, 0}, %{}}, - {{:e, 0}, %{deprecated: "oops"}} - ] + {{:e, 0}, %{deprecated: "oops", sig: {:infer, _}}} + ] = read_chunk(modules[A]).exports assert read_chunk(modules[B]).exports == [ - {{:behaviour_info, 1}, %{}} + {{:behaviour_info, 1}, %{sig: :none}} ] assert read_chunk(modules[C]).exports == [ - {{:behaviour_info, 1}, %{}} + {{:behaviour_info, 1}, %{sig: :none}} ] end + + test "type checks signatures" do + files = %{ + "a.ex" => """ + defmodule A do + def fun(:ok), do: :doki + def fun(:error), do: :bad + end + """, + "b.ex" => """ + defmodule B do + def badarg do + A.fun(:unknown) + end + + def badmatch do + :doki = A.fun(:error) + end + end + """ + } + + warnings = [ + """ + warning: incompatible types given to A.fun/1: + + A.fun(:unknown) + """, + """ + but expected one of: + + #1 + dynamic(:ok) + + #2 + dynamic(:error) + """, + """ + warning: the following pattern will never match: + + :doki = A.fun(:error) + + because the right-hand side has type: + + dynamic(:bad) + """ + ] + + assert_warnings(files, warnings) + end end describe "undefined warnings" do diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index d5fd880a0b2..490c725a4b8 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -310,20 +310,4 @@ defmodule Module.Types.PatternTest do """ end end - - describe "inference" do - test "refines information across patterns" do - result = [ - dynamic(open_map(__struct__: atom([Point]))), - dynamic(open_map(__struct__: atom([Point]))), - dynamic(atom([Point])), - dynamic(atom([Point])) - ] - - assert typeinfer!([%y{}, %x{}, x = y, x = Point]) == result - assert typeinfer!([%x{}, %y{}, x = y, x = Point]) == result - assert typeinfer!([%y{}, %x{}, x = y, y = Point]) == result - assert typeinfer!([%x{}, %y{}, x = y, y = Point]) == result - end - end end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 0f7cd95e8ad..19c29b82d30 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -8,16 +8,6 @@ defmodule TypeHelper do alias Module.Types alias Module.Types.{Pattern, Expr, Descr} - @doc """ - Main helper for inferring the given pattern + guards. - """ - defmacro typeinfer!(patterns \\ [], guards \\ true) do - quote do - unquote(typeinfer(patterns, guards, __CALLER__)) - |> TypeHelper.__typecheck__!() - end - end - @doc """ Main helper for checking the given AST type checks without warnings. """ @@ -111,22 +101,6 @@ defmodule TypeHelper do def __typewarn__!({_type, %{warnings: warnings, failed: true}}), do: raise("type checking errored with warnings: #{inspect(warnings)}") - defp typeinfer(patterns, guards, env) do - {patterns, guards, :ok} = expand_and_unpack(patterns, guards, :ok, env) - - quote do - TypeHelper.__typeinfer__( - unquote(Macro.escape(patterns)), - unquote(Macro.escape(guards)) - ) - end - end - - def __typeinfer__(patterns, guards) do - expected = Enum.map(patterns, fn _ -> Module.Types.Descr.dynamic() end) - Pattern.of_head(patterns, guards, expected, :default, [], new_stack(:infer), new_context()) - end - defp typecheck(mode, patterns, guards, body, env) do {patterns, guards, body} = expand_and_unpack(patterns, guards, body, env) @@ -163,7 +137,7 @@ defmodule TypeHelper do end defp new_stack(mode) do - cache = Module.ParallelChecker.test_cache() + cache = if mode == :infer, do: :none, else: Module.ParallelChecker.test_cache() Types.stack(mode, "types_test.ex", TypesTest, {:test, 0}, [], cache) end diff --git a/lib/elixir/test/elixir/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs index 8a20fe86037..43ce2380c54 100644 --- a/lib/elixir/test/elixir/protocol/consolidation_test.exs +++ b/lib/elixir/test/elixir/protocol/consolidation_test.exs @@ -131,9 +131,10 @@ defmodule Protocol.ConsolidationTest do {:ok, {Sample, [{~c"ExCk", check_bin}]}} = :beam_lib.chunks(@sample_binary, [~c"ExCk"]) assert {:elixir_checker_v1, contents} = :erlang.binary_to_term(check_bin) - assert {{:ok, 1}, %{deprecated: "Reason"}} in contents.exports + assert %{{:ok, 1} => %{deprecated: "Reason", sig: _}} = Map.new(contents.exports) end + @tag :requires_source test "consolidation keeps source" do assert Sample.__info__(:compile)[:source] end diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index ccd014214eb..72184400e10 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -305,6 +305,7 @@ defmodule ProtocolTest do assert Derivable.ok(struct) == {:ok, struct, struct(InlineStruct), :oops} end + @tag :requires_source test "derived implementation keeps local file/line info" do assert ProtocolTest.WithAny.ProtocolTest.ImplStruct.__info__(:compile)[:source] == String.to_charlist(__ENV__.file) diff --git a/lib/elixir/test/elixir/test_helper.exs b/lib/elixir/test/elixir/test_helper.exs index a5b9e6a285b..1d578b1bedd 100644 --- a/lib/elixir/test/elixir/test_helper.exs +++ b/lib/elixir/test/elixir/test_helper.exs @@ -109,9 +109,16 @@ distributed_exclude = [distributed: true] end +source_exclude = + if :deterministic in :compile.env_compiler_options() do + [:requires_source] + else + [] + end + ExUnit.start( trace: !!System.get_env("TRACE"), assert_receive_timeout: assert_timeout, - exclude: epmd_exclude ++ os_exclude ++ line_exclude ++ distributed_exclude, + exclude: epmd_exclude ++ os_exclude ++ line_exclude ++ distributed_exclude ++ source_exclude, include: line_include ) diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index 3ac413dc8a1..38b0be797a3 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -532,8 +532,12 @@ defmodule ExUnit.Assertions do end end - failure_message = - failure_message || + on_timeout = + if failure_message do + quote do + flunk(unquote(failure_message)) + end + else quote do ExUnit.Assertions.__timeout__( unquote(Macro.escape(expanded_pattern)), @@ -543,6 +547,7 @@ defmodule ExUnit.Assertions do timeout ) end + end quote do timeout = ExUnit.Assertions.__timeout__(unquote(timeout), :assert_receive_timeout) @@ -551,7 +556,7 @@ defmodule ExUnit.Assertions do receive do unquote(pattern) -> {received, unquote(mark_as_generated(vars))} after - timeout -> flunk(unquote(failure_message)) + timeout -> unquote(on_timeout) end received diff --git a/lib/ex_unit/lib/ex_unit/doc_test.ex b/lib/ex_unit/lib/ex_unit/doc_test.ex index 6ecc785dfea..dd354b2e4f8 100644 --- a/lib/ex_unit/lib/ex_unit/doc_test.ex +++ b/lib/ex_unit/lib/ex_unit/doc_test.ex @@ -165,9 +165,15 @@ defmodule ExUnit.DocTest do module = Keyword.fetch!(opts, :module) message = Keyword.fetch!(opts, :message) - file = module.module_info(:compile)[:source] |> Path.relative_to_cwd() - info = Exception.format_file_line(file, opts[:line]) - %__MODULE__{message: info <> " " <> message} + message = + if source = module.module_info(:compile)[:source] do + file_line = Exception.format_file_line(Path.relative_to_cwd(source), opts[:line]) + file_line <> " " <> message + else + message + end + + %__MODULE__{message: message} end end @@ -275,24 +281,31 @@ defmodule ExUnit.DocTest do @doc false def __file__(module) do - source = - module.module_info(:compile)[:source] || - raise "#{inspect(module)} does not have compile-time source information" + info = + if source = module.module_info(:compile)[:source] do + Path.relative_to_cwd(source) + else + inspect(module) + end - "(for doctest at) " <> Path.relative_to_cwd(source) + "(for doctest at) " <> info end @doc false def __doctests__(module, opts) do tags = [doctest: module] ++ Keyword.get(opts, :tags, []) import = Keyword.get(opts, :import, false) - file = module.module_info(:compile)[:source] |> Path.relative_to_cwd() + + maybe_source = + if source = module.module_info(:compile)[:source] do + Path.relative_to_cwd(source) + end extract(module) |> filter_by_opts(module, opts) |> Enum.sort_by(& &1.line) |> Enum.with_index(fn test, index -> - compile_test(test, module, import, index + 1, file, tags) + compile_test(test, module, import, index + 1, maybe_source, tags) end) end @@ -338,8 +351,8 @@ defmodule ExUnit.DocTest do ## Compilation of extracted tests - defp compile_test(test, module, do_import, n, file, tags) do - {test_name(test, module, n), test_content(test, module, do_import, file), + defp compile_test(test, module, do_import, n, maybe_source, tags) do + {test_name(test, module, n), test_content(test, module, do_import, maybe_source), test_tags(test, tags)} end @@ -351,7 +364,7 @@ defmodule ExUnit.DocTest do "#{inspect(m)}.#{f}/#{a} (#{n})" end - defp test_content(%{exprs: exprs, line: line}, module, do_import, file) do + defp test_content(%{exprs: exprs, line: line}, module, do_import, maybe_source) do if multiple_exceptions?(exprs) do raise Error, line: line, @@ -361,7 +374,7 @@ defmodule ExUnit.DocTest do "please separate your iex> prompts by multiple newlines to start new examples" end - tests = Enum.map(exprs, fn expr -> test_case_content(expr, module, file) end) + tests = Enum.map(exprs, fn expr -> test_case_content(expr, module, maybe_source) end) {:__block__, [], test_import(module, do_import) ++ tests} end @@ -376,15 +389,18 @@ defmodule ExUnit.DocTest do end) > 1 end - defp test_case_content(%{expected: :test} = data, module, file) do + defp test_case_content(%{expected: :test} = data, module, maybe_source) do %{expr: expr, expr_line: expr_line, doctest: doctest} = data - string_to_quoted(module, file, expr_line, expr, doctest) |> insert_assertions() + string_to_quoted(module, maybe_source, expr_line, expr, doctest) |> insert_assertions() end - defp test_case_content(%{expected: {:test, expected}} = data, module, file) do + defp test_case_content(%{expected: {:test, expected}} = data, module, maybe_source) do %{expr: expr, expr_line: expr_line, expected_line: expected_line, doctest: doctest} = data - expr_ast = string_to_quoted(module, file, expr_line, expr, doctest) |> insert_assertions() - expected_ast = string_to_quoted(module, file, expected_line, expected, doctest) + + expr_ast = + string_to_quoted(module, maybe_source, expr_line, expr, doctest) |> insert_assertions() + + expected_ast = string_to_quoted(module, maybe_source, expected_line, expected, doctest) last_expr = Macro.to_string(last_expr(expr_ast)) quote do @@ -399,15 +415,18 @@ defmodule ExUnit.DocTest do unquote(last_expr), unquote(expected), unquote(module), - unquote(file), + unquote(maybe_source), unquote(expr_line) ) end end - defp test_case_content(%{expected: {:inspect, expected}} = data, module, file) do + defp test_case_content(%{expected: {:inspect, expected}} = data, module, maybe_source) do %{expr: expr, expr_line: expr_line, doctest: doctest} = data - expr_ast = string_to_quoted(module, file, expr_line, expr, doctest) |> insert_assertions() + + expr_ast = + string_to_quoted(module, maybe_source, expr_line, expr, doctest) |> insert_assertions() + last_expr = Macro.to_string(last_expr(expr_ast)) quote do @@ -418,15 +437,15 @@ defmodule ExUnit.DocTest do unquote(last_expr), unquote(inspect(expected)), unquote(module), - unquote(file), + unquote(maybe_source), unquote(expr_line) ) end end - defp test_case_content(%{expected: {:error, exception, message}} = data, module, file) do + defp test_case_content(%{expected: {:error, exception, message}} = data, module, maybe_source) do %{expr: expr, expr_line: expr_line, doctest: doctest} = data - expr_ast = string_to_quoted(module, file, expr_line, expr, doctest) + expr_ast = string_to_quoted(module, maybe_source, expr_line, expr, doctest) quote do ExUnit.DocTest.__error__( @@ -435,14 +454,14 @@ defmodule ExUnit.DocTest do unquote(exception), unquote(doctest), unquote(module), - unquote(file), + unquote(maybe_source), unquote(expr_line) ) end end @doc false - def __test__(value, expected, doctest, last_expr, expected_expr, module, file, line) do + def __test__(value, expected, doctest, last_expr, expected_expr, module, maybe_source, line) do case value do ^expected -> {:ok, value} @@ -456,12 +475,12 @@ defmodule ExUnit.DocTest do right: expected ] - reraise ExUnit.AssertionError, error, stack(module, file, line) + reraise ExUnit.AssertionError, error, stack(module, maybe_source, line) end end @doc false - def __inspect__(value, expected, doctest, last_expr, expected_expr, module, file, line) do + def __inspect__(value, expected, doctest, last_expr, expected_expr, module, maybe_source, line) do result = try do inspect(value, safe: false) @@ -481,12 +500,12 @@ defmodule ExUnit.DocTest do {extra, stack} -> expr = "inspect(#{last_expr}) === #{String.trim(expected_expr)}" error = [doctest: doctest, expr: expr] ++ extra - reraise ExUnit.AssertionError, error, stack ++ stack(module, file, line) + reraise ExUnit.AssertionError, error, stack ++ stack(module, maybe_source, line) end end @doc false - def __error__(fun, message, exception, doctest, module, file, line) do + def __error__(fun, message, exception, doctest, module, maybe_source, line) do try do fun.() rescue @@ -513,22 +532,22 @@ defmodule ExUnit.DocTest do if failed do reraise ExUnit.AssertionError, [message: failed, doctest: doctest], - stack(module, file, line) + stack(module, maybe_source, line) end else _ -> failed = "Doctest failed: expected exception #{inspect(exception)} but nothing was raised" error = [message: failed, doctest: doctest] - reraise ExUnit.AssertionError, error, stack(module, file, line) + reraise ExUnit.AssertionError, error, stack(module, maybe_source, line) end end defp test_import(_mod, false), do: [] defp test_import(mod, _), do: [quote(do: import(unquote(mod)))] - defp string_to_quoted(module, file, line, expr, doctest) when is_binary(expr) do + defp string_to_quoted(module, maybe_source, line, expr, doctest) when is_binary(expr) do try do - Code.string_to_quoted!(expr, file: file, line: line) + Code.string_to_quoted!(expr, file: maybe_source || inspect(module), line: line) rescue e -> ex_message = "(#{inspect(e.__struct__)}) #{Exception.message(e)}" @@ -557,14 +576,14 @@ defmodule ExUnit.DocTest do quote do reraise ExUnit.AssertionError, unquote(opts), - unquote(Macro.escape(stack(module, file, line))) + unquote(Macro.escape(stack(module, maybe_source, line))) end end end defp stack(module, file, line) do - location = [line: line, file: Path.relative_to_cwd(file)] - [{module, :__MODULE__, 0, location}] + file_info = if file, do: [file: String.to_charlist(file)], else: [] + [{module, :__MODULE__, 0, [line: line] ++ file_info}] end ## Extraction of the tests diff --git a/lib/ex_unit/test/ex_unit/doc_test_test.exs b/lib/ex_unit/test/ex_unit/doc_test_test.exs index 08ed9edb1a6..9d5339695a6 100644 --- a/lib/ex_unit/test/ex_unit/doc_test_test.exs +++ b/lib/ex_unit/test/ex_unit/doc_test_test.exs @@ -578,22 +578,23 @@ defmodule ExUnit.DocTestTest do ExUnit.configure(seed: 0, colors: [enabled: false]) output = capture_io(fn -> ExUnit.run() end) output = String.replace(output, [IO.ANSI.red(), IO.ANSI.reset()], "") + location = location(ExUnit.DocTestTest.Failure) assert output =~ """ 1) doctest module ExUnit.DocTestTest.Failure (1) (ExUnit.DocTestTest.FailureCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{starting_line}:6: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{starting_line}:6: error: syntax error before: '*' │ #{starting_line} │ 1 + * 1 │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{starting_line}:6 + └─ #{location}:#{starting_line}:6 doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line}: ExUnit.DocTestTest.Failure (module) + #{stack(starting_line)}ExUnit.DocTestTest.Failure (module) """ assert output =~ """ @@ -607,7 +608,7 @@ defmodule ExUnit.DocTestTest do left: 2 right: 3 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 3}: ExUnit.DocTestTest.Failure (module) + #{stack(starting_line + 3)}ExUnit.DocTestTest.Failure (module) """ assert output =~ """ @@ -624,7 +625,7 @@ defmodule ExUnit.DocTestTest do left: "This is a slightly shorter text string." right: "This is a much shorter text string." stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 6}: ExUnit.DocTestTest.Failure (module) + #{stack(starting_line + 6)}ExUnit.DocTestTest.Failure (module) """ assert output =~ """ @@ -638,7 +639,7 @@ defmodule ExUnit.DocTestTest do left: ":oops" right: "#Inspect<[]>" stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 12}: ExUnit.DocTestTest.Failure (module) + #{stack(starting_line + 12)}ExUnit.DocTestTest.Failure (module) """ assert output =~ """ @@ -647,7 +648,7 @@ defmodule ExUnit.DocTestTest do ** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available). Make sure the module name is correct and has been specified in full (or that an alias has been defined) stacktrace: Hello.world() - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 15}: (test) + (for doctest at) #{location}:#{starting_line + 15}: (test) """ assert output =~ """ @@ -658,7 +659,7 @@ defmodule ExUnit.DocTestTest do iex> raise "oops" ** (WhatIsThis) oops stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 18}: ExUnit.DocTestTest.Failure (module) + #{stack(starting_line + 18)}ExUnit.DocTestTest.Failure (module) """ assert output =~ """ @@ -673,14 +674,14 @@ defmodule ExUnit.DocTestTest do iex> raise "oops" ** (RuntimeError) hello stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line + 21}: ExUnit.DocTestTest.Failure (module) + #{stack(starting_line + 21)}ExUnit.DocTestTest.Failure (module) """ assert output =~ "8) doctest ExUnit.DocTestTest.Failure.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 + 28}: ExUnit.DocTestTest.Failure (module)" + "#{stack(starting_line + 28)}ExUnit.DocTestTest.Failure (module)" assert output =~ "8 doctests, 8 failures" end @@ -696,26 +697,29 @@ defmodule ExUnit.DocTestTest do doctest_line = __ENV__.line - 3 starting_line = ExUnit.DocTestTest.Invalid.starting_line() + 1 + location = location(ExUnit.DocTestTest.Invalid) ExUnit.configure(seed: 0, colors: [enabled: false]) output = capture_io(fn -> ExUnit.run() end) output = String.replace(output, [IO.ANSI.red(), IO.ANSI.reset()], "") + line = starting_line + assert output =~ """ 1) doctest ExUnit.DocTestTest.Invalid.a/0 (1) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{starting_line}:6: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{line}:6: error: syntax error before: '*' │ - #{starting_line} │ 1 + * 1 + #{line} │ 1 + * 1 │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{starting_line}:6 + └─ #{location}:#{line}:6 doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{starting_line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 6 @@ -723,18 +727,18 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 2) doctest ExUnit.DocTestTest.Invalid.b/0 (2) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{line}:6: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{line}:6: error: syntax error before: '*' │ #{line} │ 1 + * 1 │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:6 + └─ #{location}:#{line}:6 doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 15 @@ -742,19 +746,19 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 3) doctest ExUnit.DocTestTest.Invalid.indented_too_much/0 (3) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{line}:3: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{line}:3: error: unexpected token: "`" (column 3, code point U+0060) │ #{line} │ ``` │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:3 + └─ #{location}:#{line}:3 doctest: iex> 1 + 2 3 ``` stacktrace: - test/ex_unit/doc_test_test.exs:#{line - 1}: ExUnit.DocTestTest.Invalid (module) + #{stack(line - 1)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 23 @@ -762,19 +766,19 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 4) doctest ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (4) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{line}:5: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{line}:5: error: unexpected token: "`" (column 5, code point U+0060) │ #{line} │ ``` │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:5 + └─ #{location}:#{line}:5 doctest: iex> 1 + 2 3 ``` stacktrace: - test/ex_unit/doc_test_test.exs:#{line - 1}: ExUnit.DocTestTest.Invalid (module) + #{stack(line - 1)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 28 @@ -784,7 +788,7 @@ defmodule ExUnit.DocTestTest do test/ex_unit/doc_test_test.exs:#{doctest_line} Doctest did not compile, got: (UnicodeConversionError) invalid encoding starting at <<255, 34, 41>> stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 35 @@ -792,20 +796,20 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 6) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (6) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:#{line}:20: + Doctest did not compile, got: (TokenMissingError) token missing on #{location}:#{line}:20: error: missing terminator: } │ 228 │ {:ok, #Inspect<[]>} │ │ └ missing closing delimiter (expected "}") │ └ unclosed delimiter │ - └─ test/ex_unit/doc_test_test.exs:#{line}:20 + └─ #{location}:#{line}:20 If you are planning to assert on the result of an iex> expression which contains a value inspected as #Name<...>, please make sure the inspected value is placed at the beginning of the expression, otherwise Elixir will treat it as a comment due to the leading sign #. doctest: iex> {:ok, :oops} {:ok, #Inspect<[]>} stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 40 @@ -813,18 +817,18 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 7) doctest ExUnit.DocTestTest.Invalid.t/0 (7) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{line}:6: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{line}:6: error: syntax error before: '*' │ #{line} │ 1 + * 1 │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:6 + └─ #{location}:#{line}:6 doctest: iex> 1 + * 1 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 48 @@ -832,19 +836,19 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 8) doctest ExUnit.DocTestTest.Invalid.result/0 (8) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (SyntaxError) invalid syntax found on test/ex_unit/doc_test_test.exs:#{line}:5: + Doctest did not compile, got: (SyntaxError) invalid syntax found on #{location}:#{line}:5: error: syntax error before: '*' │ #{line} │ 1 + * 1 │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:5 + └─ #{location}:#{line}:5 doctest: iex> :foo iex> :bar 1 + * 1 stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 53 @@ -852,18 +856,18 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 9) doctest ExUnit.DocTestTest.Invalid.mixed/0 (9) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:#{line}:6: + Doctest did not compile, got: (TokenMissingError) token missing on #{location}:#{line}:6: error: syntax error: expression is incomplete │ #{line} │ 123 + │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:6 + └─ #{location}:#{line}:6 doctest: iex> 123 + :mixed stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ line = starting_line + 61 @@ -871,18 +875,18 @@ defmodule ExUnit.DocTestTest do assert output =~ """ 10) doctest ExUnit.DocTestTest.Invalid.invalid_second/0 (10) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:#{line}:6: + Doctest did not compile, got: (TokenMissingError) token missing on #{location}:#{line}:6: error: syntax error: expression is incomplete │ #{line} │ 123 + │ ^ │ - └─ test/ex_unit/doc_test_test.exs:#{line}:6 + └─ #{location}:#{line}:6 doctest: iex> 123 + :mixed stacktrace: - test/ex_unit/doc_test_test.exs:#{line}: ExUnit.DocTestTest.Invalid (module) + #{stack(line)}ExUnit.DocTestTest.Invalid (module) """ assert output =~ "10 doctests, 10 failures" @@ -896,6 +900,7 @@ defmodule ExUnit.DocTestTest do doctest_line = __ENV__.line - 3 starting_line = ExUnit.DocTestTest.PatternMatching.starting_line() + location = location(ExUnit.DocTestTest.PatternMatching) ExUnit.configure(seed: 0, colors: [enabled: false]) output = capture_io(fn -> ExUnit.run() end) @@ -908,7 +913,7 @@ defmodule ExUnit.DocTestTest do left: {1, 2} right: {1, 3} stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 3}: (test) + (for doctest at) #{location}:#{starting_line + 3}: (test) """ assert output =~ """ @@ -919,7 +924,7 @@ defmodule ExUnit.DocTestTest do left: {1, 2} right: {1, 3} stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 6}: (test) + (for doctest at) #{location}:#{starting_line + 6}: (test) """ assert output =~ """ @@ -930,7 +935,7 @@ defmodule ExUnit.DocTestTest do left: "Hello, world" right: "Hello world" stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 8}: (test) + (for doctest at) #{location}:#{starting_line + 8}: (test) """ assert output =~ """ @@ -941,7 +946,7 @@ defmodule ExUnit.DocTestTest do left: "Hello, " <> _ right: "Hello world" stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 10}: (test) + (for doctest at) #{location}:#{starting_line + 10}: (test) """ assert output =~ """ @@ -952,7 +957,7 @@ defmodule ExUnit.DocTestTest do left: [:a | _] right: [:b, :a] stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 12}: (test) + (for doctest at) #{location}:#{starting_line + 12}: (test) """ assert output =~ """ @@ -965,7 +970,7 @@ defmodule ExUnit.DocTestTest do left: [^atom | _] right: [:b, :a] stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 15}: (test) + (for doctest at) #{location}:#{starting_line + 15}: (test) """ assert output =~ """ @@ -976,7 +981,7 @@ defmodule ExUnit.DocTestTest do left: %{b: _, d: :e} right: %{a: :c, d: :e} stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 17}: (test) + (for doctest at) #{location}:#{starting_line + 17}: (test) """ assert output =~ """ @@ -987,7 +992,7 @@ defmodule ExUnit.DocTestTest do left: %{year: 2001, day: 1} right: ~D[2000-01-01] stacktrace: - (for doctest at) test/ex_unit/doc_test_test.exs:#{starting_line + 19}: (test) + (for doctest at) #{location}:#{starting_line + 19}: (test) """ assert output =~ "10 doctests, 8 failures" @@ -1006,9 +1011,7 @@ defmodule ExUnit.DocTestTest do message = ~s[unknown IEx prompt: "iex(foo@bar)1> 1 +".\nAccepted formats are: iex>, iex(1)>, ...>, ...(1)>] - regex = ~r[test/ex_unit/doc_test_test\.exs:\d+: #{Regex.escape(message)}] - - assert_raise ExUnit.DocTest.Error, regex, fn -> + assert_raise ExUnit.DocTest.Error, ~r/#{Regex.escape(message)}/, fn -> defmodule HostUsage do use ExUnit.Case doctest ExUnit.DocTestTest.Host @@ -1074,7 +1077,7 @@ defmodule ExUnit.DocTestTest 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: + #{stack(0)}undefined or private functions given to doctest: ExUnit.DocTestTest.SomewhatGoodModuleWithOnly.three/0 ExUnit.DocTestTest.SomewhatGoodModuleWithOnly.four/1 @@ -1105,7 +1108,7 @@ defmodule ExUnit.DocTestTest do test "fails in indentation mismatch" do message = - ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch on doctest line: \" iex> bar = 2\".*is exactly 2 spaces]s + ~r[indentation level mismatch on doctest line: \" iex> bar = 2\".*is exactly 2 spaces]s assert_raise ExUnit.DocTest.Error, message, fn -> defmodule NeverCompiled do @@ -1114,8 +1117,7 @@ defmodule ExUnit.DocTestTest do end end - message = - ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch on doctest line: \" 3\".*is exactly 2 spaces]s + message = ~r[indentation level mismatch on doctest line: \" 3\".*is exactly 2 spaces]s assert_raise ExUnit.DocTest.Error, message, fn -> defmodule NeverCompiled do @@ -1124,8 +1126,7 @@ defmodule ExUnit.DocTestTest do end end - message = - ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch on doctest line: \" 3\".*is exactly 4 spaces]s + message = ~r[indentation level mismatch on doctest line: \" 3\".*is exactly 4 spaces]s assert_raise ExUnit.DocTest.Error, message, fn -> defmodule NeverCompiled do @@ -1134,8 +1135,7 @@ defmodule ExUnit.DocTestTest do end end - message = - ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch on doctest line: \"```\".*is exactly 2 spaces]s + message = ~r[indentation level mismatch on doctest line: \"```\".*is exactly 2 spaces]s assert_raise ExUnit.DocTest.Error, message, fn -> defmodule NeverCompiled do @@ -1145,7 +1145,7 @@ defmodule ExUnit.DocTestTest do end message = - ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch on doctest line: \"world\\\"\".*is exactly 4 spaces]s + ~r[indentation level mismatch on doctest line: \"world\\\"\".*is exactly 4 spaces]s assert_raise ExUnit.DocTest.Error, message, fn -> defmodule NeverCompiled do @@ -1211,4 +1211,20 @@ defmodule ExUnit.DocTestTest do [head | _] = Direct.__ex_unit__().tests assert apply(Direct, head.name, [%{}]) == {:ok, 2} end + + defp location(module) do + if __MODULE__.module_info(:compile)[:source] do + "test/ex_unit/doc_test_test.exs" + else + inspect(module) + end + end + + defp stack(line) do + if __MODULE__.module_info(:compile)[:source] do + [Exception.format_file_line("test/ex_unit/doc_test_test.exs", line), ?\s] + else + "" + end + end end diff --git a/lib/iex/lib/iex/introspection.ex b/lib/iex/lib/iex/introspection.ex index 7f793a23c50..840c14ff45f 100644 --- a/lib/iex/lib/iex/introspection.ex +++ b/lib/iex/lib/iex/introspection.ex @@ -97,7 +97,7 @@ defmodule IEx.Introspection do case open_mfa(module, :__info__, 1) do {source, nil, _} -> open(source) {_, tuple, _} -> open(tuple) - :error -> puts_error("Could not open: #{inspect(module)}. Module is not available.") + {:error, reason} -> puts_error("Could not open #{inspect(module)}, #{reason}") end dont_display_result() @@ -107,14 +107,14 @@ defmodule IEx.Introspection do case open_mfa(module, function, :*) do {_, _, nil} -> puts_error( - "Could not open: #{inspect(module)}.#{function}. Function/macro is not available." + "Could not open #{inspect(module)}.#{function}, function/macro is not available" ) {_, _, tuple} -> open(tuple) - :error -> - puts_error("Could not open: #{inspect(module)}.#{function}. Module is not available.") + {:error, reason} -> + puts_error("Could not open #{inspect(module)}.#{function}, #{reason}") end dont_display_result() @@ -125,16 +125,14 @@ defmodule IEx.Introspection do case open_mfa(module, function, arity) do {_, _, nil} -> puts_error( - "Could not open: #{inspect(module)}.#{function}/#{arity}. Function/macro is not available." + "Could not open #{inspect(module)}.#{function}/#{arity}, function/macro is not available" ) {_, _, tuple} -> open(tuple) - :error -> - puts_error( - "Could not open: #{inspect(module)}.#{function}/#{arity}. Module is not available." - ) + {:error, reason} -> + puts_error("Could not open #{inspect(module)}.#{function}/#{arity}, #{reason}") end dont_display_result() @@ -143,7 +141,7 @@ defmodule IEx.Introspection do def open({file, line}) when is_binary(file) and is_integer(line) do cond do not File.regular?(file) -> - puts_error("Could not open: #{inspect(file)}. File is not available.") + puts_error("Could not open #{inspect(file)}, file is not available.") editor = System.get_env("ELIXIR_EDITOR") || System.get_env("EDITOR") -> command = @@ -174,12 +172,19 @@ defmodule IEx.Introspection do end defp open_mfa(module, fun, arity) do - with {:module, _} <- Code.ensure_loaded(module), - source when is_list(source) <- module.module_info(:compile)[:source] do - source = rewrite_source(module, source) - open_abstract_code(module, fun, arity, source) - else - _ -> :error + case Code.ensure_loaded(module) do + {:module, _} -> + case module.module_info(:compile)[:source] do + [_ | _] = source -> + source = rewrite_source(module, source) + open_abstract_code(module, fun, arity, source) + + _ -> + {:error, "source code is not available"} + end + + _ -> + {:error, "module is not available"} end end diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 975c46ce41c..1af56114bae 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -142,6 +142,7 @@ defmodule IEx.HelpersTest do end describe "open" do + @describetag :requires_source @iex_helpers "iex/lib/iex/helpers.ex" @elixir_erl "elixir/src/elixir.erl" @lists_erl Application.app_dir(:stdlib, "src/lists.erl") @@ -235,35 +236,35 @@ defmodule IEx.HelpersTest do end test "errors if module is not available" do - assert capture_iex("open(:unknown)") == "Could not open: :unknown. Module is not available." + assert capture_iex("open(:unknown)") == "Could not open :unknown, module is not available" end test "errors if module.function is not available" do assert capture_iex("open(:unknown.unknown)") == - "Could not open: :unknown.unknown. Module is not available." + "Could not open :unknown.unknown, module is not available" assert capture_iex("open(:elixir.unknown)") == - "Could not open: :elixir.unknown. Function/macro is not available." + "Could not open :elixir.unknown, function/macro is not available" assert capture_iex("open(:lists.unknown)") == - "Could not open: :lists.unknown. Function/macro is not available." + "Could not open :lists.unknown, function/macro is not available" assert capture_iex("open(:httpc.unknown)") == - "Could not open: :httpc.unknown. Function/macro is not available." + "Could not open :httpc.unknown, function/macro is not available" end test "errors if module.function/arity is not available" do assert capture_iex("open(:unknown.start/10)") == - "Could not open: :unknown.start/10. Module is not available." + "Could not open :unknown.start/10, module is not available" assert capture_iex("open(:elixir.start/10)") == - "Could not open: :elixir.start/10. Function/macro is not available." + "Could not open :elixir.start/10, function/macro is not available" assert capture_iex("open(:lists.reverse/10)") == - "Could not open: :lists.reverse/10. Function/macro is not available." + "Could not open :lists.reverse/10, function/macro is not available" assert capture_iex("open(:httpc.request/10)") == - "Could not open: :httpc.request/10. Function/macro is not available." + "Could not open :httpc.request/10, function/macro is not available" end test "errors if module is in-memory" do @@ -289,7 +290,7 @@ defmodule IEx.HelpersTest do test "errors when given {file, line} is not available" do assert capture_iex("open({~s[foo], 3})") =~ - "Could not open: \"foo\". File is not available." + "Could not open \"foo\", file is not available" end defp maybe_trim_quotes(string) do @@ -1445,6 +1446,8 @@ defmodule IEx.HelpersTest do end describe "r" do + @describetag :requires_source + test "raises when reloading a nonexistent module" do assert_raise ArgumentError, "could not load nor find module: :nonexistent_module", fn -> r(:nonexistent_module) diff --git a/lib/iex/test/iex/info_test.exs b/lib/iex/test/iex/info_test.exs index 3b28a358e95..0238b66db7e 100644 --- a/lib/iex/test/iex/info_test.exs +++ b/lib/iex/test/iex/info_test.exs @@ -19,7 +19,6 @@ defmodule IEx.InfoTest do test "loaded module (without docs)" do info = Info.info(Foo) assert get_key(info, "Data type") == "Atom" - assert get_key(info, "Source") == Path.relative_to_cwd(__ENV__.file) assert get_key(info, "Description") == "Call IEx.InfoTest.Foo.module_info() to access metadata." @@ -27,6 +26,12 @@ defmodule IEx.InfoTest do assert get_key(info, "Raw representation") == ~s(:"Elixir.IEx.InfoTest.Foo") end + @tag :requires_source + test "loaded module (with source)" do + info = Info.info(Foo) + assert get_key(info, "Source") == Path.relative_to_cwd(__ENV__.file) + end + test "loaded module (with docs)" do info = Info.info(List) diff --git a/lib/iex/test/test_helper.exs b/lib/iex/test/test_helper.exs index 8ebfbe9811a..7c7316141e0 100644 --- a/lib/iex/test/test_helper.exs +++ b/lib/iex/test/test_helper.exs @@ -15,11 +15,18 @@ erlang_doc_exclude = [:erlang_doc] end +source_exclude = + if :deterministic in :compile.env_compiler_options() do + [:requires_source] + else + [] + end + ExUnit.start( assert_receive_timeout: assert_timeout, trace: !!System.get_env("TRACE"), include: line_include, - exclude: line_exclude ++ erlang_doc_exclude + exclude: line_exclude ++ erlang_doc_exclude ++ source_exclude ) defmodule IEx.Case do diff --git a/lib/logger/test/logger_test.exs b/lib/logger/test/logger_test.exs index 2d26722711d..8cf69a4ba47 100644 --- a/lib/logger/test/logger_test.exs +++ b/lib/logger/test/logger_test.exs @@ -346,8 +346,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:info, fn -> - assert Logger.debug(raise("not invoked"), []) == :ok + assert Logger.debug(send(self(), :something), []) == :ok end) == "" + + refute_received :something end test "info/2" do @@ -360,8 +362,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:notice, fn -> - assert Logger.info(raise("not invoked"), []) == :ok + assert Logger.info(send(self(), :something), []) == :ok end) == "" + + refute_received :something end test "warning/2" do @@ -374,8 +378,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:error, fn -> - assert Logger.warning(raise("not invoked"), []) == :ok + assert Logger.warning(send(self(), :something), []) == :ok end) == "" + + refute_received :something end test "error/2" do @@ -388,8 +394,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:critical, fn -> - assert Logger.error(raise("not invoked"), []) == :ok + assert Logger.error(send(self(), :something), []) == :ok end) == "" + + refute_received :something end test "critical/2" do @@ -402,8 +410,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:alert, fn -> - assert Logger.critical(raise("not invoked"), []) == :ok + assert Logger.critical(send(self(), :something), []) == :ok end) == "" + + refute_received :something end test "alert/2" do @@ -416,8 +426,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:emergency, fn -> - assert Logger.alert(raise("not invoked"), []) == :ok + assert Logger.alert(send(self(), :something), []) == :ok end) == "" + + refute_received :something end test "emergency/2" do @@ -430,8 +442,10 @@ defmodule LoggerTest do end) == "" assert capture_log(:none, fn -> - assert Logger.emergency(raise("not invoked"), []) == :ok + assert Logger.emergency(send(self(), :something), []) == :ok end) == "" + + refute_received :something end end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 3d620b76591..019e155de9d 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,7 +1,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 25 + @manifest_vsn 26 @checkpoint_vsn 2 import Record @@ -1042,6 +1042,7 @@ defmodule Mix.Compilers.Elixir do threshold = opts[:long_compilation_threshold] || 10 profile = opts[:profile] verbose = opts[:verbose] || false + warnings_as_errors = opts[:warnings_as_errors] || false pid = spawn_link(fn -> @@ -1063,7 +1064,8 @@ defmodule Mix.Compilers.Elixir do long_compilation_threshold: threshold, profile: profile, beam_timestamp: timestamp, - return_diagnostics: true + return_diagnostics: true, + warnings_as_errors: warnings_as_errors ] response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) diff --git a/lib/mix/lib/mix/compilers/erlang.ex b/lib/mix/lib/mix/compilers/erlang.ex index 14e22940df5..98a84311a94 100644 --- a/lib/mix/lib/mix/compilers/erlang.ex +++ b/lib/mix/lib/mix/compilers/erlang.ex @@ -295,9 +295,8 @@ defmodule Mix.Compilers.Erlang do defp to_diagnostics(warnings_or_errors, severity) do for {file, issues} <- warnings_or_errors, + file = if(?/ in file, do: Path.absname(file), else: List.to_string(file)), {location, module, data} <- issues do - file = Path.absname(file) - %Mix.Task.Compiler.Diagnostic{ file: file, source: file, diff --git a/lib/mix/lib/mix/compilers/test.ex b/lib/mix/lib/mix/compilers/test.ex index f518b8ffaa9..1cd86d20e30 100644 --- a/lib/mix/lib/mix/compilers/test.ex +++ b/lib/mix/lib/mix/compilers/test.ex @@ -22,7 +22,9 @@ defmodule Mix.Compilers.Test do test patterns, the test paths, and the opts from the test task. """ def require_and_run(matched_test_files, test_paths, elixirc_opts, opts) do - elixirc_opts = Keyword.merge([docs: false, debug_info: false], elixirc_opts) + elixirc_opts = + Keyword.merge([docs: false, debug_info: false, infer_signatures: false], elixirc_opts) + previous_opts = Code.compiler_options(elixirc_opts) try do diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex index d5f1f57ecf2..62526b929d0 100644 --- a/lib/mix/lib/mix/project.ex +++ b/lib/mix/lib/mix/project.ex @@ -163,7 +163,12 @@ defmodule Mix.Project do # Only the top of the stack can be accessed. @doc false def push(module, file \\ nil, app \\ nil) when is_atom(module) do - file = file || (module && List.to_string(module.__info__(:compile)[:source])) + file = + cond do + file != nil -> file + source = module && module.module_info(:compile)[:source] -> List.to_string(source) + true -> "nofile" + end case Mix.ProjectStack.push(module, push_config(module, app), file) do :ok -> diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index bf1572dba2f..7c3894f5992 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -77,9 +77,9 @@ defmodule Mix.Tasks.Compile.Elixir do Defaults to `["lib"]`. * `:elixirc_options` - compilation options that apply to Elixir's compiler. - See `Code.put_compiler_option/2` for a complete list of options. These - options are often overridable from the command line using the switches - above. + It supports many of the options above plus the options listed in + `Code.put_compiler_option/2`. In case conflicting options are given, + the ones given through the command line are used. * `[xref: [exclude: ...]]` - a list of `module` or `{module, function, arity}` that should not be warned on in case on undefined modules or undefined diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index ea02db83278..ec3a0b60c5f 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -221,8 +221,8 @@ defmodule Mix.Tasks.Test do mechanism. See the "Coverage" section for more information * `:test_elixirc_options` - the compiler options to used when - loading/compiling test files. By default it disables the debug chunk - and docs chunk + loading/compiling test files. By default it disables the debug chunk, + docs chunk, and module type inference * `:test_paths` - list of paths containing test files. Defaults to `["test"]` if the `test` directory exists, otherwise, it defaults to `[]`. diff --git a/lib/mix/test/mix/cli_test.exs b/lib/mix/test/mix/cli_test.exs index e14c640860d..4679f1f8ecf 100644 --- a/lib/mix/test/mix/cli_test.exs +++ b/lib/mix/test/mix/cli_test.exs @@ -240,6 +240,7 @@ defmodule Mix.CLITest do System.delete_env("MIX_EXS") end + @tag :cover @tag tmp_dir: "new_with_tests" test "new with tests and cover", %{tmp_dir: tmp_dir} do File.cd!(tmp_dir, fn -> diff --git a/lib/mix/test/mix/tasks/compile.erlang_test.exs b/lib/mix/test/mix/tasks/compile.erlang_test.exs index c0086f914f1..f309cbd78be 100644 --- a/lib/mix/test/mix/tasks/compile.erlang_test.exs +++ b/lib/mix/test/mix/tasks/compile.erlang_test.exs @@ -83,6 +83,7 @@ defmodule Mix.Tasks.Compile.ErlangTest do test "continues even if one file fails to compile" do in_fixture("compile_erlang", fn -> file = Path.absname("src/zzz.erl") + source = deterministic_source(file) File.write!(file, """ -module(zzz). @@ -94,8 +95,8 @@ defmodule Mix.Tasks.Compile.ErlangTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "erl_parse", - file: ^file, - source: ^file, + file: ^source, + source: ^source, message: "syntax error before: zzz", position: position(2, 5), severity: :error @@ -110,6 +111,7 @@ defmodule Mix.Tasks.Compile.ErlangTest do test "saves warnings between builds" do in_fixture("compile_erlang", fn -> file = Path.absname("src/has_warning.erl") + source = deterministic_source(file) File.write!(file, """ -module(has_warning). @@ -120,8 +122,8 @@ defmodule Mix.Tasks.Compile.ErlangTest do assert {:ok, [diagnostic]} = Mix.Tasks.Compile.Erlang.run([]) assert %Mix.Task.Compiler.Diagnostic{ - file: ^file, - source: ^file, + file: ^source, + source: ^source, compiler_name: "erl_lint", message: "function my_fn/0 is unused", position: position(2, 1), @@ -161,11 +163,11 @@ defmodule Mix.Tasks.Compile.ErlangTest do assert capture_io(fn -> assert {:noop, _} = Mix.Tasks.Compile.Erlang.run([]) - end) =~ ~r"src/has_warning.erl:2:(1:)? warning: function my_fn/0 is unused\n" + end) =~ ~r"has_warning.erl:2:(1:)? warning: function my_fn/0 is unused\n" assert capture_io(fn -> assert {:noop, _} = Mix.Tasks.Compile.Erlang.run([]) - end) =~ ~r"src/has_warning.erl:2:(1:)? warning: function my_fn/0 is unused\n" + end) =~ ~r"has_warning.erl:2:(1:)? warning: function my_fn/0 is unused\n" # Should not print old warnings after fixing File.write!(file, """ @@ -183,6 +185,39 @@ defmodule Mix.Tasks.Compile.ErlangTest do end) end + test "returns syntax error from an Erlang file when --return-errors is set" do + in_fixture("no_mixfile", fn -> + import ExUnit.CaptureIO + + file = Path.absname("src/a.erl") + source = deterministic_source(file) + + File.mkdir!("src") + + File.write!(file, """ + -module(b). + def b(), do: b + """) + + capture_io(fn -> + assert {:error, [diagnostic]} = + Mix.Tasks.Compile.Erlang.run(["--force", "--return-errors"]) + + assert %Mix.Task.Compiler.Diagnostic{ + compiler_name: "erl_parse", + file: ^source, + source: ^source, + message: "syntax error before: b", + position: position(2, 5), + severity: :error + } = diagnostic + end) + + refute File.regular?("ebin/Elixir.A.beam") + refute File.regular?("ebin/Elixir.B.beam") + end) + end + @tag erlc_options: [{:warnings_as_errors, true}] test "adds :debug_info to erlc_options by default" do in_fixture("compile_erlang", fn -> @@ -196,4 +231,10 @@ defmodule Mix.Tasks.Compile.ErlangTest do assert debug_info != :none end) end + + if :deterministic in :compile.env_compiler_options() do + defp deterministic_source(file), do: Path.basename(file) + else + defp deterministic_source(file), do: file + end end diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index aeed28ffd77..2ecf29cbeb7 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -224,38 +224,6 @@ defmodule Mix.Tasks.CompileTest do end) end - test "returns syntax error from an Erlang file when --return-errors is set" do - in_fixture("no_mixfile", fn -> - import ExUnit.CaptureIO - - file = Path.absname("src/a.erl") - File.mkdir!("src") - - File.write!(file, """ - -module(b). - def b(), do: b - """) - - assert File.regular?(file) - - capture_io(fn -> - assert {:error, [diagnostic]} = Mix.Task.run("compile", ["--force", "--return-errors"]) - - assert %Mix.Task.Compiler.Diagnostic{ - compiler_name: "erl_parse", - file: ^file, - source: ^file, - message: "syntax error before: b", - position: position(2, 5), - severity: :error - } = diagnostic - end) - - refute File.regular?("ebin/Elixir.A.beam") - refute File.regular?("ebin/Elixir.B.beam") - end) - end - test "skip protocol consolidation when --no-protocol-consolidation" do in_fixture("no_mixfile", fn -> File.rm("_build/dev/lib/sample/.mix/compile.protocols") diff --git a/lib/mix/test/mix/tasks/test_test.exs b/lib/mix/test/mix/tasks/test_test.exs index 5898b90f4cd..b51ed680db1 100644 --- a/lib/mix/test/mix/tasks/test_test.exs +++ b/lib/mix/test/mix/tasks/test_test.exs @@ -134,6 +134,7 @@ defmodule Mix.Tasks.TestTest do end describe "--cover" do + @describetag :cover test "reports the coverage of each app's modules in an umbrella" do in_fixture("umbrella_test", fn -> # This fixture by default results in coverage above the default threshold @@ -341,7 +342,8 @@ defmodule Mix.Tasks.TestTest do end describe "--partitions" do - test "splits tests into partitions" do + @tag :cover + test "splits tests into partitions (with coverage)" do in_fixture("test_stale", fn -> assert mix(["test", "--partitions", "3", "--cover"], [{"MIX_TEST_PARTITION", "1"}]) =~ "1 test, 0 failures" diff --git a/lib/mix/test/mix/tasks/xref_test.exs b/lib/mix/test/mix/tasks/xref_test.exs index 1d231e14be5..1f4430a26c3 100644 --- a/lib/mix/test/mix/tasks/xref_test.exs +++ b/lib/mix/test/mix/tasks/xref_test.exs @@ -398,7 +398,7 @@ defmodule Mix.Tasks.XrefTest do assert Mix.Task.run("xref", opts ++ ["trace", file]) == :ok end) - assert ^expected = receive_until_no_messages([]) + assert receive_until_no_messages([]) == expected end) end end diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index 16e4c687846..0fc0abc486e 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -37,9 +37,16 @@ git_exclude = {line_exclude, line_include} = if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []} +cover_exclude = + if :deterministic in :compile.env_compiler_options() do + [:cover] + else + [] + end + ExUnit.start( trace: !!System.get_env("TRACE"), - exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude, + exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude, include: line_include )