Repository: phoenixframework/phoenix
Pull request #4895 (2022-07-22 02:29:28)

Add verified routes
Still needs to support interpolated query string form, as well as moduledocs


Patch:
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 3324a2bdf..5fa0002c8 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -1514,7 +1514,7 @@ defmodule Phoenix.Controller do
   See `current_url/2` to override the default parameters.
   """
   def current_url(%Plug.Conn{} = conn) do
-    Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn)
+    Phoenix.VerifiedRoutes.unverified_url(conn, current_path(conn))
   end
 
   @doc ~S"""
@@ -1551,17 +1551,16 @@ defmodule Phoenix.Controller do
   For example, to get the current URL always in HTTPS format:
 
       def current_secure_url(conn, params \\ %{}) do
-        cur_uri  = MyAppWeb.Endpoint.struct_url()
-        cur_path = Phoenix.Controller.current_path(conn, params)
-
-        MyAppWeb.Router.Helpers.url(%URI{cur_uri | scheme: "https"}) <> cur_path
+        current_uri = MyAppWeb.Endpoint.struct_url()
+        current_path = Phoenix.Controller.current_path(conn, params)
+        Phoenix.VerifiedRoutes.unverified_url(%URI{current_uri | scheme: "https"}, current_path)
       end
 
   However, if you want all generated URLs to always have a certain schema,
   host, etc, you may use `put_router_url/2`.
   """
   def current_url(%Plug.Conn{} = conn, %{} = params) do
-    Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn, params)
+    Phoenix.VerifiedRoutes.unverified_url(conn, current_path(conn, params))
   end
 
   @doc false
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 870ae6091..e8169d381 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -103,8 +103,13 @@ defmodule Phoenix.Router do
   ## Helpers
 
   Phoenix automatically generates a module `Helpers` inside your router
-  which contains named helpers to help developers generate and keep
-  their routes up to date.
+  by default, which contains named helpers to help developers generate and keep
+  their routes up to date. Helpers can be disabled by passing `helpers: false`
+  to `use Phoenix.Router`.
+
+  See the `Phoenix.VerifiedRoutes` documentation for `~p` based route generation
+  which is the preferred way to generate route paths and URLs with compile-time
+  verification.
 
   Helpers are automatically generated based on the controller name.
   For example, the route:
@@ -174,7 +179,7 @@ defmodule Phoenix.Router do
 
   Scopes allow us to scope on any path or even on the helper name:
 
-      scope "/api/v1", MyAppWeb, as: :api_v1 do
+      scope "/v1", MyAppWeb, host: "api." do
         get "/pages/:id", PageController, :show
       end
 
@@ -309,6 +314,7 @@ defmodule Phoenix.Router do
   and you can compose them as necessary on each scope you define.
   """
 
+  # TODO: Deprecate trailing_slash? and remove :as from the docs.
   alias Phoenix.Router.{Resource, Scope, Route, Helpers}
 
   @http_methods [:get, :post, :put, :patch, :delete, :options, :connect, :trace, :head]
@@ -324,10 +330,9 @@ defmodule Phoenix.Router do
 
   defp prelude(opts) do
     quote do
-      @helpers_moduledoc Keyword.get(unquote(opts), :helpers_moduledoc, true)
-
-      Module.register_attribute __MODULE__, :phoenix_routes, accumulate: true
+      Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
       @phoenix_forwards %{}
+      @phoenix_helpers Keyword.get(unquote(opts), :helpers, true)
 
       import Phoenix.Router
 
@@ -396,14 +401,17 @@ defmodule Phoenix.Router do
       _ -> Enum.reduce(pipes, conn, fn pipe, acc -> apply(router, pipe, [acc, []]) end)
     end
   end
+
   def __call__(%{private: %{phoenix_bypass: :all}} = conn, {metadata, prepare, _, _}) do
     prepare.(conn, metadata)
   end
+
   def __call__(conn, {metadata, prepare, pipeline, {plug, opts}}) do
     conn = prepare.(conn, metadata)
     start = System.monotonic_time()
+    measurements = %{system_time: System.system_time()}
     metadata = %{metadata | conn: conn}
-    :telemetry.execute([:phoenix, :router_dispatch, :start], %{system_time: System.system_time()}, metadata)
+    :telemetry.execute([:phoenix, :router_dispatch, :start], measurements, metadata)
 
     case pipeline.(conn) do
       %Plug.Conn{halted: true} = halted_conn ->
@@ -411,6 +419,7 @@ defmodule Phoenix.Router do
         metadata = %{metadata | conn: halted_conn}
         :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
         halted_conn
+
       %Plug.Conn{} = piped_conn ->
         try do
           plug.call(piped_conn, plug.init(opts))
@@ -423,13 +432,15 @@ defmodule Phoenix.Router do
         rescue
           e in Plug.Conn.WrapperError ->
             measurements = %{duration: System.monotonic_time() - start}
-            metadata = Map.merge(metadata, %{conn: conn, kind: :error, reason: e, stacktrace: __STACKTRACE__})
+            new_metadata = %{conn: conn, kind: :error, reason: e, stacktrace: __STACKTRACE__}
+            metadata = Map.merge(metadata, new_metadata)
             :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata)
             Plug.Conn.WrapperError.reraise(e)
         catch
           kind, reason ->
             measurements = %{duration: System.monotonic_time() - start}
-            metadata = Map.merge(metadata, %{conn: conn, kind: kind, reason: reason, stacktrace: __STACKTRACE__})
+            new_metadata = %{conn: conn, kind: kind, reason: reason, stacktrace: __STACKTRACE__}
+            metadata = Map.merge(metadata, new_metadata)
             :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata)
             Plug.Conn.WrapperError.reraise(piped_conn, kind, reason, __STACKTRACE__)
         end
@@ -463,9 +474,11 @@ defmodule Phoenix.Router do
               raise MalformedURIError, "malformed URI path: #{inspect conn.request_path}"
           end
 
-        case __match_route__(method, decoded, host) do
-          :error -> raise NoRouteError, conn: conn, router: __MODULE__
-          match -> Phoenix.Router.__call__(conn, match)
+        with {_forward_plug, _warn_on_verify?, fun} <- __match_route__(decoded),
+             {_, _, _, _} = match <- fun.(method, host) do
+          Phoenix.Router.__call__(conn, match)
+        else
+          _ -> raise NoRouteError, conn: conn, router: __MODULE__
         end
       end
 
@@ -475,25 +488,41 @@ defmodule Phoenix.Router do
 
   @doc false
   defmacro __before_compile__(env) do
-    routes = env.module |> Module.get_attribute(:phoenix_routes) |> Enum.reverse
-    routes_with_exprs = Enum.map(routes, &{&1, Route.exprs(&1)})
+    routes = env.module |> Module.get_attribute(:phoenix_routes) |> Enum.reverse()
+    forwards = env.module |> Module.get_attribute(:phoenix_forwards)
+    routes_with_exprs = Enum.map(routes, &{&1, Route.exprs(&1, forwards)})
+    helpers? = Module.get_attribute(env.module, :phoenix_helpers)
 
-    helpers_moduledoc = Module.get_attribute(env.module, :helpers_moduledoc)
+    if helpers?, do: Helpers.define(env, routes_with_exprs)
 
-    Helpers.define(env, routes_with_exprs, docs: helpers_moduledoc)
-    {matches, _} = Enum.map_reduce(routes_with_exprs, %{}, &build_match/2)
+    group = Enum.group_by(routes_with_exprs, fn {_route, exprs} -> exprs.path end)
+    {matches, _} = Enum.flat_map_reduce(routes_with_exprs, {group, %{}}, &build_match/2)
 
-    checks =
-      for %{line: line, plug: plug, plug_opts: plug_opts} <- routes, into: %{} do
-        quote line: line do
-          {unquote(plug).init(unquote(Macro.escape(plug_opts))), []}
+    match_catch_all =
+      quote generated: true do
+        @doc false
+        def __match_route__(_path_info) do
+          :error
         end
       end
 
-    match_404 =
-      quote [generated: true] do
-        def __match_route__(_method, _path_info, _host) do
-          :error
+    forwards =
+      for {plug, script_name} <- forwards do
+        quote do
+          def __forward__(unquote(plug)), do: unquote(script_name)
+        end
+      end
+
+    forward_catch_all =
+      quote generated: true do
+        @doc false
+        def __forward__(_), do: nil
+      end
+
+    checks =
+      for %{line: line, plug: plug} <- routes, into: %{} do
+        quote line: line do
+          {_ = &unquote(plug).init/1, []}
         end
       end
 
@@ -502,65 +531,101 @@ defmodule Phoenix.Router do
 
     quote do
       @doc false
-      def __routes__,  do: unquote(Macro.escape(routes))
+      def __routes__, do: unquote(Macro.escape(routes))
 
       @doc false
       def __checks__, do: unquote({:__block__, [], Map.keys(checks)})
 
       @doc false
-      def __helpers__, do: __MODULE__.Helpers
+      if unquote(helpers?) do
+        def __helpers__, do: __MODULE__.Helpers
+      else
+        def __helpers__, do: nil
+      end
 
       defp prepare(conn) do
-        merge_private(
-          conn,
-          [
-            {:phoenix_router, __MODULE__},
-            {__MODULE__, {conn.script_name, @phoenix_forwards}}
-          ]
-        )
+        merge_private(conn, [{:phoenix_router, __MODULE__}, {__MODULE__, conn.script_name}])
       end
 
       unquote(matches)
-      unquote(match_404)
+      unquote(match_catch_all)
+      unquote(forwards)
+      unquote(forward_catch_all)
+    end
+  end
+
+  defp build_match({route, expr}, {groups, known_pipes}) do
+    # We need to process the routes in the order they are defined
+    # while grouping them. So we keep the original route ordering
+    # and unpack the groups.
+    {grouped_routes_with_exprs, groups} = Map.pop(groups, expr.path)
+
+    if grouped_routes_with_exprs do
+      {clauses, pipes, known_pipes} =
+        Enum.reduce(grouped_routes_with_exprs, {[], [], known_pipes}, &build_match_group/2)
+
+      forward_plug =
+        Enum.find_value(grouped_routes_with_exprs, fn
+          {%{kind: :forward, plug: plug}, _expr} -> plug
+          {_, _} -> nil
+        end)
+
+      catch_all =
+        quote generated: true do
+          _, _ -> :error
+        end
+
+      block =
+        quote line: route.line do
+          unquote_splicing(pipes)
+
+          def __match_route__(unquote(expr.path)) do
+            {unquote(forward_plug), unquote(route.warn_on_verify?), unquote({:fn, [], Enum.reverse(clauses, catch_all)})}
+          end
+        end
+
+      {[block], {groups, known_pipes}}
+    else
+      {[], {groups, known_pipes}}
     end
   end
 
-  defp build_match({route, exprs}, known_pipelines) do
+  defp build_match_pipes(route, acc_pipes, known_pipes) do
     %{pipe_through: pipe_through} = route
 
+    case known_pipes do
+      %{^pipe_through => name} ->
+        {name, acc_pipes, known_pipes}
+
+      %{} ->
+        name = :"__pipe_through#{map_size(known_pipes)}__"
+        acc_pipes = [build_pipes(name, pipe_through) | acc_pipes]
+        known_pipes = Map.put(known_pipes, pipe_through, name)
+        {name, acc_pipes, known_pipes}
+    end
+  end
+
+  defp build_match_group({route, expr}, {acc_clauses, acc_pipes, known_pipes}) do
+    {pipe_name, acc_pipes, known_pipes} = build_match_pipes(route, acc_pipes, known_pipes)
+
     %{
       prepare: prepare,
       dispatch: dispatch,
       verb_match: verb_match,
       path_params: path_params,
-      path: path,
       host: host
-    } = exprs
-
-    {pipe_name, pipe_definition, known_pipelines} =
-      case known_pipelines do
-        %{^pipe_through => name} ->
-          {name, :ok, known_pipelines}
-
-        %{} ->
-          name = :"__pipe_through#{map_size(known_pipelines)}__"
-          {name, build_pipes(name, pipe_through), Map.put(known_pipelines, pipe_through, name)}
-      end
-
-    quoted =
-      quote line: route.line do
-        unquote(pipe_definition)
+    } = expr
 
-        @doc false
-        def __match_route__(unquote(verb_match), unquote(path), unquote(host)) do
+    [clause] =
+      quote do
+        unquote(verb_match), unquote(host) ->
           {unquote(build_metadata(route, path_params)),
            fn var!(conn, :conn), %{path_params: var!(path_params, :conn)} -> unquote(prepare) end,
            &unquote(Macro.var(pipe_name, __MODULE__))/1,
            unquote(dispatch)}
-        end
       end
 
-    {quoted, known_pipelines}
+    {[clause | acc_clauses], acc_pipes, known_pipes}
   end
 
   defp build_metadata(route, path_params) do
@@ -615,15 +680,15 @@ defmodule Phoenix.Router do
       Defaults to true, disables scoping if false.
     * `:log` - the level to log the route dispatching under,
       may be set to false. Defaults to `:debug`
-    * `:host` - a string containing the host scope, or prefix host scope,
-      ie `"foo.bar.com"`, `"foo."`
     * `:private` - a map of private data to merge into the connection
       when a route matches
     * `:assigns` - a map of data to merge into the connection when a route matches
     * `:metadata` - a map of metadata used by the telemetry events and returned by
       `route_info/4`
-    * `:trailing_slash` - a boolean to flag whether or not the helper functions
-      append a trailing slash. Defaults to `false`.
+    * `:warn_on_verify` - the boolean for whether matches to this route trigger
+      an unmatched route warning for `Phoenix.VerifiedRoutes`. Useful to ignore
+      an otherwise catch-all route definition from being matched when verifying routes.
+      Defaults `true`.
 
   ## Examples
 
@@ -914,7 +979,7 @@ defmodule Phoenix.Router do
 
   ## Examples
 
-      scope path: "/api/v1", as: :api_v1, alias: API.V1 do
+      scope path: "/api/v1", alias: API.V1 do
         get "/pages/:id", PageController, :show
       end
 
@@ -937,8 +1002,6 @@ defmodule Phoenix.Router do
     * `:assigns` - a map of data to merge into the connection when a route matches
     * `:log` - the level to log the route dispatching under,
       may be set to false. Defaults to `:debug`
-    * `:trailing_slash` - whether or not the helper functions append a trailing
-      slash. Defaults to `false`.
 
   """
   defmacro scope(options, do: context) do
@@ -963,7 +1026,7 @@ defmodule Phoenix.Router do
 
   ## Examples
 
-      scope "/api/v1", as: :api_v1 do
+      scope "/v1", host: "api." do
         get "/pages/:id", PageController, :show
       end
 
@@ -1000,7 +1063,7 @@ defmodule Phoenix.Router do
 
   ## Examples
 
-      scope "/api/v1", API.V1, as: :api_v1 do
+      scope "/v1", API.V1, host: "api." do
         get "/pages/:id", PageController, :show
       end
 
@@ -1072,7 +1135,6 @@ defmodule Phoenix.Router do
     router_opts = Keyword.put(router_opts, :as, nil)
 
     quote unquote: true, bind_quoted: [path: path, plug: plug] do
-      plug = Scope.register_forwards(__MODULE__, path, plug)
       unquote(add_route(:forward, :*, path, plug, plug_opts, router_opts))
     end
   end
@@ -1119,9 +1181,9 @@ defmodule Phoenix.Router do
   end
 
   def route_info(router, method, split_path, host) when is_list(split_path) do
-    case router.__match_route__(method, split_path, host) do
-      {%{} = metadata, _prepare, _pipeline, {_plug, _opts}} -> Map.delete(metadata, :conn)
-      :error -> :error
+    with {_forward_plug, _warn_on_verify?, fun} <- router.__match_route__(split_path),
+         {metadata, _prepare, _pipeline, {_plug, _opts}} <- fun.(method, host) do
+      Map.delete(metadata, :conn)
     end
   end
 end
diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex
index 7e65dfaaf..97bd2cec9 100644
--- a/lib/phoenix/router/helpers.ex
+++ b/lib/phoenix/router/helpers.ex
@@ -5,96 +5,10 @@ defmodule Phoenix.Router.Helpers do
   alias Phoenix.Router.Route
   alias Plug.Conn
 
-  @doc """
-  Callback invoked by the url generated in each helper module.
-  """
-  def url(_router, %Conn{private: private}) do
-    case private do
-      %{phoenix_router_url: url} when is_binary(url) -> url
-      %{phoenix_endpoint: endpoint} -> endpoint.url()
-    end
-  end
-
-  def url(_router, %_{endpoint: endpoint}) do
-    endpoint.url()
-  end
-
-  def url(_router, %URI{} = uri) do
-    URI.to_string(%{uri | path: nil})
-  end
-
-  def url(_router, endpoint) when is_atom(endpoint) do
-    endpoint.url()
-  end
-
-  def url(router, other) do
-    raise ArgumentError,
-      "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
-      "or a Phoenix.Endpoint when building url for #{inspect(router)}, got: #{inspect(other)}"
-  end
-
-  @doc """
-  Callback invoked by path generated in each helper module.
-  """
-  def path(router, %Conn{} = conn, path) do
-    conn
-    |> build_own_forward_path(router, path)
-    |> Kernel.||(build_conn_forward_path(conn, router, path))
-    |> Kernel.||(path_with_script(path, conn.script_name))
-  end
-
-  def path(_router, %URI{} = uri, path) do
-    (uri.path || "") <> path
-  end
-
-  def path(_router, %_{endpoint: endpoint}, path) do
-    endpoint.path(path)
-  end
-
-  def path(_router, endpoint, path) when is_atom(endpoint) do
-    endpoint.path(path)
-  end
-
-  def path(router, other, _path) do
-    raise ArgumentError,
-      "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
-      "or a Phoenix.Endpoint when building path for #{inspect(router)}, got: #{inspect(other)}"
-  end
-
-  ## Helpers
-
-  defp build_own_forward_path(conn, router, path) do
-    case Map.fetch(conn.private, router) do
-      {:ok, {local_script, _}} ->
-        path_with_script(path, local_script)
-      :error -> nil
-    end
-  end
-
-  defp build_conn_forward_path(%Conn{private: %{phoenix_router: phx_router}} = conn, router, path) do
-    case Map.fetch(conn.private, phx_router) do
-      {:ok, {script_name, forwards}} ->
-        case Map.fetch(forwards, router) do
-          {:ok, local_script} ->
-            path_with_script(path, script_name ++ local_script)
-          :error -> nil
-        end
-      :error -> nil
-    end
-  end
-  defp build_conn_forward_path(_conn, _router, _path), do: nil
-
-  defp path_with_script(path, []) do
-    path
-  end
-  defp path_with_script(path, script) do
-    "/" <> Enum.join(script, "/") <> path
-  end
-
   @doc """
   Generates the helper module for the given environment and routes.
   """
-  def define(env, routes, opts \\ []) do
+  def define(env, routes) do
     # Ignore any route without helper or forwards.
     routes =
       Enum.reject(routes, fn {route, _exprs} ->
@@ -114,75 +28,128 @@ defmodule Phoenix.Router.Helpers do
 
     catch_all = Enum.map(groups, &defhelper_catch_all/1)
 
-    defhelper = quote [generated: true, unquote: false] do
-      defhelper = fn helper, vars, opts, bins, segs, trailing_slash? ->
-        def unquote(:"#{helper}_path")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars)) do
-          unquote(:"#{helper}_path")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars), [])
-        end
-
-        def unquote(:"#{helper}_path")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars), params)
-            when is_list(params) or is_map(params) do
-          path(conn_or_endpoint, segments(unquote(segs), params, unquote(bins), unquote(trailing_slash?),
-                {unquote(helper), unquote(Macro.escape(opts)), unquote(Enum.map(vars, &Macro.to_string/1))}))
-        end
-
-        def unquote(:"#{helper}_url")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars)) do
-          unquote(:"#{helper}_url")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars), [])
-        end
-
-        def unquote(:"#{helper}_url")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars), params)
-            when is_list(params) or is_map(params) do
-          url(conn_or_endpoint) <> unquote(:"#{helper}_path")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars), params)
-        end
-      end
-    end
+    defhelper =
+      quote generated: true, unquote: false do
+        defhelper = fn helper, vars, opts, bins, segs, trailing_slash? ->
+          def unquote(:"#{helper}_path")(
+                conn_or_endpoint,
+                unquote(Macro.escape(opts)),
+                unquote_splicing(vars)
+              ) do
+            unquote(:"#{helper}_path")(
+              conn_or_endpoint,
+              unquote(Macro.escape(opts)),
+              unquote_splicing(vars),
+              []
+            )
+          end
 
-    defcatch_all = quote [generated: true, unquote: false] do
-      defcatch_all = fn helper, binding_lengths, params_lengths, routes ->
-	      for length <- binding_lengths do
-	        binding = List.duplicate({:_, [], nil}, length)
-	        arity = length + 2
+          def unquote(:"#{helper}_path")(
+                conn_or_endpoint,
+                unquote(Macro.escape(opts)),
+                unquote_splicing(vars),
+                params
+              )
+              when is_list(params) or is_map(params) do
+            path(
+              conn_or_endpoint,
+              segments(
+                unquote(segs),
+                params,
+                unquote(bins),
+                unquote(trailing_slash?),
+                {unquote(helper), unquote(Macro.escape(opts)),
+                 unquote(Enum.map(vars, &Macro.to_string/1))}
+              )
+            )
+          end
 
-          def unquote(:"#{helper}_path")(conn_or_endpoint, action, unquote_splicing(binding)) do
-            path(conn_or_endpoint, "/")
-            raise_route_error(unquote(helper), :path, unquote(arity), action, [])
+          def unquote(:"#{helper}_url")(
+                conn_or_endpoint,
+                unquote(Macro.escape(opts)),
+                unquote_splicing(vars)
+              ) do
+            unquote(:"#{helper}_url")(
+              conn_or_endpoint,
+              unquote(Macro.escape(opts)),
+              unquote_splicing(vars),
+              []
+            )
           end
 
-          def unquote(:"#{helper}_url")(conn_or_endpoint, action, unquote_splicing(binding)) do
-            url(conn_or_endpoint)
-            raise_route_error(unquote(helper), :url, unquote(arity), action, [])
+          def unquote(:"#{helper}_url")(
+                conn_or_endpoint,
+                unquote(Macro.escape(opts)),
+                unquote_splicing(vars),
+                params
+              )
+              when is_list(params) or is_map(params) do
+            url(conn_or_endpoint) <>
+              unquote(:"#{helper}_path")(
+                conn_or_endpoint,
+                unquote(Macro.escape(opts)),
+                unquote_splicing(vars),
+                params
+              )
           end
         end
+      end
 
-        for length <- params_lengths do
-          binding = List.duplicate({:_, [], nil}, length)
-          arity = length + 2
-
-          def unquote(:"#{helper}_path")(conn_or_endpoint, action, unquote_splicing(binding), params) do
-            path(conn_or_endpoint, "/")
-            raise_route_error(unquote(helper), :path, unquote(arity + 1), action, params)
+    defcatch_all =
+      quote generated: true, unquote: false do
+        defcatch_all = fn helper, binding_lengths, params_lengths, routes ->
+          for length <- binding_lengths do
+            binding = List.duplicate({:_, [], nil}, length)
+            arity = length + 2
+
+            def unquote(:"#{helper}_path")(conn_or_endpoint, action, unquote_splicing(binding)) do
+              path(conn_or_endpoint, "/")
+              raise_route_error(unquote(helper), :path, unquote(arity), action, [])
+            end
+
+            def unquote(:"#{helper}_url")(conn_or_endpoint, action, unquote_splicing(binding)) do
+              url(conn_or_endpoint)
+              raise_route_error(unquote(helper), :url, unquote(arity), action, [])
+            end
           end
 
-          def unquote(:"#{helper}_url")(conn_or_endpoint, action, unquote_splicing(binding), params) do
-            url(conn_or_endpoint)
-            raise_route_error(unquote(helper), :url, unquote(arity + 1), action, params)
+          for length <- params_lengths do
+            binding = List.duplicate({:_, [], nil}, length)
+            arity = length + 2
+
+            def unquote(:"#{helper}_path")(
+                  conn_or_endpoint,
+                  action,
+                  unquote_splicing(binding),
+                  params
+                ) do
+              path(conn_or_endpoint, "/")
+              raise_route_error(unquote(helper), :path, unquote(arity + 1), action, params)
+            end
+
+            def unquote(:"#{helper}_url")(
+                  conn_or_endpoint,
+                  action,
+                  unquote_splicing(binding),
+                  params
+                ) do
+              url(conn_or_endpoint)
+              raise_route_error(unquote(helper), :url, unquote(arity + 1), action, params)
+            end
           end
-        end
 
-        defp raise_route_error(unquote(helper), suffix, arity, action, params) do
-          Phoenix.Router.Helpers.raise_route_error(
-            __MODULE__,
-            "#{unquote(helper)}_#{suffix}",
-            arity,
-            action,
-            unquote(Macro.escape(routes)),
-            params
-          )
+          defp raise_route_error(unquote(helper), suffix, arity, action, params) do
+            Phoenix.Router.Helpers.raise_route_error(
+              __MODULE__,
+              "#{unquote(helper)}_#{suffix}",
+              arity,
+              action,
+              unquote(Macro.escape(routes)),
+              params
+            )
+          end
         end
       end
-    end
-
-    docs = Keyword.get(opts, :docs, true)
 
     # It is in general bad practice to generate large chunks of code
     # inside quoted expressions. However, we can get away with this
@@ -193,110 +160,83 @@ defmodule Phoenix.Router.Helpers do
     # * We inline most of the code for performance, so it is specific
     #   per helper module anyway.
     #
-    code = quote do
-      @moduledoc unquote(docs) && """
-      Module with named helpers generated from #{inspect unquote(env.module)}.
-      """
-      unquote(defhelper)
-      unquote(defcatch_all)
-      unquote_splicing(impls)
-      unquote_splicing(catch_all)
-
-      @doc """
-      Generates the path information including any necessary prefix.
-      """
-      def path(data, path) do
-        Phoenix.Router.Helpers.path(unquote(env.module), data, path)
-      end
-
-      @doc """
-      Generates the connection/endpoint base URL without any path information.
-      """
-      def url(data) do
-        Phoenix.Router.Helpers.url(unquote(env.module), data)
-      end
-
-      @doc """
-      Generates path to a static asset given its file path.
-      """
-      def static_path(%Conn{private: private} = conn, path) do
-        private.phoenix_endpoint.static_path(path)
-      end
-
-      def static_path(%_{endpoint: endpoint} = conn, path) do
-        endpoint.static_path(path)
-      end
-
-      def static_path(endpoint, path) when is_atom(endpoint) do
-        endpoint.static_path(path)
-      end
-
-      @doc """
-      Generates url to a static asset given its file path.
-      """
-      def static_url(%Conn{private: private}, path) do
-        case private do
-          %{phoenix_static_url: url} when is_binary(url) -> url <> path
-          %{phoenix_endpoint: endpoint} -> static_url(endpoint, path)
+    code =
+      quote do
+        @moduledoc false
+        unquote(defhelper)
+        unquote(defcatch_all)
+        unquote_splicing(impls)
+        unquote_splicing(catch_all)
+
+        @doc """
+        Generates the path information including any necessary prefix.
+        """
+        def path(data, path) do
+          Phoenix.VerifiedRoutes.unverified_path(data, unquote(env.module), path)
         end
-      end
 
-      def static_url(%_{endpoint: endpoint} = conn, path) do
-        static_url(endpoint, path)
-      end
-
-      def static_url(endpoint, path) when is_atom(endpoint) do
-        endpoint.static_url() <> endpoint.static_path(path)
-      end
+        @doc """
+        Generates the connection/endpoint base URL without any path information.
+        """
+        def url(data) do
+          Phoenix.VerifiedRoutes.unverified_url(data, "")
+        end
 
-      @doc """
-      Generates an integrity hash to a static asset given its file path.
-      """
-      def static_integrity(%Conn{private: %{phoenix_endpoint: endpoint}}, path) do
-        static_integrity(endpoint, path)
-      end
+        @doc """
+        Generates path to a static asset given its file path.
+        """
+        def static_path(conn_or_endpoint_ctx, path) do
+          Phoenix.VerifiedRoutes.static_path(conn_or_endpoint_ctx, path)
+        end
 
-      def static_integrity(%_{endpoint: endpoint}, path) do
-        static_integrity(endpoint, path)
-      end
+        @doc """
+        Generates url to a static asset given its file path.
+        """
+        def static_url(conn_or_endpoint_ctx, path) do
+          Phoenix.VerifiedRoutes.static_url(conn_or_endpoint_ctx, path)
+        end
 
-      def static_integrity(endpoint, path) when is_atom(endpoint) do
-        endpoint.static_integrity(path)
-      end
+        @doc """
+        Generates an integrity hash to a static asset given its file path.
+        """
+        def static_integrity(conn_or_endpoint_ctx, path) do
+          Phoenix.VerifiedRoutes.static_integrity(conn_or_endpoint_ctx, path)
+        end
 
-      # Functions used by generated helpers
-      # Those are inlined here for performance
+        # Functions used by generated helpers
+        # Those are inlined here for performance
 
-      defp to_param(int) when is_integer(int), do: Integer.to_string(int)
-      defp to_param(bin) when is_binary(bin), do: bin
-      defp to_param(false), do: "false"
-      defp to_param(true), do: "true"
-      defp to_param(data), do: Phoenix.Param.to_param(data)
+        defp to_param(int) when is_integer(int), do: Integer.to_string(int)
+        defp to_param(bin) when is_binary(bin), do: bin
+        defp to_param(false), do: "false"
+        defp to_param(true), do: "true"
+        defp to_param(data), do: Phoenix.Param.to_param(data)
 
-      defp segments(segments, [], _reserved, trailing_slash?, _opts) do
-        maybe_append_slash(segments, trailing_slash?)
-      end
+        defp segments(segments, [], _reserved, trailing_slash?, _opts) do
+          maybe_append_slash(segments, trailing_slash?)
+        end
 
-      defp segments(segments, query, reserved, trailing_slash?, _opts) when is_list(query) or is_map(query) do
-        dict = for {k, v} <- query,
-               not ((k = to_string(k)) in reserved),
-               do: {k, v}
+        defp segments(segments, query, reserved, trailing_slash?, _opts)
+             when is_list(query) or is_map(query) do
+          dict =
+            for {k, v} <- query,
+                (k = to_string(k)) not in reserved,
+                do: {k, v}
 
+          case Conn.Query.encode(dict, &to_param/1) do
+            "" -> maybe_append_slash(segments, trailing_slash?)
+            o -> maybe_append_slash(segments, trailing_slash?) <> "?" <> o
+          end
+        end
 
-        case Conn.Query.encode dict, &to_param/1 do
-          "" -> maybe_append_slash(segments, trailing_slash?)
-          o  -> maybe_append_slash(segments, trailing_slash?) <> "?" <> o
+        if unquote(trailing_slash?) do
+          defp maybe_append_slash("/", _), do: "/"
+          defp maybe_append_slash(path, true), do: path <> "/"
         end
-      end
 
-      if unquote(trailing_slash?) do
-        defp maybe_append_slash("/", _), do: "/"
-        defp maybe_append_slash(path, true), do: path <> "/"
+        defp maybe_append_slash(path, _), do: path
       end
 
-      defp maybe_append_slash(path, _), do: path
-    end
-
     Module.create(Module.concat(env.module, Helpers), code, line: env.line, file: env.file)
   end
 
@@ -328,13 +268,15 @@ defmodule Phoenix.Router.Helpers do
   def defhelper_catch_all({helper, routes_and_exprs}) do
     routes =
       routes_and_exprs
-      |> Enum.map(fn {routes, exprs} -> {routes.plug_opts, Enum.map(exprs.binding, &elem(&1, 0))} end)
+      |> Enum.map(fn {routes, exprs} ->
+        {routes.plug_opts, Enum.map(exprs.binding, &elem(&1, 0))}
+      end)
       |> Enum.sort()
 
     params_lengths =
-	    routes
-	    |> Enum.map(fn {_, bindings} -> length(bindings) end)
-	    |> Enum.uniq()
+      routes
+      |> Enum.map(fn {_, bindings} -> length(bindings) end)
+      |> Enum.uniq()
 
     # Each helper defines catch alls like this:
     #
@@ -344,8 +286,7 @@ defmodule Phoenix.Router.Helpers do
     # Given the helpers are ordered by binding length, the additional
     # helper with param for a helper_path/n will always override the
     # binding for helper_path/n+1, so we skip those here to avoid warnings.
-    binding_lengths =
-      Enum.reject(params_lengths, &(&1 - 1 in params_lengths))
+    binding_lengths = Enum.reject(params_lengths, &((&1 - 1) in params_lengths))
 
     quote do
       defcatch_all.(
@@ -363,11 +304,11 @@ defmodule Phoenix.Router.Helpers do
   def raise_route_error(mod, fun, arity, action, routes, params) do
     cond do
       is_atom(action) and not Keyword.has_key?(routes, action) ->
-        "no action #{inspect action} for #{inspect mod}.#{fun}/#{arity}"
+        "no action #{inspect(action)} for #{inspect(mod)}.#{fun}/#{arity}"
         |> invalid_route_error(fun, routes)
 
       is_list(params) or is_map(params) ->
-        "no function clause for #{inspect mod}.#{fun}/#{arity} and action #{inspect action}"
+        "no function clause for #{inspect(mod)}.#{fun}/#{arity} and action #{inspect(action)}"
         |> invalid_route_error(fun, routes)
 
       true ->
@@ -382,7 +323,8 @@ defmodule Phoenix.Router.Helpers do
         "\n    #{fun}(conn_or_endpoint, #{bindings}, params \\\\ [])"
       end
 
-    raise ArgumentError, "#{prelude}. The following actions/clauses are supported:\n#{suggestions}"
+    raise ArgumentError,
+          "#{prelude}. The following actions/clauses are supported:\n#{suggestions}"
   end
 
   defp invalid_param_error(mod, fun, arity, action, routes) do
@@ -416,13 +358,22 @@ defmodule Phoenix.Router.Helpers do
   end
 
   defp expand_segments([{:|, _, [h, t]}], acc),
-    do: quote(do: unquote(expand_segments([h], acc)) <> "/" <> Enum.map_join(unquote(t), "/", &unquote(__MODULE__).encode_param/1))
+    do:
+      quote(
+        do:
+          unquote(expand_segments([h], acc)) <>
+            "/" <> Enum.map_join(unquote(t), "/", &unquote(__MODULE__).encode_param/1)
+      )
 
-  defp expand_segments([h|t], acc) when is_binary(h),
+  defp expand_segments([h | t], acc) when is_binary(h),
     do: expand_segments(t, quote(do: unquote(acc) <> unquote("/" <> h)))
 
-  defp expand_segments([h|t], acc),
-    do: expand_segments(t, quote(do: unquote(acc) <> "/" <> unquote(__MODULE__).encode_param(to_param(unquote(h)))))
+  defp expand_segments([h | t], acc),
+    do:
+      expand_segments(
+        t,
+        quote(do: unquote(acc) <> "/" <> unquote(__MODULE__).encode_param(to_param(unquote(h))))
+      )
 
   defp expand_segments([], acc),
     do: acc
diff --git a/lib/phoenix/router/route.ex b/lib/phoenix/router/route.ex
index f24bed084..0fb1593bd 100644
--- a/lib/phoenix/router/route.ex
+++ b/lib/phoenix/router/route.ex
@@ -26,7 +26,7 @@ defmodule Phoenix.Router.Route do
 
   defstruct [:verb, :line, :kind, :path, :host, :plug, :plug_opts,
              :helper, :private, :pipe_through, :assigns, :metadata,
-             :trailing_slash?]
+             :trailing_slash?, :warn_on_verify?]
 
   @type t :: %Route{}
 
@@ -46,8 +46,8 @@ defmodule Phoenix.Router.Route do
   Receives the verb, path, plug, options and helper
   and returns a `Phoenix.Router.Route` struct.
   """
-  @spec build(non_neg_integer, :match | :forward, atom, String.t, String.t | nil, atom, atom, atom | nil, list(atom), map, map, map, boolean) :: t
-  def build(line, kind, verb, path, host, plug, plug_opts, helper, pipe_through, private, assigns, metadata, trailing_slash?)
+  @spec build(non_neg_integer, :match | :forward, atom, String.t, String.t | nil, atom, atom, atom | nil, list(atom), map, map, map, boolean, boolean) :: t
+  def build(line, kind, verb, path, host, plug, plug_opts, helper, pipe_through, private, assigns, metadata, trailing_slash?, warn_on_verify?)
       when is_atom(verb) and (is_binary(host) or is_nil(host)) and
            is_atom(plug) and (is_binary(helper) or is_nil(helper)) and
            is_list(pipe_through) and is_map(private) and is_map(assigns) and
@@ -56,23 +56,23 @@ defmodule Phoenix.Router.Route do
     %Route{kind: kind, verb: verb, path: path, host: host, private: private,
            plug: plug, plug_opts: plug_opts, helper: helper,
            pipe_through: pipe_through, assigns: assigns, line: line, metadata: metadata,
-           trailing_slash?: trailing_slash?}
+           trailing_slash?: trailing_slash?, warn_on_verify?: warn_on_verify?}
   end
 
   @doc """
   Builds the compiled expressions used by the route.
   """
-  def exprs(route) do
+  def exprs(route, forwards) do
     {path, binding} = build_path_and_binding(route)
 
     %{
       path: path,
-      host: build_host(route.host),
-      verb_match: verb_match(route.verb),
       binding: binding,
-      prepare: build_prepare(route),
+      dispatch: build_dispatch(route, forwards),
+      host: Plug.Router.Utils.build_host_match(route.host),
       path_params: build_path_params(binding),
-      dispatch: build_dispatch(route)
+      prepare: build_prepare(route),
+      verb_match: verb_match(route.verb)
     }
   end
 
@@ -82,24 +82,28 @@ defmodule Phoenix.Router.Route do
   defp build_path_params(binding), do: {:%{}, [], binding}
 
   defp build_path_and_binding(%Route{path: path} = route) do
-    {params, segments} = case route.kind do
-      :forward -> Plug.Router.Utils.build_path_match(path <> "/*_forward_path_info")
-      :match   -> Plug.Router.Utils.build_path_match(path)
-    end
-
-    binding = for var <- params, var != :_forward_path_info do
-      {Atom.to_string(var), Macro.var(var, nil)}
-    end
+    {_params, segments} =
+      case route.kind do
+        :forward -> Plug.Router.Utils.build_path_match(path <> "/*_forward_path_info")
+        :match   -> Plug.Router.Utils.build_path_match(path)
+      end
 
-    {segments, binding}
+    rewrite_segments(segments)
   end
 
-  defp build_host(host) do
-    cond do
-      is_nil(host)             -> quote do: _
-      String.last(host) == "." -> quote do: unquote(host) <> _
-      true                     -> host
-    end
+  # We rewrite segments to use consistent variable naming as we want to group routes later on.
+  defp rewrite_segments(segments) do
+    {segments, {binding, _counter}} =
+      Macro.prewalk(segments, {[], 0}, fn
+        {name, _meta, nil}, {binding, counter} when is_atom(name) and name != :_forward_path_info ->
+          var = Macro.var(:"arg#{counter}", __MODULE__)
+          {var, {[{Atom.to_string(name), var} | binding], counter + 1}}
+
+        other, acc ->
+          {other, acc}
+      end)
+
+    {segments, Enum.reverse(binding)}
   end
 
   defp build_prepare(route) do
@@ -122,28 +126,29 @@ defmodule Phoenix.Router.Route do
     end
   end
 
-  defp build_dispatch(%Route{kind: :forward} = route) do
-    {_params, fwd_segments} = Plug.Router.Utils.build_path_match(route.path)
+  defp build_prepare_expr(_key, data) when data == %{}, do: {[], []}
 
-    quote do
-      {
-        Phoenix.Router.Route,
-        {unquote(fwd_segments), unquote(route.plug), unquote(Macro.escape(route.plug_opts))}
-      }
-    end
+  defp build_prepare_expr(key, data) do
+    var = Macro.var(key, :conn)
+    merge = quote(do: Map.merge(unquote(var), unquote(Macro.escape(data))))
+    {[{key, var}], [{key, merge}]}
   end
 
-  defp build_dispatch(%Route{} = route) do
+  defp build_dispatch(%Route{kind: :match, plug: plug, plug_opts: plug_opts}, _forwards) do
     quote do
-      {unquote(route.plug), unquote(Macro.escape(route.plug_opts))}
+      {unquote(plug), unquote(Macro.escape(plug_opts))}
     end
   end
 
-  defp build_prepare_expr(_key, data) when data == %{}, do: {[], []}
-  defp build_prepare_expr(key, data) do
-    var = Macro.var(key, :conn)
-    merge = quote(do: Map.merge(unquote(var), unquote(Macro.escape(data))))
-    {[{key, var}], [{key, merge}]}
+  defp build_dispatch(%Route{kind: :forward, plug: plug, plug_opts: plug_opts}, forwards) do
+    segments = Map.fetch!(forwards, plug)
+
+    quote do
+      {
+        Phoenix.Router.Route,
+        {unquote(segments), unquote(plug), unquote(Macro.escape(plug_opts))}
+      }
+    end
   end
 
   defp build_params() do
@@ -162,22 +167,4 @@ defmodule Phoenix.Router.Route do
   """
   def merge_params(%Plug.Conn.Unfetched{}, path_params), do: path_params
   def merge_params(params, path_params), do: Map.merge(params, path_params)
-
-  @doc """
-  Validates and returns the list of forward path segments.
-
-  Raises `RuntimeError` if the `plug` is already forwarded or the
-  `path` contains a dynamic segment.
-  """
-  def forward_path_segments(path, plug, phoenix_forwards) do
-    case Plug.Router.Utils.build_path_match(path) do
-      {[], path_segments} ->
-        if phoenix_forwards[plug] do
-          raise ArgumentError, "#{inspect plug} has already been forwarded to. A module can only be forwarded a single time."
-        end
-        path_segments
-      _ ->
-        raise ArgumentError, "dynamic segment \"#{path}\" not allowed when forwarding. Use a static path instead."
-    end
-  end
 end
diff --git a/lib/phoenix/router/scope.ex b/lib/phoenix/router/scope.ex
index 67164e66d..b383c4d07 100644
--- a/lib/phoenix/router/scope.ex
+++ b/lib/phoenix/router/scope.ex
@@ -1,5 +1,5 @@
 defmodule Phoenix.Router.Scope do
-  alias Phoenix.Router.{Scope, Route}
+  alias Phoenix.Router.Scope
   @moduledoc false
 
   @stack :phoenix_router_scopes
@@ -21,6 +21,10 @@ defmodule Phoenix.Router.Scope do
   Builds a route based on the top of the stack.
   """
   def route(line, module, kind, verb, path, plug, plug_opts, opts) do
+    unless is_atom(plug) do
+      raise ArgumentError, "routes expect a module plug as second argument, got: #{inspect plug}"
+    end
+
     top = get_top(module)
     path    = validate_path(path)
     private = Keyword.get(opts, :private, %{})
@@ -28,6 +32,7 @@ defmodule Phoenix.Router.Scope do
     as      = Keyword.get(opts, :as, Phoenix.Naming.resource_name(plug, "Controller"))
     alias?  = Keyword.get(opts, :alias, true)
     trailing_slash? = Keyword.get(opts, :trailing_slash, top.trailing_slash?) == true
+    warn_on_verify? = Keyword.get(opts, :warn_on_verify, true)
 
     if to_string(as) == "static"  do
       raise ArgumentError, "`static` is a reserved route prefix generated from #{inspect plug} or `:as` option"
@@ -41,7 +46,37 @@ defmodule Phoenix.Router.Scope do
       |> Keyword.get(:metadata, %{})
       |> Map.put(:log, Keyword.get(opts, :log, top.log))
 
-    Phoenix.Router.Route.build(line, kind, verb, path, top.host, alias, plug_opts, as, top.pipes, private, assigns, metadata, trailing_slash?)
+    if kind == :forward do
+      register_forwards(module, path, plug)
+    end
+
+    Phoenix.Router.Route.build(line, kind, verb, path, top.host, alias, plug_opts, as, top.pipes, private, assigns, metadata, trailing_slash?, warn_on_verify?)
+  end
+
+  defp register_forwards(module, path, plug) when is_atom(plug) do
+    plug = expand_alias(module, plug)
+    phoenix_forwards = Module.get_attribute(module, :phoenix_forwards)
+
+    path_segments =
+      case Plug.Router.Utils.build_path_match(path) do
+        {[], path_segments} ->
+          if phoenix_forwards[plug] do
+            raise ArgumentError, "#{inspect plug} has already been forwarded to. A module can only be forwarded a single time"
+          end
+
+          path_segments
+
+        _ ->
+          raise ArgumentError, "dynamic segment \"#{path}\" not allowed when forwarding. Use a static path instead"
+      end
+
+    phoenix_forwards = Map.put(phoenix_forwards, plug, path_segments)
+    Module.put_attribute(module, :phoenix_forwards, phoenix_forwards)
+    plug
+  end
+
+  defp register_forwards(_, _, plug) do
+    raise ArgumentError, "forward expects a module as the second argument, #{inspect plug} given"
   end
 
   @doc """
@@ -49,11 +84,7 @@ defmodule Phoenix.Router.Scope do
   """
   def validate_path("/" <> _ = path), do: path
   def validate_path(path) when is_binary(path) do
-    IO.warn """
-    router paths should begin with a forward slash, got: #{inspect path}
-    #{Exception.format_stacktrace()}
-    """
-
+    IO.warn "router paths should begin with a forward slash, got: #{inspect path}"
     "/" <> path
   end
   def validate_path(path) do
@@ -139,22 +170,6 @@ defmodule Phoenix.Router.Scope do
     end)
   end
 
-  @doc """
-  Add a forward to the router.
-  """
-  def register_forwards(module, path, plug) when is_atom(plug) do
-    plug = expand_alias(module, plug)
-    phoenix_forwards = Module.get_attribute(module, :phoenix_forwards)
-    path_segments = Route.forward_path_segments(path, plug, phoenix_forwards)
-    phoenix_forwards = Map.put(phoenix_forwards, plug, path_segments)
-    Module.put_attribute(module, :phoenix_forwards, phoenix_forwards)
-    plug
-  end
-
-  def register_forwards(_, _, plug) do
-    raise ArgumentError, "forward expects a module as the second argument, #{inspect plug} given"
-  end
-
   @doc """
   Expands the alias in the current router scope.
   """
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
new file mode 100644
index 000000000..cf44e9efb
--- /dev/null
+++ b/lib/phoenix/verified_routes.ex
@@ -0,0 +1,753 @@
+defmodule Phoenix.VerifiedRoutes do
+  @moduledoc ~S"""
+  Provides route generation with compile-time verification.
+
+  Use of the `sigil_p` macro allows paths and URLs throughout your
+  application to be compile-time verified against your Phoenix router(s).
+  For example the following path and URL usages:
+
+
+      <.link href={~p"/sessions/new"} method="post">Sign in</.link>
+
+      redirect(to: url(~p"/posts/#{post}"))
+
+  Will be verified against your standard `Phoenix.Router` definitions:
+
+      get "/posts/:post_id", PostController, :show
+      post "/sessions/new", SessionController, :create
+
+  Unmatched routes will issue compiler warnings:
+
+      warning: no route path for AppWeb.Router matches "/postz/#{post}"
+        lib/app_web/controllers/post_controller.ex:100: AppWeb.PostController.show/2
+
+  Additionally, interpolated ~p values are encoded via the `Phoenix.Param` protocol.
+  For example, a `%Post{}` struct in your application may derive the `Phoenix.Param`
+  protocol to generate slug-based paths rather than ID based ones. This allows you to
+  use `~p"/posts/#{post}"` rather than `~p"/posts/#{post.slug}"` throughout your
+  application. See the `Phoenix.Param` documentation for more details.
+
+  Query strings are also supported in verified routes, either in traditional query
+  string form:
+
+      ~p"/posts?page=#{page}"
+
+  Or as a keyword list or map of values:
+
+      params = %{page: 1, direction: "asc"}
+      ~p"/posts?#{params}"
+
+  Like path segments, query strings params are proper URL encoded and may be interpolated
+  directly into the ~p string.
+
+  ## Options
+
+  To verify routes in your application modules, such as controller, templates, and views,
+  `use Phoenix.VerifiedRoutes`, which supports the following options:
+
+    * `:router` - The required router to verify ~p paths against
+    * `:endpoint` - The optional endpoint for ~p script_name and URL generation
+    * `:statics` - The optional list of static directories to treat as verified paths
+
+  For example:
+
+      use Phoenix.VerifiedRoutes,
+        router: AppWeb.Router,
+        endpoint: AppWeb.Endpoint,
+        statics: ~(images)
+
+  ## Usage
+
+  The majority of path and URL generation needs your application will be met
+  with `~p` and `url/1`, where all information necessary to construct the path
+  or URL is provided the by the compile-time information stored in the Endpoint
+  and Router passed to `use Phoenix.VerifiedRoutes`.
+
+  That said, there are some circumstances where `path/2`, `path/3`, `url/2`, and `url/3`
+  are required:
+
+    * When the runtime values of the `%Plug.Conn{}`, `%Phoenix.LiveSocket{}`, or a `%URI{}`
+      dictate the formation of the path or URL, which happens under the following scenarios:
+
+      - `Phoenix.Controller.put_router_url/2` is used to override the endpoint's URL
+      - `Phoenix.Controller.put_static_url/2` is used to override the endpoint's static URL
+
+    * When the Router module differs from the the one passed to `use Phoenix.VerifiedRoutes`,
+      such as library code, or application code that relies on multiple routers. In such cases,
+      the router module can be provided explicitly to `path/3` and `url/3`.
+
+  ## Tracking Warnings
+
+  All static path segments must start with forward slash and you must have a static segment
+  between dynamic interpolations in order for a route to be verified without warnings.
+  For example, the following path generates proper warnings
+
+      ~p"/media/posts/#{post}"
+
+  While this one will not allow the compiler to see the full path:
+
+      type = "posts"
+      ~p"/media/#{type}/#{post}"
+
+  In such cases, it's better to write a function such as `media_path/1` which branches
+  on different `~p`'s to handle each type.
+
+  Like any other compilation warning, the Elixir compiler will warn any time the file
+  that a ~p resides in changes, or if the router is changed. To view previously issued
+  warnings for files that lack new changes, the `--all-warnings` flag may be passed to
+  the `mix compile` task. For the following will show all warnings the compiler
+  has previously encountered when compiling the current application code:
+
+      $ mix compile --all-warnigns
+
+  *Note: Elixir >= 1.14.0 is required for comprehensive warnings. Older versions
+  will work properly and warn on new compilations, but changes to the router file
+  will not issue new warnings.
+  """
+  @doc false
+  defstruct router: nil,
+            route: nil,
+            inspected_route: nil,
+            stacktrace: nil,
+            test_path: nil
+
+  defmacro __using__(opts) do
+    opts =
+      if Macro.quoted_literal?(opts) do
+        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
+      else
+        opts
+      end
+
+    quote do
+      unquote(__MODULE__).__using__(__MODULE__, unquote(opts))
+      import unquote(__MODULE__)
+    end
+  end
+
+  @doc false
+  def __using__(mod, opts) do
+    Module.register_attribute(mod, :phoenix_verified_routes, accumulate: true)
+    Module.put_attribute(mod, :before_compile, __MODULE__)
+    Module.put_attribute(mod, :router, Keyword.fetch!(opts, :router))
+    Module.put_attribute(mod, :endpoint, Keyword.get(opts, :endpoint))
+
+    statics =
+      case Keyword.get(opts, :statics, []) do
+        list when is_list(list) -> list
+        other -> raise ArgumentError, "expected statics to be a list, got: #{inspect(other)}"
+      end
+
+    Module.put_attribute(mod, :phoenix_verified_statics, statics)
+  end
+
+  defmacro __before_compile__(env) do
+    if Version.match?(System.version(), ">= 1.14.0-dev") do
+      quote do
+        @after_verify {__MODULE__, :__verify_routes__}
+
+        @doc false
+        def __verify_routes__(_module) do
+          unquote(__MODULE__).__verify__(@phoenix_verified_routes)
+        end
+      end
+    else
+      __verify__(Module.get_attribute(env.module, :phoenix_verified_routes))
+    end
+  end
+
+  @doc false
+  def __verify__(routes) when is_list(routes) do
+    Enum.each(routes, fn %__MODULE__{} = route ->
+      unless match_route?(route.router, route.test_path) do
+        IO.warn(
+          "no route path for #{inspect(route.router)} matches #{route.inspected_route}",
+          route.stacktrace
+        )
+      end
+    end)
+  end
+
+  defp expand_alias({:__aliases__, _, _} = alias, env),
+    do: Macro.expand(alias, %{env | function: {:path, 2}})
+
+  defp expand_alias(other, _env), do: other
+
+  @doc ~S'''
+  Generates the router path with route verification.
+
+  Interpolated named parameters are encoded via the `Phoenix.Param` protocol.
+
+  Warns when the provided path does not match against the router specified
+  in `use Phoenix.VerifiedRoutes` or the `@router` module attribute.
+
+  ## Examples
+
+      use Phoenix.VerifiedRoutes, endpoint: MyAppWeb.Endpoint, router: MyAppWeb.Router
+
+      redirect(to: ~p"/users/top")
+
+      redirect(to: ~p"/users/#{@user}")
+
+      ~H"""
+      <.link to={~p"/users??page=#{@page}"}>profile</.link>
+
+      <.link to={~p"/users?#{@params}"}>profile</.link>
+      """
+  '''
+  defmacro sigil_p({:<<>>, _meta, _segments} = route, extra) do
+    validate_sigil_p!(extra)
+    endpoint = attr!(__CALLER__, :endpoint)
+    router = attr!(__CALLER__, :router)
+
+    route
+    |> build_route(route, __CALLER__, endpoint, router)
+    |> inject_path(__CALLER__)
+  end
+
+  defp inject_path(
+         {%__MODULE__{} = route, type, _endpoint_ctx, _route_ast, path_ast, static_ast},
+         env
+       ) do
+    case type do
+      :static ->
+        static_ast
+
+      other when other in [:match, :error] ->
+        Module.put_attribute(env.module, :phoenix_verified_routes, route)
+        path_ast
+    end
+  end
+
+  defp inject_url(
+         {%__MODULE__{} = route, type, endpoint_ctx, route_ast, path_ast, _static_ast},
+         env
+       ) do
+    case type do
+      :static ->
+        quote do
+          unquote(__MODULE__).static_url(unquote_splicing([endpoint_ctx, route_ast]))
+        end
+
+      other when other in [:match, :error] ->
+        Module.put_attribute(env.module, :phoenix_verified_routes, route)
+
+        quote do
+          unquote(__MODULE__).unverified_url(unquote_splicing([endpoint_ctx, path_ast]))
+        end
+    end
+  end
+
+  defp validate_sigil_p!([]), do: :ok
+
+  defp validate_sigil_p!(extra) do
+    raise ArgumentError, "~p does not support trailing fragment, got: #{inspect(extra)}"
+  end
+
+  defp raise_invalid_route(ast) do
+    raise ArgumentError,
+          "expected compile-time ~p path string, got: #{Macro.to_string(ast)}\n" <>
+            "Use unverified_path/2 and unverified_url/2 if you need to build an arbitrary path."
+  end
+
+  @doc ~S'''
+  Generates the router path with route verification.
+
+  See `sigil_p/1` for more information.
+
+  Warns when the provided path does not match against the router specified
+  in `use Phoenix.VerifiedRoutes` or the `@router` module attribute.
+
+  ## Examples
+
+      import Phoenix.VerifiedRoutes
+
+      redirect(to: path(conn, MyAppWeb.Router, ~p"/users/top"))
+
+      redirect(to: path(conn, MyAppWeb.Router, ~p"/users/#{@user}"))
+
+      ~H"""
+      <.link to={path(@uri, MyAppWeb.Router, "/users?page=#{@page}")}>profile</.link>
+      <.link to={path(@uri, MyAppWeb.Router, "/users?#{@params}")}>profile</.link>
+      """
+  '''
+  defmacro path(
+             conn_or_socket_or_endpoint_or_uri,
+             router,
+             {:sigil_p, _, [{:<<>>, _meta, _segments} = route, extra]} = og_ast
+           ) do
+    validate_sigil_p!(extra)
+
+    route
+    |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+    |> inject_path(__CALLER__)
+  end
+
+  defmacro path(_endpoint, _router, other), do: raise_invalid_route(other)
+
+  defmacro path(
+             conn_or_socket_or_endpoint_or_uri,
+             {:sigil_p, _, [{:<<>>, _meta, _segments} = route, extra]} = og_ast
+           ) do
+    validate_sigil_p!(extra)
+    router = attr!(__CALLER__, :router)
+
+    route
+    |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+    |> inject_path(__CALLER__)
+  end
+
+  defmacro path(_conn_or_socket_or_endpoint_or_uri, other), do: raise_invalid_route(other)
+
+  @doc ~S'''
+  Generates the router url with route verification.
+
+  See `sigil_p/1` for more information.
+
+  Warns when the provided path does not match against the router specified
+  in `use Phoenix.VerifiedRoutes` or the `@router` module attribute.
+
+  ## Examples
+
+      use Phoenix.VerifiedRoutes, endpoint: MyAppWeb.Endpoint, router: MyAppWeb.Router
+
+      redirect(to: url(conn, ~p"/users/top"))
+
+      redirect(to: url(conn, ~p"/users/#{@user}"))
+
+      ~H"""
+      <.link to={url(@uri, "/users?#{[page: @page]}")}>profile</.link>
+      """
+
+  The router may also be provided in cases where you want to verify routes for a
+  router other than the one passed to `use Phoenix.VerifiedRoutes`:
+
+      redirect(to: url(conn, OtherRouter, ~p"/users"))
+
+  Forwarded routes are also resolved automatically. For example, imagine you
+  have a forward path to an admin router in your main router:
+
+      defmodule AppWeb.Router do
+        ...
+        forward "/admin", AppWeb.AdminRouter
+      end
+
+      defmodule AppWeb.AdminRouter do
+        ...
+        get "/users", AppWeb.Admin.UserController
+      end
+
+  Forwarded paths in your main application router will be verified as usual,
+  such as `~p"/admin/users"`.
+  '''
+  defmacro url({:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = og_ast) do
+    endpoint = attr!(__CALLER__, :endpoint)
+    router = attr!(__CALLER__, :router)
+
+    route
+    |> build_route(og_ast, __CALLER__, endpoint, router)
+    |> inject_url(__CALLER__)
+  end
+
+  defmacro url(other), do: raise_invalid_route(other)
+
+  @doc """
+  Generates the router url with route verification from the connection, socket, or URI.
+
+  See `url/1` for more information.
+  """
+  defmacro url(
+             conn_or_socket_or_endpoint_or_uri,
+             {:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = og_ast
+           ) do
+    router = attr!(__CALLER__, :router)
+
+    route
+    |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+    |> inject_url(__CALLER__)
+  end
+
+  defmacro url(_conn_or_socket_or_endpoint_or_uri, other), do: raise_invalid_route(other)
+
+  @doc """
+  Generates the url with route verification from the connection, socket, or URI and router.
+
+  See `url/1` for more information.
+  """
+  defmacro url(
+             conn_or_socket_or_endpoint_or_uri,
+             router,
+             {:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = og_ast
+           ) do
+    router = Macro.expand(router, __CALLER__)
+
+    route
+    |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+    |> inject_url(__CALLER__)
+  end
+
+  defmacro url(_conn_or_socket_or_endpoint_or_uri, _router, other), do: raise_invalid_route(other)
+
+  @doc """
+  Generates url to a static asset given its file path.
+  """
+  def static_url(%Plug.Conn{private: private}, path) do
+    case private do
+      %{phoenix_static_url: static_url} -> concat_url(static_url, path)
+      %{phoenix_endpoint: endpoint} -> static_url(endpoint, path)
+    end
+  end
+
+  def static_url(%_{endpoint: endpoint}, path) do
+    static_url(endpoint, path)
+  end
+
+  def static_url(endpoint, path) when is_atom(endpoint) do
+    endpoint.static_url() <> endpoint.static_path(path)
+  end
+
+  def static_url(other, path) do
+    raise ArgumentError,
+          "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
+            "or a Phoenix.Endpoint when building static url for #{path}, got: #{inspect(other)}"
+  end
+
+  @doc """
+  Returns the URL for the endpoint from the path without verification.
+  """
+  def unverified_url(%Plug.Conn{private: private}, path) do
+    case private do
+      %{phoenix_router_url: url} when is_binary(url) -> concat_url(url, path)
+      %{phoenix_endpoint: endpoint} -> concat_url(endpoint.url(), path)
+    end
+  end
+
+  def unverified_url(%_{endpoint: endpoint}, path) do
+    concat_url(endpoint.url(), path)
+  end
+
+  def unverified_url(%URI{} = uri, path) do
+    URI.to_string(%{uri | path: path})
+  end
+
+  def unverified_url(endpoint, path) when is_atom(endpoint) do
+    concat_url(endpoint.url(), path)
+  end
+
+  def unverified_url(other, path) do
+    raise ArgumentError,
+          "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
+            "or a Phoenix.Endpoint when building url at #{path}, got: #{inspect(other)}"
+  end
+
+  defp concat_url(url, path) when is_binary(path), do: url <> path
+
+  @doc """
+  Generates path to a static asset given its file path.
+  """
+  def static_path(%Plug.Conn{private: private}, path) do
+    case private do
+      %{phoenix_static_url: _} -> path
+      %{phoenix_endpoint: endpoint} -> endpoint.static_path(path)
+    end
+  end
+
+  def static_path(%URI{} = uri, path) do
+    (uri.path || "") <> path
+  end
+
+  def static_path(%_{endpoint: endpoint}, path) do
+    static_path(endpoint, path)
+  end
+
+  def static_path(endpoint, path) when is_atom(endpoint) do
+    endpoint.static_path(path)
+  end
+
+  @doc """
+  Returns the path with relevant script name prefixes without verification.
+  """
+  def unverified_path(%Plug.Conn{} = conn, router, path) do
+    conn
+    |> build_own_forward_path(router, path)
+    |> Kernel.||(build_conn_forward_path(conn, router, path))
+    |> Kernel.||(path_with_script(path, conn.script_name))
+  end
+
+  def unverified_path(%URI{} = uri, _router, path) do
+    (uri.path || "") <> path
+  end
+
+  def unverified_path(%_{endpoint: endpoint}, router, path) do
+    unverified_path(endpoint, router, path)
+  end
+
+  def unverified_path(endpoint, _router, path) when is_atom(endpoint) do
+    endpoint.path(path)
+  end
+
+  def unverified_path(other, router, path) do
+    raise ArgumentError,
+          "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
+            "or a Phoenix.Endpoint when building path for #{inspect(router)} at #{path}, got: #{inspect(other)}"
+  end
+
+  @doc false
+  def __encode_segment__(data) do
+    case data do
+      [] -> ""
+      [str | _] when is_binary(str) -> Enum.map_join(data, "/", &encode_segment/1)
+      _ -> encode_segment(data)
+    end
+  end
+
+  defp encode_segment(data) do
+    data
+    |> Phoenix.Param.to_param()
+    |> URI.encode(&URI.char_unreserved?/1)
+  end
+
+  defp verify_segment(["/" | rest], route, acc), do: verify_segment(rest, route, ["/" | acc])
+
+  # we've found a static segment, return to caller with rewritten query if found
+  defp verify_segment(["/" <> _ = segment | rest], route, acc) do
+    case {String.split(segment, "?"), rest} do
+      {[segment], _} ->
+        verify_segment(rest, route, [URI.encode(segment) | acc])
+
+      {[segment, static_query], dynamic_query} ->
+        {Enum.reverse([URI.encode(segment) | acc]),
+         verify_query(dynamic_query, route, [static_query, "?"])}
+    end
+  end
+
+  # we reached the static query string, return to caller
+  defp verify_segment(["?" <> _ = query], _route, acc) do
+    {Enum.reverse(acc), [query]}
+  end
+
+  # we reached the dynamic query string, return to call with rewritten query
+  defp verify_segment(["?" <> _ = static_query_segment | rest], route, acc) do
+    {Enum.reverse(acc), verify_query(rest, route, [static_query_segment])}
+  end
+
+  defp verify_segment([segment | _], route, _acc) when is_binary(segment) do
+    raise ArgumentError,
+          "path segments must begin with /, got: #{inspect(segment)} in #{Macro.to_string(route)}"
+  end
+
+  defp verify_segment(
+         [
+           {:"::", m1, [{{:., m2, [Kernel, :to_string]}, m3, [dynamic]}, {:binary, _, _} = bin]}
+           | rest
+         ],
+         route,
+         [prev | _] = acc
+       ) when is_binary(prev) do
+    rewrite = {:"::", m1, [{{:., m2, [__MODULE__, :__encode_segment__]}, m3, [dynamic]}, bin]}
+    verify_segment(rest, route, [rewrite | acc])
+  end
+
+  defp verify_segment([_ | _], route, _acc) do
+    raise ArgumentError,
+          "a dynamic ~p interpolation must follow a static segment, got: #{Macro.to_string(route)}"
+  end
+
+  # we've reached the end of the path without finding query, return to caller
+  defp verify_segment([], _route, acc), do: {Enum.reverse(acc), _query = []}
+
+  defp verify_query(
+         [
+           {:"::", m1, [{{:., m2, [Kernel, :to_string]}, m2, [arg]}, {:binary, _, _} = bin]}
+           | rest
+         ],
+         route,
+         acc
+       ) do
+    unless is_binary(hd(acc)) do
+      raise ArgumentError,
+            "interpolated query string params must be separated by &, got: #{Macro.to_string(route)}"
+    end
+
+    rewrite = {:"::", m1, [{{:., m2, [__MODULE__, :__encode_query__]}, m2, [arg]}, bin]}
+    verify_query(rest, route, [rewrite | acc])
+  end
+
+  defp verify_query([], _route, acc), do: Enum.reverse(acc)
+
+  defp verify_query(["=" | rest], route, acc) do
+    verify_query(rest, route, ["=" | acc])
+  end
+
+  defp verify_query(["&" <> _ = param | rest], route, acc) do
+    unless String.contains?(param, "=") do
+      raise ArgumentError,
+            "expected query string param key to end with = or declare a static key value pair, got: #{inspect(param)}"
+    end
+
+    verify_query(rest, route, [param | acc])
+  end
+
+  defp verify_query(_other, route, _acc) do
+    raise_invalid_query(route)
+  end
+
+  defp raise_invalid_query(route) do
+    raise ArgumentError,
+          "expected query string param to be compile-time map or keyword list, got: #{Macro.to_string(route)}"
+  end
+
+  @doc """
+  Generates an integrity hash to a static asset given its file path.
+  """
+  def static_integrity(%Plug.Conn{private: %{phoenix_endpoint: endpoint}}, path) do
+    static_integrity(endpoint, path)
+  end
+
+  def static_integrity(%_{endpoint: endpoint}, path) do
+    static_integrity(endpoint, path)
+  end
+
+  def static_integrity(endpoint, path) when is_atom(endpoint) do
+    endpoint.static_integrity(path)
+  end
+
+  @doc false
+  def __encode_query__(dict) when is_list(dict) or is_map(dict) do
+    case Plug.Conn.Query.encode(dict, &to_param/1) do
+      "" -> ""
+      query_str -> query_str
+    end
+  end
+
+  def __encode_query__(val), do: val |> to_param() |> URI.encode_www_form()
+
+  defp to_param(int) when is_integer(int), do: Integer.to_string(int)
+  defp to_param(bin) when is_binary(bin), do: bin
+  defp to_param(false), do: "false"
+  defp to_param(true), do: "true"
+  defp to_param(data), do: Phoenix.Param.to_param(data)
+
+  defp route_type(router, statics, test_path) do
+    cond do
+      static_path?(test_path, statics) -> :static
+      match_route?(router, test_path) -> :match
+      true -> :error
+    end
+  end
+
+  defp match_route?(router, test_path) when is_binary(test_path) do
+    split_path = for segment <- String.split(test_path, "/"), segment != "", do: segment
+    match_route?(router, split_path)
+  end
+
+  defp match_route?(router, split_path) when is_list(split_path) do
+    case router.__match_route__(split_path) do
+      {_forward_plug, false = _warn_on_verify?, _} -> false
+      {nil = _forward_plug, true = _warn?, _} -> true
+      {forward_plug, true = _warn?, _} -> match_forward_route?(router, forward_plug, split_path)
+      :error -> false
+    end
+  end
+
+  defp match_forward_route?(router, forward_router, split_path) do
+    if function_exported?(forward_router, :__routes__, 0) do
+      script_name = router.__forward__(forward_router)
+      match_route?(forward_router, split_path -- script_name)
+    else
+      true
+    end
+  end
+
+  defp build_route(route_ast, og_ast, env, endpoint_ctx, router) do
+    statics = Module.get_attribute(env.module, :phoenix_verified_statics, [])
+
+    router =
+      case Macro.expand(router, env) do
+        mod when is_atom(mod) ->
+          mod
+
+        other ->
+          raise ArgumentError, """
+          expected router to be to module, got: #{inspect(other)}
+
+          If your router is not defined at compile-time, use unverified_path/3 instead.
+          """
+      end
+
+    {type, test_path, path_ast, static_ast} =
+      rewrite_path(route_ast, endpoint_ctx, router, statics)
+
+    route = %__MODULE__{
+      router: router,
+      stacktrace: Macro.Env.stacktrace(env),
+      inspected_route: Macro.to_string(og_ast),
+      test_path: test_path
+    }
+
+    {route, type, endpoint_ctx, route_ast, path_ast, static_ast}
+  end
+
+  defp rewrite_path(route, endpoint, router, statics) do
+    {:<<>>, meta, segments} = route
+    {path_rewrite, query_rewrite} = verify_segment(segments, route, [])
+    rewrite_route = {:<<>>, meta, path_rewrite ++ query_rewrite}
+    test_path = Enum.map_join(path_rewrite, &if(is_binary(&1), do: &1, else: "1"))
+
+    type = route_type(router, statics, test_path)
+
+    path_ast =
+      quote do
+        unquote(__MODULE__).unverified_path(unquote_splicing([endpoint, router, rewrite_route]))
+      end
+
+    static_ast =
+      quote do
+        unquote(__MODULE__).static_path(unquote_splicing([endpoint, rewrite_route]))
+      end
+
+    {type, test_path, path_ast, static_ast}
+  end
+
+  defp attr!(env, :endpoint) do
+    Module.get_attribute(env.module, :endpoint) ||
+      raise """
+      expected @endpoint to be set. For dynamic endpoint resolution, use path/2 instead.
+
+      for example:
+
+          path(conn_or_socket, ~p"/my-path")
+      """
+  end
+
+  defp attr!(env, name) do
+    Module.get_attribute(env.module, name) || raise "expected @#{name} module attribute to be set"
+  end
+
+  defp static_path?(path, statics) do
+    Enum.find(statics, &String.starts_with?(path, "/" <> &1))
+  end
+
+  defp build_own_forward_path(conn, router, path) do
+    case conn.private do
+      %{^router => local_script} when is_list(local_script) ->
+        path_with_script(path, local_script)
+
+      %{} ->
+        nil
+    end
+  end
+
+  defp build_conn_forward_path(%Plug.Conn{} = conn, router, path) do
+    with %{phoenix_router: phx_router} <- conn.private,
+         %{^phx_router => script_name} when is_list(script_name) <- conn.private,
+         local_script when is_list(local_script) <- phx_router.__forward__(router) do
+      path_with_script(path, script_name ++ local_script)
+    else
+      _ -> nil
+    end
+  end
+
+  defp path_with_script(path, []), do: path
+  defp path_with_script(path, script), do: "/" <> Enum.join(script, "/") <> path
+end


Test patch:
diff --git a/test/phoenix/router/forward_test.exs b/test/phoenix/router/forward_test.exs
index 1565efa4c..a219c1377 100644
--- a/test/phoenix/router/forward_test.exs
+++ b/test/phoenix/router/forward_test.exs
@@ -14,7 +14,6 @@ defmodule Phoenix.Router.ForwardTest do
 
     def index(conn, _params), do: text(conn, "admin index")
     def stats(conn, _params), do: text(conn, "stats")
-    def params(conn, params), do: text(conn, inspect(params))
     def api_users(conn, _params), do: text(conn, "api users")
     def api_root(conn, _params), do: text(conn, "api root")
     defp assign_fwd_script(conn, _), do: assign(conn, :fwd_script, conn.script_name)
@@ -57,10 +56,6 @@ defmodule Phoenix.Router.ForwardTest do
       forward "/admin", AdminDashboard
       forward "/init", InitPlug
       forward "/assign/opts", AssignOptsPlug, %{foo: "bar"}
-
-      scope "/params/:param" do
-        forward "/", Controller, :params
-      end
     end
   end
 
@@ -112,17 +107,10 @@ defmodule Phoenix.Router.ForwardTest do
     end
   end
 
-  test "accumulates phoenix_forwards" do
+  test "accumulates script names" do
     conn = call(Router, :get, "/admin")
-    assert conn.private[Router] == {[], %{
-      Phoenix.Router.ForwardTest.AdminDashboard => ["admin"],
-      Phoenix.Router.ForwardTest.InitPlug => ["init"],
-      Phoenix.Router.ForwardTest.AssignOptsPlug => ["assign", "opts"],
-      Phoenix.Router.ForwardTest.Controller => []
-    }}
-    assert conn.private[AdminDashboard] ==
-      {["admin"], %{Phoenix.Router.ForwardTest.ApiRouter => ["api-admin"]}}
-
+    assert conn.private[Router] == []
+    assert conn.private[AdminDashboard] == ["admin"]
   end
 
   test "helpers cascade script name across forwards based on main router" do
@@ -150,18 +138,14 @@ defmodule Phoenix.Router.ForwardTest do
     assert call(Router, :get, "/assign/opts").assigns.opts == %{foo: "bar"}
   end
 
-  test "forward can handle params" do
-    assert call(Router, :get, "/params/hello_world").resp_body =~ ~s["param" => "hello_world"]
-  end
-
   test "forward with scoped alias" do
     conn = call(ApiRouter, :get, "/health")
     assert conn.resp_body == "health"
-    assert conn.private[ApiRouter] == {[], %{Phoenix.Test.HealthController => []}}
+    assert conn.private[ApiRouter] == []
   end
 
   test "forwards raises if using the plug to arguments" do
-    error_message = ~r/expects a module/
+    error_message = ~r/expect a module/
     assert_raise(ArgumentError, error_message, fn ->
       defmodule BrokenRouter do
         use Phoenix.Router
diff --git a/test/phoenix/router/helpers_test.exs b/test/phoenix/router/helpers_test.exs
index 3bbfefa55..b631af2b9 100644
--- a/test/phoenix/router/helpers_test.exs
+++ b/test/phoenix/router/helpers_test.exs
@@ -36,6 +36,9 @@ defmodule Phoenix.Router.HelpersTest do
         resources "/files", FileController
       end
     end
+    scope "/", host: "users." do
+      post "/host_users/:id/info", UserController, :create
+    end
 
     resources "/files", FileController
 
@@ -79,23 +82,25 @@ defmodule Phoenix.Router.HelpersTest do
 
   # Emulate regular endpoint functions
 
-  def url do
-    "https://example.com"
-  end
+  defmodule Endpoint do
+    def url do
+      "https://example.com"
+    end
 
-  def static_url do
-    "https://static.example.com"
-  end
+    def static_url do
+      "https://static.example.com"
+    end
 
-  def path(path) do
-    path
-  end
+    def path(path) do
+      path
+    end
 
-  def static_path(path) do
-    path
-  end
+    def static_path(path) do
+      path
+    end
 
-  def static_integrity(_path), do: nil
+    def static_integrity(_path), do: nil
+  end
 
   alias Router.Helpers
 
@@ -104,32 +109,32 @@ defmodule Phoenix.Router.HelpersTest do
   end
 
   test "root helper" do
-    conn = conn(:get, "/") |> put_private(:phoenix_endpoint, __MODULE__)
+    conn = conn(:get, "/") |> put_private(:phoenix_endpoint, Endpoint)
     assert Helpers.page_path(conn, :root) == "/"
-    assert Helpers.page_path(__MODULE__, :root) == "/"
+    assert Helpers.page_path(Endpoint, :root) == "/"
   end
 
   test "url helper with query strings" do
-    assert Helpers.post_path(__MODULE__, :show, 5, id: 3) == "/posts/5"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: "bar") == "/posts/5?foo=bar"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: :bar) == "/posts/5?foo=bar"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: true) == "/posts/5?foo=true"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: false) == "/posts/5?foo=false"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: nil) == "/posts/5?foo="
-
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: ~w(bar baz)) ==
+    assert Helpers.post_path(Endpoint, :show, 5, id: 3) == "/posts/5"
+    assert Helpers.post_path(Endpoint, :show, 5, foo: "bar") == "/posts/5?foo=bar"
+    assert Helpers.post_path(Endpoint, :show, 5, foo: :bar) == "/posts/5?foo=bar"
+    assert Helpers.post_path(Endpoint, :show, 5, foo: true) == "/posts/5?foo=true"
+    assert Helpers.post_path(Endpoint, :show, 5, foo: false) == "/posts/5?foo=false"
+    assert Helpers.post_path(Endpoint, :show, 5, foo: nil) == "/posts/5?foo="
+
+    assert Helpers.post_path(Endpoint, :show, 5, foo: ~w(bar baz)) ==
            "/posts/5?foo[]=bar&foo[]=baz"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: %{id: 5}) ==
+    assert Helpers.post_path(Endpoint, :show, 5, foo: %{id: 5}) ==
            "/posts/5?foo[id]=5"
-    assert Helpers.post_path(__MODULE__, :show, 5, foo: %{__struct__: Foo, id: 5}) ==
+    assert Helpers.post_path(Endpoint, :show, 5, foo: %{__struct__: Foo, id: 5}) ==
            "/posts/5?foo=5"
   end
 
   test "url helper with param protocol" do
-    assert Helpers.post_path(__MODULE__, :show, %{__struct__: Foo, id: 5}) == "/posts/5"
+    assert Helpers.post_path(Endpoint, :show, %{__struct__: Foo, id: 5}) == "/posts/5"
 
     assert_raise ArgumentError, fn ->
-      Helpers.post_path(__MODULE__, :show, nil)
+      Helpers.post_path(Endpoint, :show, nil)
     end
   end
 
@@ -137,33 +142,33 @@ defmodule Phoenix.Router.HelpersTest do
     error_suggestion = ~r/bottom_path\(conn, :bottom, order, count, page: 5, per_page: 10\)/
 
     assert_raise ArgumentError, error_suggestion, fn ->
-      Helpers.bottom_path(__MODULE__, :bottom, :asc, 8, {:not, :enumerable})
+      Helpers.bottom_path(Endpoint, :bottom, :asc, 8, {:not, :enumerable})
     end
 
     error_suggestion = ~r/top_path\(conn, :top, page: 5, per_page: 10\)/
 
     assert_raise ArgumentError, error_suggestion, fn ->
-      Helpers.top_path(__MODULE__, :top, "invalid")
+      Helpers.top_path(Endpoint, :top, "invalid")
     end
   end
 
   test "top-level named route" do
-    assert Helpers.post_path(__MODULE__, :show, 5) == "/posts/5"
-    assert Helpers.post_path(__MODULE__, :show, 5, []) == "/posts/5"
-    assert Helpers.post_path(__MODULE__, :show, 5, id: 5) == "/posts/5"
-    assert Helpers.post_path(__MODULE__, :show, 5, %{"id" => 5}) == "/posts/5"
-    assert Helpers.post_path(__MODULE__, :show, "foo") == "/posts/foo"
-    assert Helpers.post_path(__MODULE__, :show, "foo bar") == "/posts/foo%20bar"
-
-    assert Helpers.post_path(__MODULE__, :file, ["foo", "bar/baz"]) == "/posts/file/foo/bar%2Fbaz"
-    assert Helpers.post_path(__MODULE__, :file, ["foo", "bar"], []) == "/posts/file/foo/bar"
-    assert Helpers.post_path(__MODULE__, :file, ["foo", "bar baz"], []) == "/posts/file/foo/bar%20baz"
-
-    assert Helpers.top_path(__MODULE__, :top) == "/posts/top"
-    assert Helpers.top_path(__MODULE__, :top, id: 5) == "/posts/top?id=5"
-    assert Helpers.top_path(__MODULE__, :top, %{"id" => 5}) == "/posts/top?id=5"
-    assert Helpers.top_path(__MODULE__, :top, %{"id" => "foo"}) == "/posts/top?id=foo"
-    assert Helpers.top_path(__MODULE__, :top, %{"id" => "foo bar"}) == "/posts/top?id=foo+bar"
+    assert Helpers.post_path(Endpoint, :show, 5) == "/posts/5"
+    assert Helpers.post_path(Endpoint, :show, 5, []) == "/posts/5"
+    assert Helpers.post_path(Endpoint, :show, 5, id: 5) == "/posts/5"
+    assert Helpers.post_path(Endpoint, :show, 5, %{"id" => 5}) == "/posts/5"
+    assert Helpers.post_path(Endpoint, :show, "foo") == "/posts/foo"
+    assert Helpers.post_path(Endpoint, :show, "foo bar") == "/posts/foo%20bar"
+
+    assert Helpers.post_path(Endpoint, :file, ["foo", "bar/baz"]) == "/posts/file/foo/bar%2Fbaz"
+    assert Helpers.post_path(Endpoint, :file, ["foo", "bar"], []) == "/posts/file/foo/bar"
+    assert Helpers.post_path(Endpoint, :file, ["foo", "bar baz"], []) == "/posts/file/foo/bar%20baz"
+
+    assert Helpers.top_path(Endpoint, :top) == "/posts/top"
+    assert Helpers.top_path(Endpoint, :top, id: 5) == "/posts/top?id=5"
+    assert Helpers.top_path(Endpoint, :top, %{"id" => 5}) == "/posts/top?id=5"
+    assert Helpers.top_path(Endpoint, :top, %{"id" => "foo"}) == "/posts/top?id=foo"
+    assert Helpers.top_path(Endpoint, :top, %{"id" => "foo bar"}) == "/posts/top?id=foo+bar"
 
     error_message = fn helper, arity ->
       """
@@ -176,263 +181,255 @@ defmodule Phoenix.Router.HelpersTest do
     end
 
     assert_raise ArgumentError, error_message.("post_path", 3), fn ->
-      Helpers.post_path(__MODULE__, :skip, 5)
+      Helpers.post_path(Endpoint, :skip, 5)
     end
 
     assert_raise ArgumentError, error_message.("post_url", 3), fn ->
-      Helpers.post_url(__MODULE__, :skip, 5)
+      Helpers.post_url(Endpoint, :skip, 5)
     end
 
     assert_raise ArgumentError, error_message.("post_path", 4), fn ->
-      Helpers.post_path(__MODULE__, :skip, 5, foo: "bar", other: "param")
+      Helpers.post_path(Endpoint, :skip, 5, foo: "bar", other: "param")
     end
 
     assert_raise ArgumentError, error_message.("post_url", 4), fn ->
-      Helpers.post_url(__MODULE__, :skip, 5, foo: "bar", other: "param")
-    end
-
-    assert_raise ArgumentError, ~r/when building url for Phoenix.Router.HelpersTest.Router/, fn ->
-      Helpers.post_url("oops", :skip, 5, foo: "bar", other: "param")
-    end
-
-    assert_raise ArgumentError, ~r/when building path for Phoenix.Router.HelpersTest.Router/, fn ->
-      Helpers.post_path("oops", :skip, 5, foo: "bar", other: "param")
+      Helpers.post_url(Endpoint, :skip, 5, foo: "bar", other: "param")
     end
   end
 
   test "top-level named routes with complex ids" do
-    assert Helpers.post_path(__MODULE__, :show, "==d--+") ==
+    assert Helpers.post_path(Endpoint, :show, "==d--+") ==
       "/posts/%3D%3Dd--%2B"
-    assert Helpers.post_path(__MODULE__, :show, "==d--+", []) ==
+    assert Helpers.post_path(Endpoint, :show, "==d--+", []) ==
       "/posts/%3D%3Dd--%2B"
-    assert Helpers.top_path(__MODULE__, :top, id: "==d--+") ==
+    assert Helpers.top_path(Endpoint, :top, id: "==d--+") ==
       "/posts/top?id=%3D%3Dd--%2B"
 
-    assert Helpers.post_path(__MODULE__, :file, ["==d--+", ":O.jpg"]) ==
+    assert Helpers.post_path(Endpoint, :file, ["==d--+", ":O.jpg"]) ==
       "/posts/file/%3D%3Dd--%2B/%3AO.jpg"
-    assert Helpers.post_path(__MODULE__, :file, ["==d--+", ":O.jpg"], []) ==
+    assert Helpers.post_path(Endpoint, :file, ["==d--+", ":O.jpg"], []) ==
       "/posts/file/%3D%3Dd--%2B/%3AO.jpg"
-    assert Helpers.post_path(__MODULE__, :file, ["==d--+", ":O.jpg"], xx: "/=+/") ==
+    assert Helpers.post_path(Endpoint, :file, ["==d--+", ":O.jpg"], xx: "/=+/") ==
       "/posts/file/%3D%3Dd--%2B/%3AO.jpg?xx=%2F%3D%2B%2F"
   end
 
   test "top-level named routes with trailing slashes" do
-    assert Helpers.trail_path(__MODULE__, :top) == "/trails/top/"
-    assert Helpers.trail_path(__MODULE__, :top, id: 5) == "/trails/top/?id=5"
-    assert Helpers.trail_path(__MODULE__, :top, %{"id" => "foo"}) == "/trails/top/?id=foo"
-    assert Helpers.trail_path(__MODULE__, :top, %{"id" => "foo bar"}) == "/trails/top/?id=foo+bar"
+    assert Helpers.trail_path(Endpoint, :top) == "/trails/top/"
+    assert Helpers.trail_path(Endpoint, :top, id: 5) == "/trails/top/?id=5"
+    assert Helpers.trail_path(Endpoint, :top, %{"id" => "foo"}) == "/trails/top/?id=foo"
+    assert Helpers.trail_path(Endpoint, :top, %{"id" => "foo bar"}) == "/trails/top/?id=foo+bar"
   end
 
   test "resources generates named routes for :index, :edit, :show, :new" do
-    assert Helpers.user_path(__MODULE__, :index, []) == "/users"
-    assert Helpers.user_path(__MODULE__, :index) == "/users"
-    assert Helpers.user_path(__MODULE__, :edit, 123, []) == "/users/123/edit"
-    assert Helpers.user_path(__MODULE__, :edit, 123) == "/users/123/edit"
-    assert Helpers.user_path(__MODULE__, :show, 123, []) == "/users/123"
-    assert Helpers.user_path(__MODULE__, :show, 123) == "/users/123"
-    assert Helpers.user_path(__MODULE__, :new, []) == "/users/new"
-    assert Helpers.user_path(__MODULE__, :new) == "/users/new"
+    assert Helpers.user_path(Endpoint, :index, []) == "/users"
+    assert Helpers.user_path(Endpoint, :index) == "/users"
+    assert Helpers.user_path(Endpoint, :edit, 123, []) == "/users/123/edit"
+    assert Helpers.user_path(Endpoint, :edit, 123) == "/users/123/edit"
+    assert Helpers.user_path(Endpoint, :show, 123, []) == "/users/123"
+    assert Helpers.user_path(Endpoint, :show, 123) == "/users/123"
+    assert Helpers.user_path(Endpoint, :new, []) == "/users/new"
+    assert Helpers.user_path(Endpoint, :new) == "/users/new"
   end
 
   test "resources generated named routes with complex ids" do
-    assert Helpers.user_path(__MODULE__, :edit, "1a+/31d", []) == "/users/1a%2B%2F31d/edit"
-    assert Helpers.user_path(__MODULE__, :edit, "1a+/31d") == "/users/1a%2B%2F31d/edit"
-    assert Helpers.user_path(__MODULE__, :show, "1a+/31d", []) == "/users/1a%2B%2F31d"
-    assert Helpers.user_path(__MODULE__, :show, "1a+/31d") == "/users/1a%2B%2F31d"
+    assert Helpers.user_path(Endpoint, :edit, "1a+/31d", []) == "/users/1a%2B%2F31d/edit"
+    assert Helpers.user_path(Endpoint, :edit, "1a+/31d") == "/users/1a%2B%2F31d/edit"
+    assert Helpers.user_path(Endpoint, :show, "1a+/31d", []) == "/users/1a%2B%2F31d"
+    assert Helpers.user_path(Endpoint, :show, "1a+/31d") == "/users/1a%2B%2F31d"
 
-    assert Helpers.message_path(__MODULE__, :update, "8=/=d", []) == "/admin/messages/8%3D%2F%3Dd"
-    assert Helpers.message_path(__MODULE__, :update, "8=/=d") == "/admin/messages/8%3D%2F%3Dd"
-    assert Helpers.message_path(__MODULE__, :delete, "8=/=d", []) == "/admin/messages/8%3D%2F%3Dd"
-    assert Helpers.message_path(__MODULE__, :delete, "8=/=d") == "/admin/messages/8%3D%2F%3Dd"
+    assert Helpers.message_path(Endpoint, :update, "8=/=d", []) == "/admin/messages/8%3D%2F%3Dd"
+    assert Helpers.message_path(Endpoint, :update, "8=/=d") == "/admin/messages/8%3D%2F%3Dd"
+    assert Helpers.message_path(Endpoint, :delete, "8=/=d", []) == "/admin/messages/8%3D%2F%3Dd"
+    assert Helpers.message_path(Endpoint, :delete, "8=/=d") == "/admin/messages/8%3D%2F%3Dd"
 
-    assert Helpers.user_path(__MODULE__, :show, "1a+/31d", [dog: "8d="]) == "/users/1a%2B%2F31d?dog=8d%3D"
-    assert Helpers.user_path(__MODULE__, :index, [cat: "=8+/&"]) == "/users?cat=%3D8%2B%2F%26"
+    assert Helpers.user_path(Endpoint, :show, "1a+/31d", [dog: "8d="]) == "/users/1a%2B%2F31d?dog=8d%3D"
+    assert Helpers.user_path(Endpoint, :index, [cat: "=8+/&"]) == "/users?cat=%3D8%2B%2F%26"
   end
 
   test "resources generates named routes for :create, :update, :delete" do
-    assert Helpers.message_path(__MODULE__, :create, []) == "/admin/messages"
-    assert Helpers.message_path(__MODULE__, :create) == "/admin/messages"
+    assert Helpers.message_path(Endpoint, :create, []) == "/admin/messages"
+    assert Helpers.message_path(Endpoint, :create) == "/admin/messages"
 
-    assert Helpers.message_path(__MODULE__, :update, 1, []) == "/admin/messages/1"
-    assert Helpers.message_path(__MODULE__, :update, 1) == "/admin/messages/1"
+    assert Helpers.message_path(Endpoint, :update, 1, []) == "/admin/messages/1"
+    assert Helpers.message_path(Endpoint, :update, 1) == "/admin/messages/1"
 
-    assert Helpers.message_path(__MODULE__, :delete, 1, []) == "/admin/messages/1"
-    assert Helpers.message_path(__MODULE__, :delete, 1) == "/admin/messages/1"
+    assert Helpers.message_path(Endpoint, :delete, 1, []) == "/admin/messages/1"
+    assert Helpers.message_path(Endpoint, :delete, 1) == "/admin/messages/1"
   end
 
   test "1-Level nested resources generates nested named routes for :index, :edit, :show, :new" do
-    assert Helpers.user_comment_path(__MODULE__, :index, 99, []) == "/users/99/comments"
-    assert Helpers.user_comment_path(__MODULE__, :index, 99) == "/users/99/comments"
-    assert Helpers.user_comment_path(__MODULE__, :edit, 88, 2, []) == "/users/88/comments/2/edit"
-    assert Helpers.user_comment_path(__MODULE__, :edit, 88, 2) == "/users/88/comments/2/edit"
-    assert Helpers.user_comment_path(__MODULE__, :show, 123, 2, []) == "/users/123/comments/2"
-    assert Helpers.user_comment_path(__MODULE__, :show, 123, 2) == "/users/123/comments/2"
-    assert Helpers.user_comment_path(__MODULE__, :new, 88, []) == "/users/88/comments/new"
-    assert Helpers.user_comment_path(__MODULE__, :new, 88) == "/users/88/comments/new"
+    assert Helpers.user_comment_path(Endpoint, :index, 99, []) == "/users/99/comments"
+    assert Helpers.user_comment_path(Endpoint, :index, 99) == "/users/99/comments"
+    assert Helpers.user_comment_path(Endpoint, :edit, 88, 2, []) == "/users/88/comments/2/edit"
+    assert Helpers.user_comment_path(Endpoint, :edit, 88, 2) == "/users/88/comments/2/edit"
+    assert Helpers.user_comment_path(Endpoint, :show, 123, 2, []) == "/users/123/comments/2"
+    assert Helpers.user_comment_path(Endpoint, :show, 123, 2) == "/users/123/comments/2"
+    assert Helpers.user_comment_path(Endpoint, :new, 88, []) == "/users/88/comments/new"
+    assert Helpers.user_comment_path(Endpoint, :new, 88) == "/users/88/comments/new"
 
     assert_raise ArgumentError, ~r/no action :skip/, fn ->
-      Helpers.user_comment_file_path(__MODULE__, :skip, 123, 456)
+      Helpers.user_comment_file_path(Endpoint, :skip, 123, 456)
     end
 
     assert_raise ArgumentError, ~r/no action :skip/, fn ->
-      Helpers.user_comment_file_path(__MODULE__, :skip, 123, 456, foo: "bar")
+      Helpers.user_comment_file_path(Endpoint, :skip, 123, 456, foo: "bar")
     end
 
     assert_raise ArgumentError, ~r/no function clause for Phoenix.Router.HelpersTest.Router.Helpers.user_comment_path\/3 and action :show/, fn ->
-      Helpers.user_comment_path(__MODULE__, :show, 123)
+      Helpers.user_comment_path(Endpoint, :show, 123)
     end
   end
 
   test "multi-level nested resources generated named routes with complex ids" do
-    assert Helpers.user_comment_path(__MODULE__, :index, "f4/d+~=", []) ==
+    assert Helpers.user_comment_path(Endpoint, :index, "f4/d+~=", []) ==
       "/users/f4%2Fd%2B~%3D/comments"
-    assert Helpers.user_comment_path(__MODULE__, :index, "f4/d+~=") ==
+    assert Helpers.user_comment_path(Endpoint, :index, "f4/d+~=") ==
       "/users/f4%2Fd%2B~%3D/comments"
-    assert Helpers.user_comment_path(__MODULE__, :edit, "f4/d+~=", "x-+=/", []) ==
+    assert Helpers.user_comment_path(Endpoint, :edit, "f4/d+~=", "x-+=/", []) ==
       "/users/f4%2Fd%2B~%3D/comments/x-%2B%3D%2F/edit"
-    assert Helpers.user_comment_path(__MODULE__, :edit, "f4/d+~=", "x-+=/") ==
+    assert Helpers.user_comment_path(Endpoint, :edit, "f4/d+~=", "x-+=/") ==
       "/users/f4%2Fd%2B~%3D/comments/x-%2B%3D%2F/edit"
-    assert Helpers.user_comment_path(__MODULE__, :show, "f4/d+~=", "x-+=/", []) ==
+    assert Helpers.user_comment_path(Endpoint, :show, "f4/d+~=", "x-+=/", []) ==
       "/users/f4%2Fd%2B~%3D/comments/x-%2B%3D%2F"
-    assert Helpers.user_comment_path(__MODULE__, :show, "f4/d+~=", "x-+=/") ==
+    assert Helpers.user_comment_path(Endpoint, :show, "f4/d+~=", "x-+=/") ==
       "/users/f4%2Fd%2B~%3D/comments/x-%2B%3D%2F"
-    assert Helpers.user_comment_path(__MODULE__, :new, "/==/", []) ==
+    assert Helpers.user_comment_path(Endpoint, :new, "/==/", []) ==
       "/users/%2F%3D%3D%2F/comments/new"
-    assert Helpers.user_comment_path(__MODULE__, :new, "/==/") ==
+    assert Helpers.user_comment_path(Endpoint, :new, "/==/") ==
       "/users/%2F%3D%3D%2F/comments/new"
 
-    assert Helpers.user_comment_file_path(__MODULE__, :show, "f4/d+~=", "/==/", "x-+=/", []) ==
+    assert Helpers.user_comment_file_path(Endpoint, :show, "f4/d+~=", "/==/", "x-+=/", []) ==
       "/users/f4%2Fd%2B~%3D/comments/%2F%3D%3D%2F/files/x-%2B%3D%2F"
-    assert Helpers.user_comment_file_path(__MODULE__, :show, "f4/d+~=", "/==/", "x-+=/") ==
+    assert Helpers.user_comment_file_path(Endpoint, :show, "f4/d+~=", "/==/", "x-+=/") ==
       "/users/f4%2Fd%2B~%3D/comments/%2F%3D%3D%2F/files/x-%2B%3D%2F"
   end
 
   test "2-Level nested resources generates nested named routes for :index, :edit, :show, :new" do
-    assert Helpers.user_comment_file_path(__MODULE__, :index, 99, 1, []) ==
+    assert Helpers.user_comment_file_path(Endpoint, :index, 99, 1, []) ==
       "/users/99/comments/1/files"
-    assert Helpers.user_comment_file_path(__MODULE__, :index, 99, 1) ==
+    assert Helpers.user_comment_file_path(Endpoint, :index, 99, 1) ==
       "/users/99/comments/1/files"
 
-    assert Helpers.user_comment_file_path(__MODULE__, :edit, 88, 1, 2, []) ==
+    assert Helpers.user_comment_file_path(Endpoint, :edit, 88, 1, 2, []) ==
       "/users/88/comments/1/files/2/edit"
-    assert Helpers.user_comment_file_path(__MODULE__, :edit, 88, 1, 2) ==
+    assert Helpers.user_comment_file_path(Endpoint, :edit, 88, 1, 2) ==
       "/users/88/comments/1/files/2/edit"
 
-    assert Helpers.user_comment_file_path(__MODULE__, :show, 123, 1, 2, []) ==
+    assert Helpers.user_comment_file_path(Endpoint, :show, 123, 1, 2, []) ==
       "/users/123/comments/1/files/2"
-    assert Helpers.user_comment_file_path(__MODULE__, :show, 123, 1, 2) ==
+    assert Helpers.user_comment_file_path(Endpoint, :show, 123, 1, 2) ==
       "/users/123/comments/1/files/2"
 
-    assert Helpers.user_comment_file_path(__MODULE__, :new, 88, 1, []) ==
+    assert Helpers.user_comment_file_path(Endpoint, :new, 88, 1, []) ==
       "/users/88/comments/1/files/new"
-    assert Helpers.user_comment_file_path(__MODULE__, :new, 88, 1) ==
+    assert Helpers.user_comment_file_path(Endpoint, :new, 88, 1) ==
       "/users/88/comments/1/files/new"
   end
 
   test "resources without block generates named routes for :index, :edit, :show, :new" do
-    assert Helpers.file_path(__MODULE__, :index, []) == "/files"
-    assert Helpers.file_path(__MODULE__, :index) == "/files"
-    assert Helpers.file_path(__MODULE__, :edit, 123, []) == "/files/123/edit"
-    assert Helpers.file_path(__MODULE__, :edit, 123) == "/files/123/edit"
-    assert Helpers.file_path(__MODULE__, :show, 123, []) == "/files/123"
-    assert Helpers.file_path(__MODULE__, :show, 123) == "/files/123"
-    assert Helpers.file_path(__MODULE__, :new, []) == "/files/new"
-    assert Helpers.file_path(__MODULE__, :new) == "/files/new"
+    assert Helpers.file_path(Endpoint, :index, []) == "/files"
+    assert Helpers.file_path(Endpoint, :index) == "/files"
+    assert Helpers.file_path(Endpoint, :edit, 123, []) == "/files/123/edit"
+    assert Helpers.file_path(Endpoint, :edit, 123) == "/files/123/edit"
+    assert Helpers.file_path(Endpoint, :show, 123, []) == "/files/123"
+    assert Helpers.file_path(Endpoint, :show, 123) == "/files/123"
+    assert Helpers.file_path(Endpoint, :new, []) == "/files/new"
+    assert Helpers.file_path(Endpoint, :new) == "/files/new"
   end
 
   test "resource generates named routes for :show, :edit, :new, :update, :delete" do
-    assert Helpers.account_path(__MODULE__, :show, []) == "/account"
-    assert Helpers.account_path(__MODULE__, :show) == "/account"
-    assert Helpers.account_path(__MODULE__, :edit, []) == "/account/edit"
-    assert Helpers.account_path(__MODULE__, :edit) == "/account/edit"
-    assert Helpers.account_path(__MODULE__, :new, []) == "/account/new"
-    assert Helpers.account_path(__MODULE__, :new) == "/account/new"
-    assert Helpers.account_path(__MODULE__, :update, []) == "/account"
-    assert Helpers.account_path(__MODULE__, :update) == "/account"
-    assert Helpers.account_path(__MODULE__, :delete, []) == "/account"
-    assert Helpers.account_path(__MODULE__, :delete) == "/account"
+    assert Helpers.account_path(Endpoint, :show, []) == "/account"
+    assert Helpers.account_path(Endpoint, :show) == "/account"
+    assert Helpers.account_path(Endpoint, :edit, []) == "/account/edit"
+    assert Helpers.account_path(Endpoint, :edit) == "/account/edit"
+    assert Helpers.account_path(Endpoint, :new, []) == "/account/new"
+    assert Helpers.account_path(Endpoint, :new) == "/account/new"
+    assert Helpers.account_path(Endpoint, :update, []) == "/account"
+    assert Helpers.account_path(Endpoint, :update) == "/account"
+    assert Helpers.account_path(Endpoint, :delete, []) == "/account"
+    assert Helpers.account_path(Endpoint, :delete) == "/account"
   end
 
   test "2-Level nested resource generates nested named routes for :show" do
-    assert Helpers.account_page_path(__MODULE__, :show, []) == "/account/page"
-    assert Helpers.account_page_path(__MODULE__, :show) == "/account/page"
+    assert Helpers.account_page_path(Endpoint, :show, []) == "/account/page"
+    assert Helpers.account_page_path(Endpoint, :show) == "/account/page"
   end
 
   test "scoped route helpers generated named routes with :path, and :alias options" do
-    assert Helpers.message_path(__MODULE__, :index, []) == "/admin/messages"
-    assert Helpers.message_path(__MODULE__, :index) == "/admin/messages"
-    assert Helpers.message_path(__MODULE__, :show, 1, []) == "/admin/messages/1"
-    assert Helpers.message_path(__MODULE__, :show, 1) == "/admin/messages/1"
+    assert Helpers.message_path(Endpoint, :index, []) == "/admin/messages"
+    assert Helpers.message_path(Endpoint, :index) == "/admin/messages"
+    assert Helpers.message_path(Endpoint, :show, 1, []) == "/admin/messages/1"
+    assert Helpers.message_path(Endpoint, :show, 1) == "/admin/messages/1"
   end
 
   test "scoped route helpers generated named routes with :path, :alias, and :helper options" do
-    assert Helpers.admin_message_path(__MODULE__, :index, []) == "/admin/new/messages"
-    assert Helpers.admin_message_path(__MODULE__, :index) == "/admin/new/messages"
-    assert Helpers.admin_message_path(__MODULE__, :show, 1, []) == "/admin/new/messages/1"
-    assert Helpers.admin_message_path(__MODULE__, :show, 1) == "/admin/new/messages/1"
+    assert Helpers.admin_message_path(Endpoint, :index, []) == "/admin/new/messages"
+    assert Helpers.admin_message_path(Endpoint, :index) == "/admin/new/messages"
+    assert Helpers.admin_message_path(Endpoint, :show, 1, []) == "/admin/new/messages/1"
+    assert Helpers.admin_message_path(Endpoint, :show, 1) == "/admin/new/messages/1"
   end
 
   test "scoped route helpers generated unscoped :as options" do
-    assert Helpers.my_admin_message_path(__MODULE__, :index, []) == "/admin/new/unscoped/messages"
-    assert Helpers.my_admin_message_path(__MODULE__, :index) == "/admin/new/unscoped/messages"
-    assert Helpers.my_admin_message_path(__MODULE__, :show, 1, []) == "/admin/new/unscoped/messages/1"
-    assert Helpers.my_admin_message_path(__MODULE__, :show, 1) == "/admin/new/unscoped/messages/1"
+    assert Helpers.my_admin_message_path(Endpoint, :index, []) == "/admin/new/unscoped/messages"
+    assert Helpers.my_admin_message_path(Endpoint, :index) == "/admin/new/unscoped/messages"
+    assert Helpers.my_admin_message_path(Endpoint, :show, 1, []) == "/admin/new/unscoped/messages/1"
+    assert Helpers.my_admin_message_path(Endpoint, :show, 1) == "/admin/new/unscoped/messages/1"
   end
 
   test "scoped route helpers generated with trailing slashes" do
-    assert Helpers.trail_path(__MODULE__, :index) == "/trails/"
-    assert Helpers.trail_path(__MODULE__, :index, id: 5) == "/trails/?id=5"
-    assert Helpers.trail_path(__MODULE__, :index, %{"id" => "foo"}) == "/trails/?id=foo"
-    assert Helpers.trail_path(__MODULE__, :index, %{"id" => "foo bar"}) == "/trails/?id=foo+bar"
+    assert Helpers.trail_path(Endpoint, :index) == "/trails/"
+    assert Helpers.trail_path(Endpoint, :index, id: 5) == "/trails/?id=5"
+    assert Helpers.trail_path(Endpoint, :index, %{"id" => "foo"}) == "/trails/?id=foo"
+    assert Helpers.trail_path(Endpoint, :index, %{"id" => "foo bar"}) == "/trails/?id=foo+bar"
   end
 
   test "scoped route helpers generated with trailing slashes overridden" do
-    assert Helpers.trail_path(__MODULE__, :open) == "/trails/open"
-    assert Helpers.trail_path(__MODULE__, :open, id: 5) == "/trails/open?id=5"
-    assert Helpers.trail_path(__MODULE__, :open, %{"id" => "foo"}) == "/trails/open?id=foo"
-    assert Helpers.trail_path(__MODULE__, :open, %{"id" => "foo bar"}) == "/trails/open?id=foo+bar"
+    assert Helpers.trail_path(Endpoint, :open) == "/trails/open"
+    assert Helpers.trail_path(Endpoint, :open, id: 5) == "/trails/open?id=5"
+    assert Helpers.trail_path(Endpoint, :open, %{"id" => "foo"}) == "/trails/open?id=foo"
+    assert Helpers.trail_path(Endpoint, :open, %{"id" => "foo bar"}) == "/trails/open?id=foo+bar"
   end
 
   test "scoped route helpers generated with trailing slashes for resource" do
-    assert Helpers.vista_path(__MODULE__, :index) == "/trails/vistas/"
-    assert Helpers.vista_path(__MODULE__, :index, []) == "/trails/vistas/"
-    assert Helpers.vista_path(__MODULE__, :index, id: 5) == "/trails/vistas/?id=5"
-    assert Helpers.vista_path(__MODULE__, :index, %{"id" => "foo"}) == "/trails/vistas/?id=foo"
-
-    assert Helpers.vista_path(__MODULE__, :new) == "/trails/vistas/new/"
-    assert Helpers.vista_path(__MODULE__, :new, []) == "/trails/vistas/new/"
-    assert Helpers.vista_path(__MODULE__, :create) == "/trails/vistas/"
-    assert Helpers.vista_path(__MODULE__, :create, []) == "/trails/vistas/"
-    assert Helpers.vista_path(__MODULE__, :show, 2) == "/trails/vistas/2/"
-    assert Helpers.vista_path(__MODULE__, :show, 2, []) == "/trails/vistas/2/"
-    assert Helpers.vista_path(__MODULE__, :edit, 2) == "/trails/vistas/2/edit/"
-    assert Helpers.vista_path(__MODULE__, :edit, 2, []) == "/trails/vistas/2/edit/"
-    assert Helpers.vista_path(__MODULE__, :update, 2) == "/trails/vistas/2/"
-    assert Helpers.vista_path(__MODULE__, :update, 2, []) == "/trails/vistas/2/"
-    assert Helpers.vista_path(__MODULE__, :delete, 2) == "/trails/vistas/2/"
-    assert Helpers.vista_path(__MODULE__, :delete, 2, []) == "/trails/vistas/2/"
+    assert Helpers.vista_path(Endpoint, :index) == "/trails/vistas/"
+    assert Helpers.vista_path(Endpoint, :index, []) == "/trails/vistas/"
+    assert Helpers.vista_path(Endpoint, :index, id: 5) == "/trails/vistas/?id=5"
+    assert Helpers.vista_path(Endpoint, :index, %{"id" => "foo"}) == "/trails/vistas/?id=foo"
+
+    assert Helpers.vista_path(Endpoint, :new) == "/trails/vistas/new/"
+    assert Helpers.vista_path(Endpoint, :new, []) == "/trails/vistas/new/"
+    assert Helpers.vista_path(Endpoint, :create) == "/trails/vistas/"
+    assert Helpers.vista_path(Endpoint, :create, []) == "/trails/vistas/"
+    assert Helpers.vista_path(Endpoint, :show, 2) == "/trails/vistas/2/"
+    assert Helpers.vista_path(Endpoint, :show, 2, []) == "/trails/vistas/2/"
+    assert Helpers.vista_path(Endpoint, :edit, 2) == "/trails/vistas/2/edit/"
+    assert Helpers.vista_path(Endpoint, :edit, 2, []) == "/trails/vistas/2/edit/"
+    assert Helpers.vista_path(Endpoint, :update, 2) == "/trails/vistas/2/"
+    assert Helpers.vista_path(Endpoint, :update, 2, []) == "/trails/vistas/2/"
+    assert Helpers.vista_path(Endpoint, :delete, 2) == "/trails/vistas/2/"
+    assert Helpers.vista_path(Endpoint, :delete, 2, []) == "/trails/vistas/2/"
   end
 
   test "scoped route helpers generated within scoped routes with trailing slashes" do
-    assert Helpers.trail_path(__MODULE__, :nested_path) == "/trails/nested/path/"
-    assert Helpers.trail_path(__MODULE__, :nested_path, []) == "/trails/nested/path/"
-    assert Helpers.trail_path(__MODULE__, :nested_path, [id: 5]) == "/trails/nested/path/?id=5"
-    assert Helpers.trail_path(__MODULE__, :nested_path, %{"id" => "foo"}) == "/trails/nested/path/?id=foo"
-    assert Helpers.trail_path(__MODULE__, :nested_path, %{"id" => "foo bar"}) == "/trails/nested/path/?id=foo+bar"
+    assert Helpers.trail_path(Endpoint, :nested_path) == "/trails/nested/path/"
+    assert Helpers.trail_path(Endpoint, :nested_path, []) == "/trails/nested/path/"
+    assert Helpers.trail_path(Endpoint, :nested_path, [id: 5]) == "/trails/nested/path/?id=5"
+    assert Helpers.trail_path(Endpoint, :nested_path, %{"id" => "foo"}) == "/trails/nested/path/?id=foo"
+    assert Helpers.trail_path(Endpoint, :nested_path, %{"id" => "foo bar"}) == "/trails/nested/path/?id=foo+bar"
   end
 
   test "can pass an {m, f, a} tuple as a plug argument" do
-    assert Helpers.sub_plug_path(__MODULE__, func: {M, :f, [10]}) == "/mfa_path"
+    assert Helpers.sub_plug_path(Endpoint, func: {M, :f, [10]}) == "/mfa_path"
   end
 
   ## Others
 
   defp conn_with_endpoint do
-    conn(:get, "/") |> put_private(:phoenix_endpoint, __MODULE__)
+    conn(:get, "/") |> put_private(:phoenix_endpoint, Endpoint)
   end
 
   defp socket_with_endpoint do
-    %Phoenix.Socket{endpoint: __MODULE__}
+    %Phoenix.Socket{endpoint: Endpoint}
   end
 
   defp uri do
@@ -440,42 +437,42 @@ defmodule Phoenix.Router.HelpersTest do
   end
 
   test "helpers module generates a static_path helper" do
-    assert Helpers.static_path(__MODULE__, "/images/foo.png") == "/images/foo.png"
+    assert Helpers.static_path(Endpoint, "/images/foo.png") == "/images/foo.png"
     assert Helpers.static_path(conn_with_endpoint(), "/images/foo.png") == "/images/foo.png"
     assert Helpers.static_path(socket_with_endpoint(), "/images/foo.png") == "/images/foo.png"
   end
 
   test "helpers module generates a static_url helper" do
     url = "https://static.example.com/images/foo.png"
-    assert Helpers.static_url(__MODULE__, "/images/foo.png") == url
+    assert Helpers.static_url(Endpoint, "/images/foo.png") == url
     assert Helpers.static_url(conn_with_endpoint(), "/images/foo.png") == url
     assert Helpers.static_url(socket_with_endpoint(), "/images/foo.png") == url
   end
 
   test "helpers module generates a url helper" do
-    assert Helpers.url(__MODULE__) == "https://example.com"
+    assert Helpers.url(Endpoint) == "https://example.com"
     assert Helpers.url(conn_with_endpoint()) == "https://example.com"
     assert Helpers.url(socket_with_endpoint()) == "https://example.com"
     assert Helpers.url(uri()) == "https://example.com"
   end
 
   test "helpers module generates a path helper" do
-    assert Helpers.path(__MODULE__, "/") == "/"
+    assert Helpers.path(Endpoint, "/") == "/"
     assert Helpers.path(conn_with_endpoint(), "/") == "/"
     assert Helpers.path(socket_with_endpoint(), "/") == "/"
     assert Helpers.path(uri(), "/") == "/"
   end
 
   test "helpers module generates a static_integrity helper" do
-    assert is_nil(Helpers.static_integrity(__MODULE__, "/images/foo.png"))
+    assert is_nil(Helpers.static_integrity(Endpoint, "/images/foo.png"))
     assert is_nil(Helpers.static_integrity(conn_with_endpoint(), "/images/foo.png"))
     assert is_nil(Helpers.static_integrity(socket_with_endpoint(), "/images/foo.png"))
   end
 
   test "helpers module generates named routes url helpers" do
     url = "https://example.com/admin/new/messages/1"
-    assert Helpers.admin_message_url(__MODULE__, :show, 1) == url
-    assert Helpers.admin_message_url(__MODULE__, :show, 1, []) == url
+    assert Helpers.admin_message_url(Endpoint, :show, 1) == url
+    assert Helpers.admin_message_url(Endpoint, :show, 1, []) == url
     assert Helpers.admin_message_url(conn_with_endpoint(), :show, 1) == url
     assert Helpers.admin_message_url(conn_with_endpoint(), :show, 1, []) == url
     assert Helpers.admin_message_url(socket_with_endpoint(), :show, 1) == url
@@ -485,19 +482,19 @@ defmodule Phoenix.Router.HelpersTest do
   end
 
   test "helpers properly encode named and query string params" do
-    assert Router.Helpers.post_path(__MODULE__, :show, "my path", foo: "my param") ==
+    assert Router.Helpers.post_path(Endpoint, :show, "my path", foo: "my param") ==
       "/posts/my%20path?foo=my+param"
   end
 
   test "duplicate helpers with unique arities" do
-    assert Helpers.product_path(__MODULE__, :show) == "/products"
-    assert Helpers.product_path(__MODULE__, :show, foo: "bar") == "/products?foo=bar"
-    assert Helpers.product_path(__MODULE__, :show, 123) == "/products/123"
-    assert Helpers.product_path(__MODULE__, :show, 123, foo: "bar") == "/products/123?foo=bar"
-    assert Helpers.product_path(__MODULE__, :show, 123, "asc") == "/products/123/asc"
-    assert Helpers.product_path(__MODULE__, :show, 123, "asc", foo: "bar") == "/products/123/asc?foo=bar"
-    assert Helpers.product_path(__MODULE__, :show, 123, "asc", 1) == "/products/123/asc/1"
-    assert Helpers.product_path(__MODULE__, :show, 123, "asc", 1, foo: "bar") == "/products/123/asc/1?foo=bar"
+    assert Helpers.product_path(Endpoint, :show) == "/products"
+    assert Helpers.product_path(Endpoint, :show, foo: "bar") == "/products?foo=bar"
+    assert Helpers.product_path(Endpoint, :show, 123) == "/products/123"
+    assert Helpers.product_path(Endpoint, :show, 123, foo: "bar") == "/products/123?foo=bar"
+    assert Helpers.product_path(Endpoint, :show, 123, "asc") == "/products/123/asc"
+    assert Helpers.product_path(Endpoint, :show, 123, "asc", foo: "bar") == "/products/123/asc?foo=bar"
+    assert Helpers.product_path(Endpoint, :show, 123, "asc", 1) == "/products/123/asc/1"
+    assert Helpers.product_path(Endpoint, :show, 123, "asc", 1, foo: "bar") == "/products/123/asc/1?foo=bar"
   end
 
   ## Script name
@@ -559,10 +556,10 @@ defmodule Phoenix.Router.HelpersTest do
 
   test "static use endpoint script name only" do
     assert Helpers.static_path(conn_with_script_name(~w(foo)), "/images/foo.png") ==
-           "/api/images/foo.png"
+             "/api/images/foo.png"
 
     assert Helpers.static_url(conn_with_script_name(~w(foo)), "/images/foo.png") ==
-           "https://static.example.com/api/images/foo.png"
+             "https://static.example.com/api/images/foo.png"
   end
 
   ## Dynamics
@@ -570,10 +567,8 @@ defmodule Phoenix.Router.HelpersTest do
   test "phoenix_router_url with string takes precedence over endpoint" do
     url = "https://phoenixframework.org"
     conn = Phoenix.Controller.put_router_url(conn_with_endpoint(), url)
-
     assert Helpers.url(conn) == url
-    assert Helpers.admin_message_url(conn, :show, 1) ==
-      url <> "/admin/new/messages/1"
+    assert Helpers.admin_message_url(conn, :show, 1) == url <> "/admin/new/messages/1"
   end
 
   test "phoenix_router_url with URI takes precedence over endpoint" do
@@ -622,4 +617,20 @@ defmodule Phoenix.Router.HelpersTest do
     conn = Phoenix.Controller.put_static_url(conn_with_script_name(), uri)
     assert Helpers.static_url(conn, "/images/foo.png") == "https://phoenixframework.org:123/path/images/foo.png"
   end
+
+  describe "helpers: false" do
+    defmodule NoHelpersRouter do
+      use Phoenix.Router, helpers: false
+
+      get "/", PostController, :home
+    end
+
+    test "__helpers__ return nil" do
+      assert NoHelpersRouter.__helpers__() == nil
+    end
+
+    test "test not generate Helpers module" do
+      refute Code.ensure_loaded?(NoHelpersRouter.Helpers)
+    end
+  end
 end
diff --git a/test/phoenix/router/route_test.exs b/test/phoenix/router/route_test.exs
index 122e64fcc..8420d1a77 100644
--- a/test/phoenix/router/route_test.exs
+++ b/test/phoenix/router/route_test.exs
@@ -11,7 +11,7 @@ defmodule Phoenix.Router.RouteTest do
   end
 
   test "builds a route based on verb, path, plug, plug options and helper" do
-    route = build(1, :match, :get, "/foo/:bar", nil, Hello, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{log: :debug}, true)
+    route = build(1, :match, :get, "/foo/:bar", nil, Hello, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{log: :debug}, true, true)
     assert route.kind == :match
     assert route.verb == :get
     assert route.path == "/foo/:bar"
@@ -28,31 +28,31 @@ defmodule Phoenix.Router.RouteTest do
   end
 
   test "builds expressions based on the route" do
-    exprs = build(1, :match, :get, "/foo/:bar", nil, Hello, :world, "hello_world", [], %{}, %{}, %{}, false) |> exprs
+    exprs = build(1, :match, :get, "/foo/:bar", nil, Hello, :world, "hello_world", [], %{}, %{}, %{}, false, true) |> exprs(%{})
     assert exprs.verb_match == "GET"
-    assert exprs.path == ["foo", {:bar, [], nil}]
-    assert exprs.binding == [{"bar", {:bar, [], nil}}]
+    assert exprs.path == ["foo", {:arg0, [], Phoenix.Router.Route}]
+    assert exprs.binding == [{"bar", {:arg0, [], Phoenix.Router.Route}}]
     assert Macro.to_string(exprs.host) == "_"
 
-    exprs = build(1, :match, :get, "/", "foo.", Hello, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{}, false) |> exprs
+    exprs = build(1, :match, :get, "/", "foo.", Hello, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{}, false, true) |> exprs(%{})
     assert Macro.to_string(exprs.host) == "\"foo.\" <> _"
 
-    exprs = build(1, :match, :get, "/", "foo.com", Hello, :world, "hello_world", [], %{foo: "bar"}, %{bar: "baz"}, %{}, false) |> exprs
+    exprs = build(1, :match, :get, "/", "foo.com", Hello, :world, "hello_world", [], %{foo: "bar"}, %{bar: "baz"}, %{}, false, true) |> exprs(%{})
     assert Macro.to_string(exprs.host) == "\"foo.com\""
   end
 
   test "builds a catch-all verb_match for match routes" do
-    route = build(1, :match, :*, "/foo/:bar", nil, __MODULE__, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{}, false)
+    route = build(1, :match, :*, "/foo/:bar", nil, __MODULE__, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{}, false, true)
     assert route.verb == :*
     assert route.kind == :match
-    assert exprs(route).verb_match == {:_verb, [], nil}
+    assert exprs(route, %{}).verb_match == {:_verb, [], nil}
   end
 
   test "builds a catch-all verb_match for forwarded routes" do
-    route = build(1, :forward, :*, "/foo/:bar", nil, __MODULE__, :world, "hello_world", [:foo, :bar], %{foo: "bar"}, %{bar: "baz"}, %{}, false)
+    route = build(1, :forward, :*, "/foo", nil, __MODULE__, :world, "hello_world", [:foo], %{foo: "bar"}, %{bar: "baz"}, %{}, false, true)
     assert route.verb == :*
     assert route.kind == :forward
-    assert exprs(route).verb_match == {:_verb, [], nil}
+    assert exprs(route, %{__MODULE__ => ["foo"]}).verb_match == {:_verb, [], nil}
   end
 
   test "as a plug, it forwards and sets path_info and script_name for target, then resumes" do
@@ -64,15 +64,4 @@ defmodule Phoenix.Router.RouteTest do
     assert conn.path_info == ["admin", "stats"]
     assert conn.script_name == ["phoenix"]
   end
-
-  test "exprs/1 returns quoted expressions" do
-    opts = %{a: ~r/foo/}
-    escaped_opts = Macro.escape(opts)
-
-    route = build(1, :match, :*, "/foo", nil, __MODULE__, opts, "hello_world", [], %{}, %{}, %{}, false)
-    assert {__MODULE__, escaped_opts} == exprs(route).dispatch
-
-    fwd_route = build(1, :forward, :*, "/foo", nil, __MODULE__, opts, "hello_world", [], %{}, %{}, %{}, false)
-    assert {_, {:{}, _, [_, __MODULE__, ^escaped_opts]}} = exprs(fwd_route).dispatch
-  end
 end
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
new file mode 100644
index 000000000..13fd785fb
--- /dev/null
+++ b/test/phoenix/verified_routes_test.exs
@@ -0,0 +1,462 @@
+modules = [
+  Phoenix.VerifiedRoutesTest.PostController,
+  Phoenix.VerifiedRoutesTest.UserController
+]
+
+for module <- modules do
+  defmodule module do
+    def init(opts), do: opts
+    def call(conn, _opts), do: conn
+  end
+end
+
+defmodule Phoenix.VerifiedRoutesTest do
+  use ExUnit.Case, async: true
+  import Plug.Test
+
+  @derive {Phoenix.Param, key: :slug}
+  defstruct [:id, :slug]
+
+  defmodule AdminRouter do
+    use Phoenix.Router
+
+    get "/dashboard", Phoenix.VerifiedRoutesTest.UserController, :index
+  end
+
+  defmodule Router do
+    use Phoenix.Router
+    alias Phoenix.VerifiedRoutesTest.{PostController, UserController}
+
+    get "/posts/top", PostController, :top
+    get "/posts/bottom/:order/:count", PostController, :bottom
+    get "/posts/:id", PostController, :show
+    get "/posts/file/*file", PostController, :file
+    get "/posts/skip", PostController, :skip
+    get "/should-warn/*all", PostController, :all, warn_on_verify: false
+
+    scope "/", host: "users." do
+      post "/host_users/:id/info", UserController, :create
+    end
+
+    scope "/admin/new" do
+      resources "/messages", PostController
+    end
+
+    get "/", PostController, :root
+
+    forward "/router_forward", AdminRouter
+    forward "/plug_forward", UserController
+  end
+
+  # Emulate regular endpoint functions
+
+  defmodule Endpoint do
+    def url, do: "https://example.com"
+    def static_url, do: "https://static.example.com"
+    def path(path), do: path
+    def static_path(path), do: path
+    def static_integrity(_path), do: nil
+  end
+
+  defmodule ScriptName do
+    def url, do: "https://example.com"
+    def static_url, do: "https://static.example.com"
+    def path(path), do: "/api" <> path
+    def static_path(path), do: "/api" <> path
+  end
+
+  defp conn_with_endpoint do
+    conn(:get, "/") |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
+  end
+
+  defp socket_with_endpoint, do: %Phoenix.Socket{endpoint: Endpoint}
+
+  def conn_with_script_name(script_name \\ ~w(api)) do
+    conn = Plug.Conn.put_private(conn(:get, "/"), :phoenix_endpoint, ScriptName)
+
+    put_in(conn.script_name, script_name)
+  end
+
+  defp uri_with_script_name do
+    %URI{scheme: "https", host: "example.com", port: 123, path: "/api"}
+  end
+
+  use Phoenix.VerifiedRoutes, statics: ~w(images), endpoint: Endpoint, router: Router
+
+  test "~p with static string" do
+    assert ~p"/posts/1" == "/posts/1"
+    assert ~p"/posts/1?foo=bar" == "/posts/1?foo=bar"
+  end
+
+  test "~p with dynamic string uses Phoenix.Param" do
+    struct = %__MODULE__{id: 123, slug: "post-123"}
+    assert ~p"/posts/#{struct}" == "/posts/post-123"
+    assert ~p"/posts/#{123}" == "/posts/123"
+    assert ~p|/posts/#{"a b"}| == "/posts/a%20b"
+  end
+
+  test "~p with static and dynamic string and query params" do
+    struct = %__MODULE__{id: 123, slug: "post-123"}
+
+    # static segments
+    assert ~p"/posts/1?foo=bar" == "/posts/1?foo=bar"
+    assert ~p"/posts/bottom/asc/10?foo=bar" == "/posts/bottom/asc/10?foo=bar"
+    assert path(conn_with_endpoint(), ~p"/posts/1?foo=bar") == "/posts/1?foo=bar"
+    assert path(@endpoint, @router, ~p"/posts/1?foo=bar") == "/posts/1?foo=bar"
+
+    assert path(conn_with_endpoint(), ~p"/posts/bottom/asc/10?foo=bar") ==
+             "/posts/bottom/asc/10?foo=bar"
+
+    assert path(@endpoint, @router, ~p"/posts/bottom/asc/10?foo=bar") ==
+             "/posts/bottom/asc/10?foo=bar"
+
+    # dynamic segments
+    assert ~p"/posts/#{struct}?#{[page: 1, spaced: "a b"]}" == "/posts/post-123?page=1&spaced=a+b"
+    assert ~p"/posts/#{struct}?#{[page: 1, spaced: "a b"]}" == "/posts/post-123?page=1&spaced=a+b"
+    id = 123
+    dir = "asc"
+    assert ~p"/posts/#{id}?foo=bar" == "/posts/123?foo=bar"
+    assert ~p"/posts/post-#{id}?foo=bar" == "/posts/post-123?foo=bar"
+    assert path(conn_with_endpoint(), ~p"/posts/#{id}?foo=bar") == "/posts/123?foo=bar"
+    assert path(@endpoint, @router, ~p"/posts/#{id}?foo=bar") == "/posts/123?foo=bar"
+
+    assert path(conn_with_endpoint(), ~p"/posts/bottom/#{dir}/#{id}?foo=bar") ==
+             "/posts/bottom/asc/123?foo=bar"
+
+    assert path(@endpoint, @router, ~p"/posts/bottom/#{dir}/#{id}?foo=bar") ==
+             "/posts/bottom/asc/123?foo=bar"
+  end
+
+  test "~p with dynamic string and static query params" do
+    struct = %__MODULE__{id: 123, slug: "post-123"}
+    assert ~p"/posts/#{struct}?foo=bar" == "/posts/post-123?foo=bar"
+  end
+
+  test "~p with scoped host" do
+    assert ~p"/host_users/1/info" == "/host_users/1/info"
+  end
+
+  test "~p on splat segments" do
+    assert ~p|/posts/file/#{1}/#{"2.jpg"}| == "/posts/file/1/2.jpg"
+
+    location = ["folder", "file.jpg"]
+    assert ~p|/posts/file/#{location}| == "/posts/file/folder/file.jpg"
+  end
+
+  test "~p URI encodes interpolated segments and query params" do
+    assert ~p"/posts/my path?#{[foo: "my param"]}" == "/posts/my%20path?foo=my+param"
+    slug = "my path"
+    assert ~p"/posts/#{slug}?#{[foo: "my param"]}" == "/posts/my%20path?foo=my+param"
+  end
+
+  test "~p raises on leftover sigil" do
+    assert_raise ArgumentError, "~p does not support trailing fragment, got: 'foo'", fn ->
+      defmodule LeftOver do
+        use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+        def test, do: ~p"/posts/1"foo
+      end
+    end
+  end
+
+  test "~p raises on dynamic dynamic interpolation" do
+    msg =
+      ~S|a dynamic ~p interpolation must be followed by a static segment, got: "/posts/#{1}#{2}"|
+
+    assert_raise ArgumentError, msg, fn ->
+      defmodule DynamicDynamic do
+        use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+        def test, do: ~p"/posts/#{1}#{2}"
+      end
+    end
+  end
+
+  test "~p warns on unmatched path" do
+    warnings =
+      ExUnit.CaptureIO.capture_io(:stderr, fn ->
+        defmodule Unmatched do
+          use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+
+          def test do
+            ~p"/unknown"
+            ~p"/unknown/123"
+            ~p"/unknown/#{123}"
+          end
+        end
+      end)
+
+    assert warnings =~ ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/unknown"|
+
+    assert warnings =~
+             ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/unknown/123"|
+
+    assert warnings =~
+             ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/unknown/#{123}"|
+  end
+
+  test "~p warns on warn_on_verify: true route" do
+    warnings =
+      ExUnit.CaptureIO.capture_io(:stderr, fn ->
+        defmodule VerifyFalse do
+          use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+
+          def test, do: ~p"/should-warn/foobar"
+        end
+      end)
+
+    assert warnings =~
+             ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/should-warn/foobar"|
+  end
+
+  test "~p raises when not prefixed by /" do
+    assert_raise ArgumentError,
+                 ~s|path segments must begin with /, got: "posts/1" in "posts/1"|,
+                 fn ->
+                   defmodule SigilPPrefix do
+                     use Phoenix.VerifiedRoutes,
+                       endpoint: unquote(@endpoint),
+                       router: unquote(@router)
+
+                     def test, do: ~p"posts/1"
+                   end
+                 end
+  end
+
+  test "path arities" do
+    assert path(Endpoint, ~p"/posts/1") == "/posts/1"
+    assert path(conn_with_endpoint(), ~p"/posts/1") == "/posts/1"
+    assert path(conn_with_script_name(), ~p"/posts/1") == "/api/posts/1"
+  end
+
+  test "url arities" do
+    assert url(~p"/posts/1") == "https://example.com/posts/1"
+    assert url(~p"/posts/#{123}") == "https://example.com/posts/123"
+    assert url(Endpoint, ~p"/posts/1") == "https://example.com/posts/1"
+    assert url(conn_with_endpoint(), ~p"/posts/1") == "https://example.com/posts/1"
+    assert url(conn_with_script_name(), ~p"/posts/1") == "https://example.com/api/posts/1"
+
+    assert url(Endpoint, Router, ~p"/posts/1") == "https://example.com/posts/1"
+    assert url(conn_with_endpoint(), Router, ~p"/posts/1") == "https://example.com/posts/1"
+    assert url(conn_with_script_name(), Router, ~p"/posts/1") == "https://example.com/api/posts/1"
+  end
+
+  test "path raises when non ~p is passed" do
+    assert_raise ArgumentError, ~r|expected compile-time ~p path string, got: "/posts/1"|, fn ->
+      defmodule MissingPathPrefix do
+        use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+        def test, do: path(%URI{}, "/posts/1")
+      end
+    end
+  end
+
+  test "url raises when non ~p is passed" do
+    assert_raise ArgumentError, ~r|expected compile-time ~p path string, got: "/posts/1"|, fn ->
+      defmodule MissingURLPrefix do
+        use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+        def test, do: url("/posts/1")
+      end
+    end
+  end
+
+  test "static_integrity" do
+    assert is_nil(static_integrity(Endpoint, "/images/foo.png"))
+    assert is_nil(static_integrity(conn_with_endpoint(), "/images/foo.png"))
+    assert is_nil(static_integrity(socket_with_endpoint(), "/images/foo.png"))
+  end
+
+  test "static paths" do
+    assert path(Endpoint, ~p"/images/foo.png") == "/images/foo.png"
+    assert path(conn_with_endpoint(), ~p"/images/foo.png") == "/images/foo.png"
+    assert path(socket_with_endpoint(), ~p"/images/foo.png") == "/images/foo.png"
+  end
+
+  test "~p dict query strings" do
+    assert ~p"/posts/5?#{[id: 3]}" == "/posts/5?id=3"
+    assert ~p"/posts/5?#{[foo: "bar"]}" == "/posts/5?foo=bar"
+    assert ~p"/posts/5?#{[foo: :bar]}" == "/posts/5?foo=bar"
+    assert ~p"/posts/5?#{[foo: true]}" == "/posts/5?foo=true"
+    assert ~p"/posts/5?#{[foo: false]}" == "/posts/5?foo=false"
+    assert ~p"/posts/5?#{[foo: nil]}" == "/posts/5?foo="
+
+    assert ~p"/posts/5?#{[foo: ~w(bar baz)]}" ==
+             "/posts/5?foo[]=bar&foo[]=baz"
+
+    assert ~p"/posts/5?#{[foo: %{id: 5}]}" ==
+             "/posts/5?foo[id]=5"
+
+    assert ~p"/posts/5?#{[foo: %{__struct__: Foo, id: 5}]}" ==
+             "/posts/5?foo=5"
+  end
+
+  test "~p mixed query string interpolation" do
+    dir = "asc"
+    page = "pg"
+
+    assert ~p"/posts/5?page=#{3}" == "/posts/5?page=3"
+    assert ~p"/posts/5?page=#{3}&dir=#{dir}" == "/posts/5?page=3&dir=asc"
+    assert ~p"/posts/5?#{page}=#{3}&dir=#{dir}" == "/posts/5?pg=3&dir=asc"
+    assert ~p"/posts/5?#{"a b"}=#{3}&dir=#{"a b"}" == "/posts/5?a+b=3&dir=a+b"
+
+    assert ~p"/posts/post?foo=bar&#{"key"}=#{"val"}&baz=bat" ==
+             "/posts/post?foo=bar&key=val&baz=bat"
+
+    assert ~p"/posts/#{page}?foo=bar&#{"key"}=#{"val"}&baz=bat" == "/posts/pg?foo=bar&key=val&baz=bat"
+  end
+
+  test "invalid mixed interpolation query string raises" do
+    msg =
+      ~S|interpolated query string params must be separated by &, got: "/posts/5?page=#{3}#{dir}"|
+
+    assert_raise ArgumentError, msg, fn ->
+      defmodule InvalidQuery do
+        use Phoenix.VerifiedRoutes,
+          endpoint: unquote(@endpoint),
+          router: unquote(@router)
+
+        def test do
+          ~p"/posts/5?page=#{3}#{dir}" == "/posts/5?page=3"
+        end
+      end
+    end
+  end
+
+  test "~p with complex ids" do
+    assert ~p|/posts/#{"==d--+"}| == "/posts/%3D%3Dd--%2B"
+    assert ~p|/posts/top?#{[id: "==d--+"]}| == "/posts/top?id=%3D%3Dd--%2B"
+
+    assert ~p|/posts/file/#{"==d--+"}/#{":O.jpg"}| == "/posts/file/%3D%3Dd--%2B/%3AO.jpg"
+
+    assert ~p|/posts/file/#{"==d--+"}/#{":O.jpg"}?#{[xx: "/=+/"]}| ==
+             "/posts/file/%3D%3Dd--%2B/%3AO.jpg?xx=%2F%3D%2B%2F"
+  end
+
+  test "~p with trailing slashes" do
+    assert ~p"/posts/5/" == "/posts/5/"
+    assert ~p"/posts/5/?#{[id: 5]}" == "/posts/5/?id=5"
+    assert ~p"/posts/5/?#{%{"id" => "foo"}}" == "/posts/5/?id=foo"
+    assert ~p"/posts/5/?#{%{"id" => "foo bar"}}" == "/posts/5/?id=foo+bar"
+  end
+
+  test "forwards" do
+    warnings =
+      ExUnit.CaptureIO.capture_io(:stderr, fn ->
+        defmodule Forwards do
+          use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: unquote(@router)
+
+          def test do
+            "/router_forward/dashboard" = ~p"/router_forward/dashboard"
+            "/router_forward/warn" = ~p"/router_forward/warn"
+            "/plug_forward/home" = ~p"/plug_forward/home"
+          end
+        end
+      end)
+
+    line = __ENV__.line - 6
+
+    warnings = String.replace(warnings, ~r/(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]/, "")
+
+    assert warnings ==
+             "warning: no route path for Phoenix.VerifiedRoutesTest.Router matches \"/router_forward/warn\"\n  test/phoenix/verified_routes_test.exs:#{line}: Phoenix.VerifiedRoutesTest.Forwards.test/0\n\n"
+  end
+
+  describe "with script name" do
+    @endpoint ScriptName
+    @router Router
+
+    test "paths use script name" do
+      assert ~p"/" == "/api/"
+      assert path(ScriptName, Router, ~p"/") == "/api/"
+      assert path(conn_with_script_name(), ~p"/") == "/api/"
+      assert path(uri_with_script_name(), ~p"/") == "/api/"
+      assert path(ScriptName, Router, ~p"/posts/5") == "/api/posts/5"
+      assert ~p"/posts/5" == "/api/posts/5"
+      assert path(ScriptName, Router, ~p"/posts/#{123}") == "/api/posts/123"
+      assert ~p"/posts/#{123}" == "/api/posts/123"
+      assert path(uri_with_script_name(), ~p"/posts/5") == "/api/posts/5"
+      assert path(uri_with_script_name(), ~p"/posts/#{123}") == "/api/posts/123"
+    end
+
+    test "urls use script name" do
+      assert url(ScriptName, ~p"/") == "https://example.com/api/"
+      assert url(conn_with_script_name(~w(foo)), ~p"/") == "https://example.com/foo/"
+      assert url(uri_with_script_name(), ~p"/") == "https://example.com:123/api/"
+      assert url(ScriptName, ~p"/posts/5") == "https://example.com/api/posts/5"
+      assert url(ScriptName, ~p"/posts/#{123}") == "https://example.com/api/posts/123"
+      assert url(conn_with_script_name(), ~p"/posts/5") == "https://example.com/api/posts/5"
+
+      assert url(conn_with_script_name(~w(foo)), ~p"/posts/5") ==
+               "https://example.com/foo/posts/5"
+
+      assert url(uri_with_script_name(), ~p"/posts/5") == "https://example.com:123/api/posts/5"
+    end
+
+    test "static use endpoint script name only" do
+      assert path(conn_with_script_name(~w(foo)), ~p"/images/foo.png") ==
+               "/api/images/foo.png"
+
+      assert url(conn_with_script_name(~w(foo)), ~p"/images/foo.png") ==
+               "https://static.example.com/api/images/foo.png"
+    end
+
+    test "phoenix_router_url with string takes precedence over endpoint" do
+      url = "https://phoenixframework.org"
+      conn = Phoenix.Controller.put_router_url(conn_with_endpoint(), url)
+
+      assert url(conn, ~p"/") == url <> "/"
+      assert url(conn, ~p"/admin/new/messages/1") == url <> "/admin/new/messages/1"
+      assert url(conn, ~p"/admin/new/messages/#{123}") == url <> "/admin/new/messages/123"
+    end
+
+    test "phoenix_router_url with URI takes precedence over endpoint" do
+      uri = %URI{scheme: "https", host: "phoenixframework.org", port: 123, path: "/path"}
+      conn = Phoenix.Controller.put_router_url(conn_with_endpoint(), uri)
+
+      assert url(conn, ~p"/") == "https://phoenixframework.org:123/path/"
+
+      assert url(conn, ~p"/admin/new/messages/1") ==
+               "https://phoenixframework.org:123/path/admin/new/messages/1"
+    end
+
+    test "phoenix_static_url with string takes precedence over endpoint" do
+      url = "https://phoenixframework.org"
+
+      conn = Phoenix.Controller.put_static_url(conn_with_endpoint(), url)
+      assert url(conn, ~p"/images/foo.png") == url <> "/images/foo.png"
+
+      conn = Phoenix.Controller.put_static_url(conn_with_script_name(), url)
+      assert url(conn, ~p"/images/foo.png") == url <> "/images/foo.png"
+    end
+
+    test "phoenix_static_url set to string with path results in static url with that path" do
+      url = "https://phoenixframework.org/path"
+      conn = Phoenix.Controller.put_static_url(conn_with_endpoint(), url)
+      assert url(conn, ~p"/images/foo.png") == url <> "/images/foo.png"
+
+      conn = Phoenix.Controller.put_static_url(conn_with_script_name(), url)
+      assert url(conn, ~p"/images/foo.png") == url <> "/images/foo.png"
+    end
+
+    test "phoenix_static_url with URI takes precedence over endpoint" do
+      uri = %URI{scheme: "https", host: "phoenixframework.org", port: 123}
+
+      conn = Phoenix.Controller.put_static_url(conn_with_endpoint(), uri)
+      assert url(conn, ~p"/images/foo.png") == "https://phoenixframework.org:123/images/foo.png"
+
+      conn = Phoenix.Controller.put_static_url(conn_with_script_name(), uri)
+      assert url(conn, ~p"/images/foo.png") == "https://phoenixframework.org:123/images/foo.png"
+    end
+
+    test "phoenix_static_url set to URI with path results in static url with that path" do
+      uri = %URI{scheme: "https", host: "phoenixframework.org", port: 123, path: "/path"}
+
+      conn = Phoenix.Controller.put_static_url(conn_with_endpoint(), uri)
+
+      assert url(conn, ~p"/images/foo.png") ==
+               "https://phoenixframework.org:123/path/images/foo.png"
+
+      conn = Phoenix.Controller.put_static_url(conn_with_script_name(), uri)
+
+      assert url(conn, ~p"/images/foo.png") ==
+               "https://phoenixframework.org:123/path/images/foo.png"
+    end
+  end
+end