From c67624b2fcb16135734c497ffba9e2f7cdde69ce Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Mon, 2 Nov 2026 14:33:15 +0100 Subject: [PATCH] Make source absolute for diagnostics emitted from after_verify Warnings emitted from @after_verify callbacks run in the parallel checker, where :elixir_compiler_file is not set. Their source field is therefore whatever :file the callback passed to IO.warn/2. The checker already made :file absolute when collecting the diagnostic but left :source untouched. Mix attaches warnings to their source files by matching :source against absolute paths. A relative :source matched nothing, so such a warning was printed once and never persisted in the manifest: later compilations did not print it again with --all-warnings, and a no-op compilation passed --warnings-as-errors despite it. Phoenix wants to store these paths relative to the project so that a build reused from another directory, such as a git worktree, reports the right files (phoenixframework/phoenix#6889). --- lib/elixir/lib/module/parallel_checker.ex | 9 ++++-- .../elixir/module/types/integration_test.exs | 17 ++++++++++ .../test/mix/tasks/compile.elixir_test.exs | 31 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 7627428d315..f3211a37ad5 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -212,9 +212,14 @@ defmodule Module.ParallelChecker do defp collect_results(count, warnings, errors) do receive do - {:diagnostic, %{file: file} = diagnostic, read_snippet} -> + {:diagnostic, %{file: file, source: source} = diagnostic, read_snippet} -> :elixir_errors.print_diagnostic(diagnostic, read_snippet) - diagnostic = %{diagnostic | file: file && Path.absname(file)} + + diagnostic = %{ + diagnostic + | file: file && Path.absname(file), + source: source && Path.absname(source) + } if Map.get(diagnostic, :severity, :warning) == :error do collect_results(count, warnings, [diagnostic | errors]) diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index fa1db95edd5..3765f0e4801 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -865,10 +865,18 @@ defmodule Module.Types.IntegrationTest do files = %{ "a.ex" => """ defmodule A do + @after_verify __MODULE__ @file "generated.ex" def fun(arg) do :ok = List.to_tuple(arg) end + + def __after_verify__(__MODULE__) do + IO.warn("This is a manually emitted warning", + file: Path.relative_to_cwd(__ENV__.file), + line: __ENV__.line + ) + end end """ } @@ -880,6 +888,11 @@ defmodule Module.Types.IntegrationTest do message: "the following pattern will never match" <> _, file: file, source: source + }, + %{ + message: "This is a manually emitted warning", + file: file2, + source: source2 } ] = warnings.runtime_warnings @@ -887,6 +900,10 @@ defmodule Module.Types.IntegrationTest do assert Path.type(source) == :absolute assert String.ends_with?(file, "generated.ex") assert Path.type(file) == :absolute + assert String.ends_with?(source2, "a.ex") + assert Path.type(source2) == :absolute + assert String.ends_with?(file2, "a.ex") + assert Path.type(file2) == :absolute after purge(A) end diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index dc1e0b12860..ca83b53be81 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1313,6 +1313,37 @@ defmodule Mix.Tasks.Compile.ElixirTest do end) end + test "tracks warnings from after_verify with relative paths" do + in_fixture("no_mixfile", fn -> + Mix.Project.push(MixTest.Case.Sample) + + File.write!("lib/a.ex", """ + defmodule A do + @after_verify __MODULE__ + + def __after_verify__(__MODULE__) do + IO.warn("oops", file: Path.relative_to_cwd(__ENV__.file), line: 13) + end + end + """) + + # after_verify runs in the checker, which must make source and file absolute + file = Path.absname("lib/a.ex") + + assert capture_io(:stderr, fn -> + assert {:ok, + [%Mix.Task.Compiler.Diagnostic{source: ^file, file: ^file, position: 13}]} = + Mix.Tasks.Compile.Elixir.run([]) + + assert {:noop, + [%Mix.Task.Compiler.Diagnostic{source: ^file, file: ^file, position: 13}]} = + Mix.Tasks.Compile.Elixir.run(["--all-warnings"]) + end) =~ "oops" + + purge([A]) + end) + end + test "recompiles modules with exports tracking" do Process.register(self(), __MODULE__) Code.put_compiler_option(:tracers, [__MODULE__])