Skip to content

Instantly share code, notes, and snippets.

@lkarthee
Last active March 5, 2022 07:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lkarthee/35d1177885a69a494a2f2030e19faca7 to your computer and use it in GitHub Desktop.
Save lkarthee/35d1177885a69a494a2f2030e19faca7 to your computer and use it in GitHub Desktop.
mix phx.routes_check - Before you use this - code should be placed in <project_root>/lib/mix/tasks/phx.routes_check.ex
defmodule Mix.Tasks.Phx.RoutesCheck do
use Mix.Task
@shortdoc "Checks all routes for undefined modules and functions."
@version 1
# Code modified from
# phx.routes.ex -https://github.com/phoenixframework/phoenix/blob/22a71bd339783aa08a727b24ef8822fc5d66a511/lib/mix/tasks/phx.routes.ex#L1)
# console_formatter.ex - https://github.com/phoenixframework/phoenix/blob/22a71bd339783aa08a727b24ef8822fc5d66a511/lib/phoenix/router/console_formatter.ex#L1
@moduledoc """
Checks all routes for undefined modules and functions.
$ mix phx.routes_check
$ mix phx.routes_check MyApp.AnotherRouter
For more information about routers, see phx.routes help:
$ mix help phx.routes
"""
@doc false
def run(args, base \\ Mix.Phoenix.base()) do
Mix.Task.run("compile", args)
{router_mod, opts} =
case OptionParser.parse(args, switches: [endpoint: :string, router: :string]) do
{opts, [passed_router], _} -> {router(passed_router, base), opts}
{opts, [], _} -> {router(opts[:router], base), opts}
end
Mix.shell().info("\nChecking Routes for undefined modules and functions... ")
check_routes(router_mod, endpoint(opts[:endpoint], base))
end
def check_routes(router, endpoint \\ nil) do
routes =
router
|> get_routes()
|> Enum.reverse()
routes = find_missing(routes)
column_widths =
routes
|> Enum.map(fn {_, route} -> route end)
|> calculate_column_widths(endpoint)
if Enum.empty?(routes) do
Mix.shell().info("Checking Complete\nNo errors found")
else
Mix.shell().info("Checking Complete\nFound following errors:")
routes
|> Enum.map_join("", &format_msg(&1, column_widths))
|> Mix.shell().error()
end
end
defp get_routes(router) do
if function_exported?(Phoenix.Router, routes, 1) do
Phoenix.Router.routes(router),
else
router.__routes__()
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]
route.plug == Phoenix.LiveView.Plug ->
acc
loaded(route.plug) == nil ->
[{:module, route} | acc]
is_atom(route.plug_opts) and
function_exported?(route.plug, route.plug_opts, 2) == false ->
[{:func, route} | acc]
true ->
acc
end
end)
end
defp format_msg({error, route}, column_widths) do
msg =
case error do
:live_module ->
" - module #{inspect(route.plug_opts)} is not available\n\n"
:module ->
" - module #{inspect(route.plug)} is not available\n\n"
:func ->
" - function #{inspect(route.plug)}.#{route.plug_opts}/2 is undefined\n\n"
end
format_route(route, column_widths) <> msg
end
defp calculate_column_widths(routes, endpoint) do
sockets = endpoint && endpoint.__sockets__() || []
widths =
Enum.reduce(routes, {0, 0, 0}, fn route, acc ->
%{verb: verb, path: path, helper: helper} = route
verb = verb_name(verb)
{verb_len, path_len, route_name_len} = acc
route_name = route_name(helper)
{max(verb_len, String.length(verb)),
max(path_len, String.length(path)),
max(route_name_len, String.length(route_name))}
end)
Enum.reduce(sockets, widths, fn {path, _mod, _opts}, acc ->
{verb_len, path_len, route_name_len} = acc
{verb_len,
max(path_len, String.length(path <> "/websocket")),
max(route_name_len, String.length("websocket"))}
end)
end
defp format_route(route, column_widths) do
%{verb: verb, path: path, plug: plug, plug_opts: plug_opts, helper: helper} = route
verb = verb_name(verb)
route_name = route_name(helper)
{verb_len, path_len, route_name_len} = column_widths
module_name =
case plug do
Phoenix.LiveView.Plug ->
"#{inspect(plug_opts)}\n"
_ ->
"#{inspect(plug)} #{inspect(plug_opts)}\n"
end
String.pad_leading(route_name, route_name_len) <> " " <>
String.pad_trailing(verb, verb_len) <> " " <>
String.pad_trailing(path, path_len) <> " " <>
module_name
end
defp route_name(nil), do: ""
defp route_name(name), do: name <> "_path"
defp verb_name(verb), do: verb |> to_string() |> String.upcase()
defp endpoint(nil, base) do
loaded(web_mod(base, "Endpoint"))
end
defp endpoint(module, _base) do
loaded(Module.concat([module]))
end
defp router(nil, base) do
if Mix.Project.umbrella?() do
Mix.raise """
umbrella applications require an explicit router to be given to phx.routes, for example:
$ mix phx.routes MyAppWeb.Router
"""
end
web_router = web_mod(base, "Router")
old_router = app_mod(base, "Router")
loaded(web_router) || loaded(old_router) || Mix.raise """
no router found at #{inspect web_router} or #{inspect old_router}.
An explicit router module may be given to phx.routes, for example:
$ mix phx.routes MyAppWeb.Router
"""
end
defp router(router_name, _base) do
arg_router = Module.concat([router_name])
loaded(arg_router) || Mix.raise "the provided router, #{inspect(arg_router)}, does not exist"
end
defp loaded(module) do
if Code.ensure_loaded?(module), do: module
end
defp app_mod(base, name), do: Module.concat([base, name])
defp web_mod(base, name) do
Module.concat(["#{base}Web", name])
end
end
@lkarthee
Copy link
Author

Thank you Juan - https://elixirforum.com/u/03juan for bug fixes.

@03juan
Copy link

03juan commented Jan 21, 2022

Good call 👍

route.plug == Phoenix.LiveView.Plug ->
          acc

@hudsonbay
Copy link

hudsonbay commented Jan 27, 2022

Hi, very nice code. Thank you. I'm trying to implement it but I'm receiving this error. I'm on Elixir 1.11 and Phoenix 1.5.7

> mix phx.routes_check

Checking Routes for undefined modules and functions... 
** (UndefinedFunctionError) function Phoenix.Router.routes/1 is undefined or private. Did you mean one of:

      * route_info/4

    (phoenix 1.5.8) Phoenix.Router.routes(SoulWeb.Router)
    (soul 0.4.2) lib/mix/tasks/phx.routes_check.ex:34: Mix.Tasks.Phx.RoutesCheck.check_routes/2
    (mix 1.12.3) lib/mix/task.ex:394: anonymous fn/3 in Mix.Task.run_task/3
    (mix 1.12.3) lib/mix/cli.ex:84: Mix.CLI.run_task/2
    (elixir 1.12.3) lib/code.ex:1261: Code.require_file/2

@03juan
Copy link

03juan commented Jan 28, 2022

Hi @hudsonbay

You're using phoenix 1.5.8 which doesn't have that function defined, it was introduced in 1.6.0-rc.0

  @doc """
  Returns all routes information from the given router.
  """
  def routes(router) do
    router.__routes__()
  end

You should be able to replace line #37 with router.__routes__()

@03juan
Copy link

03juan commented Jan 28, 2022

@lkarthee

Let's break out line #37 into a get_routes/1

- 37 Phoenix.Router.routes(router)
+ 37 get_routes(router)

+ 57 defp get_routes(router) do
+ 58   if function_exported?(Phoenix.Router, routes, 1),
+ 59     do: Phoenix.Router.routes(router),
+ 60     else: router.__routes__()
+ 61 end

@lkarthee
Copy link
Author

lkarthee commented Mar 5, 2022

@hudsonbay @03juan sorry did not check your comments.

I have made changes as suggested.

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment