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
@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