From 2422d5159113ddae001a7f4408bf57aa07dbef85 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 4 Mar 2026 13:13:56 -0500 Subject: [PATCH] controller: warn when guarding against unknown actions --- lib/phoenix/controller/pipeline.ex | 44 +++++++++++++++++++++++ test/phoenix/controller/pipeline_test.exs | 32 +++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/lib/phoenix/controller/pipeline.ex b/lib/phoenix/controller/pipeline.ex index a99f921ebf..3f9707d80c 100644 --- a/lib/phoenix/controller/pipeline.ex +++ b/lib/phoenix/controller/pipeline.ex @@ -11,6 +11,7 @@ defmodule Phoenix.Controller.Pipeline do Module.register_attribute(__MODULE__, :plugs, accumulate: true) @before_compile Phoenix.Controller.Pipeline + @after_compile Phoenix.Controller.Pipeline @phoenix_fallback :unregistered @doc false @@ -38,6 +39,7 @@ defmodule Phoenix.Controller.Pipeline do @doc false def __action_fallback__(plug, caller) do plug = Macro.expand(plug, %{caller | function: {:init, 1}}) + quote bind_quoted: [plug: plug] do @phoenix_fallback Phoenix.Controller.Pipeline.validate_fallback( plug, @@ -156,6 +158,48 @@ defmodule Phoenix.Controller.Pipeline do Plug.Conn.WrapperError.reraise(conn, :error, reason, stack) end + @doc false + defmacro __after_compile__(env, _bytecode) do + guards = + for {_plug, _opts, guard} <- Module.get_attribute(env.module, :plugs), + guard != true, + do: guard + + actions = + guards + |> Macro.prewalk([], fn node, acc -> + acc = + case node do + {:in, _, [{:action, _, _}, actions]} when is_list(actions) -> + actions |> Enum.filter(&is_atom/1) |> Enum.concat(acc) + + {:==, _, [{:action, _, _}, action]} when is_atom(action) -> + [action | acc] + + {:!=, _, [{:action, _, _}, action]} when is_atom(action) -> + [action | acc] + + _ -> + acc + end + + {node, acc} + end) + |> elem(1) + |> Enum.uniq() + + unknown = actions -- (:functions |> env.module.__info__() |> Keyword.keys()) + + if unknown != [] do + IO.warn( + "Unknown action(s) referenced in #{inspect(env.module)} plug guards: #{inspect(unknown)}", + env + ) + end + + :ok + end + @doc """ Stores a plug to be executed as part of the plug pipeline. """ diff --git a/test/phoenix/controller/pipeline_test.exs b/test/phoenix/controller/pipeline_test.exs index 151027297e..279827e9b9 100644 --- a/test/phoenix/controller/pipeline_test.exs +++ b/test/phoenix/controller/pipeline_test.exs @@ -232,6 +232,38 @@ defmodule Phoenix.Controller.PipelineTest do end end + describe "unknown actions in plug guards" do + test "warns when unknown actions are found" do + warning = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + defmodule UnknownActionsController do + use Phoenix.Controller, formats: [] + + @module_attribute [:unknown3] + + plug :identity when action == :unknown0 + plug :identity when action in [:index, :unknown1, :unknown2] + plug :identity when action in @module_attribute + plug :identity when action != :unknown4 + plug :identity when action not in [:unknown5] + + defp identity(conn, _), do: conn + + def index(conn, _), do: conn + end + end) + + assert warning =~ + "Unknown action(s) referenced in Phoenix.Controller.PipelineTest.UnknownActionsController plug guards: [" + + for n <- 0..5 do + assert warning =~ ":unknown#{n}" + end + + refute warning =~ ":index" + end + end + defp stack_conn() do conn(:get, "/") |> fetch_query_params()