Last active
March 5, 2022 07:22
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Good call 👍
route.plug == Phoenix.LiveView.Plug ->
acc
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
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__()
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you Juan - https://elixirforum.com/u/03juan for bug fixes.