Skip to content

Commit a00e548

Browse files
composerinteraliajhawthornluanzeba
committed
Print source location when inspecting routes
In larger route files, or when routes are spread across multiple files, it can be difficult to get from the output of the route inspector to the relevant route definition. This commit adds a route source location to the route, and uses that in the HtmlTableFormatter (for rails/info and the debug exceptions middleware) and the Expanded formatter (for `rails routes -E`). To avoid doing extra work in production, it only sets the source location in development. This commit injects the application's backtrace cleaner so we can use it to remove the rails root from the path. This also means we don't get source locations for the routes defined by Rails. If mounting an engine from a gem, we'll get a source location for where we mount it in the application, but not for the routes defined in the gem itself. That's probably good enough, since Rails already prints routes for an engine separately under the title "Routes for Foo::Engine". Co-authored-by: John Hawthorn <[email protected]> Co-authored-by: Luan Vieira <[email protected]> Co-authored-by: Daniel Colson <[email protected]>
1 parent 946fc17 commit a00e548

File tree

7 files changed

+63
-28
lines changed

7 files changed

+63
-28
lines changed

actionpack/lib/action_dispatch/journey/route.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module ActionDispatch
55
module Journey
66
class Route
77
attr_reader :app, :path, :defaults, :name, :precedence, :constraints,
8-
:internal, :scope_options, :ast
8+
:internal, :scope_options, :ast, :source_location
99

1010
alias :conditions :constraints
1111

@@ -53,7 +53,7 @@ def self.verb_matcher(verb)
5353
##
5454
# +path+ is a path constraint.
5555
# +constraints+ is a hash of constraints to be applied to this route.
56-
def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false)
56+
def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil)
5757
@name = name
5858
@app = app
5959
@path = path
@@ -69,6 +69,7 @@ def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], d
6969
@path_formatter = @path.build_formatter
7070
@scope_options = scope_options
7171
@internal = internal
72+
@source_location = source_location
7273

7374
@ast = @path.ast.root
7475
@path.ast.route = self

actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@
1313
<td>
1414
<%=simple_format route[:reqs] %>
1515
</td>
16+
<td>
17+
<%=simple_format route[:source_location] %>
18+
</td>
1619
</tr>

actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@
9090
<th class="http-verb">HTTP Verb</th>
9191
<th>Path</th>
9292
<th>Controller#Action</th>
93+
<th>Source Location</th>
9394
</tr>
9495
<tr>
95-
<th colspan="4" id="search_container"><%= search_field(:query, nil, id: 'search', placeholder: "Search") %></th>
96+
<th colspan="5" id="search_container"><%= search_field(:query, nil, id: 'search', placeholder: "Search") %></th>
9697
</tr>
9798
</thead>
9899
<tbody class='exact_matches' id='exact_matches'>

actionpack/lib/action_dispatch/railtie.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class Railtie < Rails::Railtie # :nodoc:
6767
config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
6868
ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
6969

70+
ActionDispatch::Routing::Mapper.route_source_locations = Rails.env.development?
71+
ActionDispatch::Routing::Mapper.backtrace_cleaner = Rails.backtrace_cleaner
72+
7073
ActionDispatch.test_app = app
7174
end
7275
end

actionpack/lib/action_dispatch/routing/inspector.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ def collect_routes(routes)
133133
{ name: route.name,
134134
verb: route.verb,
135135
path: route.path,
136-
reqs: route.reqs }
136+
reqs: route.reqs,
137+
source_location: route.source_location }
137138
end
138139
end
139140

@@ -239,13 +240,16 @@ def section(routes)
239240
private
240241
def draw_expanded_section(routes)
241242
routes.map.each_with_index do |r, i|
242-
<<~MESSAGE.chomp
243+
route_rows = <<~MESSAGE.chomp
243244
#{route_header(index: i + 1)}
244245
Prefix | #{r[:name]}
245246
Verb | #{r[:verb]}
246247
URI | #{r[:path]}
247248
Controller#Action | #{r[:reqs]}
248249
MESSAGE
250+
source_location = "\nSource Location | #{r[:source_location]}"
251+
route_rows += source_location if r[:source_location].present?
252+
route_rows
249253
end
250254
end
251255

actionpack/lib/action_dispatch/routing/mapper.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ module Routing
1212
class Mapper
1313
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
1414

15+
cattr_accessor :route_source_locations, instance_accessor: false, default: false
16+
cattr_accessor :backtrace_cleaner, instance_accessor: false, default: ActiveSupport::BacktraceCleaner.new
17+
1518
class Constraints < Routing::Endpoint # :nodoc:
1619
attr_reader :app, :constraints
1720

@@ -170,7 +173,7 @@ def make_route(name, precedence)
170173
Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
171174
required_defaults: required_defaults, defaults: defaults,
172175
request_method_match: request_method, precedence: precedence,
173-
scope_options: scope_options, internal: @internal)
176+
scope_options: scope_options, internal: @internal, source_location: route_source_location)
174177
end
175178

176179
def application
@@ -356,6 +359,15 @@ def constraints(options, path_params)
356359
def dispatcher(raise_on_name_error)
357360
Routing::RouteSet::Dispatcher.new raise_on_name_error
358361
end
362+
363+
def route_source_location
364+
if Mapper.route_source_locations
365+
action_dispatch_dir = File.expand_path("..", __dir__)
366+
caller_location = caller_locations.find { |location| !location.path.include?(action_dispatch_dir) }
367+
cleaned_path = Mapper.backtrace_cleaner.clean([caller_location.path]).first
368+
"#{cleaned_path}:#{caller_location.lineno}" if cleaned_path
369+
end
370+
end
359371
end
360372

361373
# Invokes Journey::Router::Utils.normalize_path, then ensures that

actionpack/test/dispatch/routing/inspector_test.rb

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,14 @@ def test_routes_can_be_filtered
317317
end
318318

319319
def test_routes_when_expanded
320+
ActionDispatch::Routing::Mapper.route_source_locations = true
320321
engine = Class.new(Rails::Engine) do
321322
def self.inspect
322323
"Blog::Engine"
323324
end
324325
end
326+
file_name = ActiveSupport::BacktraceCleaner.new.clean([__FILE__]).first
327+
lineno = __LINE__
325328
engine.routes.draw do
326329
get "/cart", to: "cart#show"
327330
end
@@ -332,28 +335,36 @@ def self.inspect
332335
mount engine => "/blog", :as => "blog"
333336
end
334337

335-
assert_equal ["--[ Route 1 ]----------",
336-
"Prefix | custom_assets",
337-
"Verb | GET",
338-
"URI | /custom/assets(.:format)",
339-
"Controller#Action | custom_assets#show",
340-
"--[ Route 2 ]----------",
341-
"Prefix | custom_furnitures",
342-
"Verb | GET",
343-
"URI | /custom/furnitures(.:format)",
344-
"Controller#Action | custom_furnitures#show",
345-
"--[ Route 3 ]----------",
346-
"Prefix | blog",
347-
"Verb | ",
348-
"URI | /blog",
349-
"Controller#Action | Blog::Engine",
350-
"",
351-
"[ Routes for Blog::Engine ]",
352-
"--[ Route 1 ]----------",
353-
"Prefix | cart",
354-
"Verb | GET",
355-
"URI | /cart(.:format)",
356-
"Controller#Action | cart#show"], output
338+
expected = ["--[ Route 1 ]----------",
339+
"Prefix | custom_assets",
340+
"Verb | GET",
341+
"URI | /custom/assets(.:format)",
342+
"Controller#Action | custom_assets#show",
343+
"Source Location | #{file_name}:#{lineno + 6}",
344+
"--[ Route 2 ]----------",
345+
"Prefix | custom_furnitures",
346+
"Verb | GET",
347+
"URI | /custom/furnitures(.:format)",
348+
"Controller#Action | custom_furnitures#show",
349+
"Source Location | #{file_name}:#{lineno + 7}",
350+
"--[ Route 3 ]----------",
351+
"Prefix | blog",
352+
"Verb | ",
353+
"URI | /blog",
354+
"Controller#Action | Blog::Engine",
355+
"Source Location | #{file_name}:#{lineno + 8}",
356+
"",
357+
"[ Routes for Blog::Engine ]",
358+
"--[ Route 1 ]----------",
359+
"Prefix | cart",
360+
"Verb | GET",
361+
"URI | /cart(.:format)",
362+
"Controller#Action | cart#show",
363+
"Source Location | #{file_name}:#{lineno + 2}"]
364+
365+
assert_equal expected, output
366+
ensure
367+
ActionDispatch::Routing::Mapper.route_source_locations = false
357368
end
358369

359370
def test_no_routes_matched_filter_when_expanded

0 commit comments

Comments
 (0)