Is there a tool for checking non-existed controller action?

for example, in my router.ex:

get "/api/foo", UserController, :index
put "/api/foo", UserController, :update

and in my user_controller, I just defined the index action:

def index(conn, params)

and when someone hit the endpoint with PUT /api/foo, he will get a 500 error, so I wonder if there is a tool for checking non-existed controller action and give me a warning?

1 Like

You want to check for this at compile time ?

It would be great if it can be checked at compile time.

You can try this mix task by running - mix phx.routes_check and let me know if this works for you. Before you use this - code should be placed in <project_root>/lib/mix/tasks/phx.routes_check.ex.

Sample output for the task:

$ mix phx.routes_check
Checking Routes for undefined modules and functions...
Checking Complete
Found following errors:
page_path  GET  /pages/:id                             HelloSurfaceWeb.PageController :id
  - function HelloSurfaceWeb.PageController.id/2 is undefined

test_path  GET  /tests/:id                             HelloSurfaceWeb.TestController :id
  - module HelloSurfaceWeb.TestController is not available

live_path  GET  /hello_demo                            HelloSurfaceWeb.HelloDemo
  - module HelloSurfaceWeb.HelloDemo is not available

Code modified from phx.routes.ex and console_formatter.ex


Edit:
I have moved code to gist. (earlier posted here as embed). Also updated output.

Thank you @03juan for bug fixes.

2 Likes

Code available as gist -
mix phx.routes_check - Before you use this - code should be placed in <project_root>/lib/mix/tasks/phx.routes_check.ex · GitHub .

This gave me back a lot of my existing routes as missing because of the way that /live routes are defined with and without an action (for liveview >= v1.7.5 that I tested this with) . Check the structure for these dummy routes:

[%{
  helper: "nope",
  metadata: %{
    log: :debug,
    phoenix_live_view: {MyappWeb.NopeLive, :index,
     [action: :index, router: MyappWeb.Router],
     %{extra: %{session: %{}}, name: :default, vsn: 1642751438384822700}}
  },
  path: "/nope/index",
  plug: Phoenix.LiveView.Plug,
  plug_opts: :index,
  verb: :get
},
%{
  helper: "live",
  metadata: %{
    log: :debug,
    phoenix_live_view: {MyappWeb.NopeLive, nil,
     [action: nil, router: MyappWeb.Router],
     %{extra: %{session: %{}}, name: :default, vsn: 1642751438384822700}}
  },
  path: "/nope",
  plug: Phoenix.LiveView.Plug,
  plug_opts: MyappWeb.NopeLive,
  verb: :get
}]

I extracted the reduction to its own function. Note the argument to loaded in the :live_module cond, and the route.plug and function_exported?(...) == false checks on the :func cond.

def check_routes(router, endpoint \\ nil) do
  routes =
    Phoenix.Router.routes(router)
    |> Enum.reverse()

  column_widths = calculate_column_widths(routes, endpoint)
  routes = find_missing(routes)

  if Enum.empty?(routes) do
    Mix.shell().info("\nRoutes Check - No errors found")
  else
    routes
    |> Enum.map_join("", &format_msg(&1, column_widths))
    |> Mix.shell().error()
  end
end

defp find_missing(routes) do
  Enum.reduce(routes, [], fn route, acc ->
    cond do
      route.plug == Phoenix.LiveView.Plug and
        is_atom(route.plug_opts) and
          loaded(elem(route.metadata.phoenix_live_view, 0)) == nil ->
        [{:live_module, route} | acc]

      loaded(route.plug) == nil ->
        [{:module, route} | acc]

      route.plug != Phoenix.LiveView.Plug and is_atom(route.plug_opts) and
          function_exported?(route.plug, route.plug_opts, 2) == false ->
        [{:func, route} | acc]

      true ->
        acc
    end
  end)
end
1 Like

Now the question is how to get this to run as a compile time check

Thank you @03juan for testing out and fixing the bugs.

I was testing in multiple projects and I posted stale code. I had used Enum.reject for finding missing routes in earlier version and refactored to Enum.reduce that explains the function_exported?(route.plug, route.plug_opts, 2) == true.

I tested live view routes in an old project with live_view 0.16.4 . It was not working for latest live view like you pointed.

I guess need to implement as a compiler task and include in mix.exs ?

1 Like

The problem with this code is that we’re relying on the internal implementation of the routes, so this will probably break at some earlier version.

Maybe include your earliest working version in the gist?

1 Like

Should it be included in phoenix project ? Add functionality to phx.routes task ?

@chrismccord Any thoughts ?

Updated gist with your changes when I removed the embed.

1 Like