Skip to content

Commit 13df150

Browse files
Enable DependencyTracker to evaluate interpolated paths (rails#50944)
* Enable RubyTracker to evaluate interpolated paths Previously, neither the PrismRenderParser nor the RipperRenderParser would consider an interpolated string as a dependency. The RubyTracker even included a line to explcitly filter out interpolated paths returned from the RipperRenderParser since they would end in a "/". However, the RubyTracker does include the ability to evaluate explicit "Template Dependency" comments with wildcard nodes. This commit extends the RipperRenderParser and PrismRenderParser to convert interpolated strings into wildcard globs. Additionally, it changes the RubyTracker to evaluate wildcards the same for both implicit and explicit dependencies. This enables the RubyTracker to identify potential dependencies for interpolated renders, which it was previously unable to do. * Enable ERBTracker to evaluate interpolated paths This ensures all three implementations (ERBTracker, RipperRenderParser, and PrismRenderParser) are consistent in their ability to evaluate interpolated paths. * Extract WildcardResolver to remove duplication Since both the ERBTracker and RubyTracker now support resolving interpolated template paths against the view_paths, the logic for this resolution can be extracted to its own class. * Update CHANGELOG.md --------- Co-authored-by: John Hawthorn <[email protected]>
1 parent e09dd95 commit 13df150

File tree

9 files changed

+126
-52
lines changed

9 files changed

+126
-52
lines changed

actionview/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
* Enable DependencyTracker to evaluate renders with trailing interpolation.
2+
3+
```erb
4+
<%= render "maintenance_tasks/runs/info/#{run.status}" %>
5+
```
6+
7+
Previously, the DependencyTracker would ignore this render, but now it will
8+
mark all partials in the "maintenance_tasks/runs/info" folder as
9+
dependencies.
10+
11+
*Hartley McGuire*
12+
113
* Rename `text_area` methods into `textarea`
214
315
Old names are still available as aliases.

actionview/lib/action_view/dependency_tracker.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class DependencyTracker # :nodoc:
1010

1111
autoload :ERBTracker
1212
autoload :RubyTracker
13+
autoload :WildcardResolver
1314

1415
@trackers = Concurrent::Map.new
1516

actionview/lib/action_view/dependency_tracker/erb_tracker.rb

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def initialize(name, template, view_paths = nil)
7474
end
7575

7676
def dependencies
77-
render_dependencies + explicit_dependencies
77+
WildcardResolver.new(@view_paths, render_dependencies + explicit_dependencies).resolve
7878
end
7979

8080
attr_reader :name, :template
@@ -90,15 +90,15 @@ def directory
9090
end
9191

9292
def render_dependencies
93-
render_dependencies = []
93+
dependencies = []
9494
render_calls = source.split(/\brender\b/).drop(1)
9595

9696
render_calls.each do |arguments|
97-
add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
98-
add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
97+
add_dependencies(dependencies, arguments, LAYOUT_DEPENDENCY)
98+
add_dependencies(dependencies, arguments, RENDER_ARGUMENTS)
9999
end
100100

101-
render_dependencies.uniq
101+
dependencies
102102
end
103103

104104
def add_dependencies(render_dependencies, arguments, pattern)
@@ -116,12 +116,33 @@ def add_dynamic_dependency(dependencies, dependency)
116116
end
117117

118118
def add_static_dependency(dependencies, dependency, quote_type)
119-
if quote_type == '"'
120-
# Ignore if there is interpolation
121-
return if dependency.include?('#{')
122-
end
119+
if quote_type == '"' && dependency.include?('#{')
120+
scanner = StringScanner.new(dependency)
123121

124-
if dependency
122+
wildcard_dependency = +""
123+
124+
while !scanner.eos?
125+
next unless scanner.scan_until(/\#{/)
126+
127+
unmatched_brackets = 1
128+
wildcard_dependency << scanner.pre_match
129+
130+
while unmatched_brackets > 0 && !scanner.eos?
131+
scanner.scan_until(/[{}]/)
132+
133+
case scanner.matched
134+
when "{"
135+
unmatched_brackets += 1
136+
when "}"
137+
unmatched_brackets -= 1
138+
end
139+
end
140+
141+
wildcard_dependency << "*"
142+
end
143+
144+
dependencies << wildcard_dependency
145+
elsif dependency
125146
if dependency.include?("/")
126147
dependencies << dependency
127148
else
@@ -130,24 +151,8 @@ def add_static_dependency(dependencies, dependency, quote_type)
130151
end
131152
end
132153

133-
def resolve_directories(wildcard_dependencies)
134-
return [] unless @view_paths
135-
return [] if wildcard_dependencies.empty?
136-
137-
# Remove trailing "/*"
138-
prefixes = wildcard_dependencies.map { |query| query[0..-3] }
139-
140-
@view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
141-
path.to_s if prefixes.include?(path.prefix)
142-
}.sort
143-
end
144-
145154
def explicit_dependencies
146-
dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
147-
148-
wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") }
149-
150-
(explicits + resolve_directories(wildcards)).uniq
155+
source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
151156
end
152157
end
153158
end

actionview/lib/action_view/dependency_tracker/ruby_tracker.rb

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def self.call(name, template, view_paths = nil)
1010
end
1111

1212
def dependencies
13-
render_dependencies + explicit_dependencies
13+
WildcardResolver.new(view_paths, render_dependencies + explicit_dependencies).resolve
1414
end
1515

1616
def self.supports_view_paths? # :nodoc:
@@ -31,29 +31,12 @@ def render_dependencies
3131
compiled_source = template.handler.call(template, template.source)
3232

3333
@parser_class.new(@name, compiled_source).render_calls.filter_map do |render_call|
34-
next if render_call.end_with?("/_")
3534
render_call.gsub(%r|/_|, "/")
3635
end
3736
end
3837

3938
def explicit_dependencies
40-
dependencies = template.source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
41-
42-
wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") }
43-
44-
(explicits + resolve_directories(wildcards)).uniq
45-
end
46-
47-
def resolve_directories(wildcard_dependencies)
48-
return [] unless view_paths
49-
return [] if wildcard_dependencies.empty?
50-
51-
# Remove trailing "/*"
52-
prefixes = wildcard_dependencies.map { |query| query[0..-3] }
53-
54-
view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
55-
path.to_s if prefixes.include?(path.prefix)
56-
}.sort
39+
template.source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
5740
end
5841
end
5942
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
module ActionView
4+
class DependencyTracker # :nodoc:
5+
class WildcardResolver # :nodoc:
6+
def initialize(view_paths, dependencies)
7+
@view_paths = view_paths
8+
9+
@wildcard_dependencies, @explicit_dependencies =
10+
dependencies.partition { |dependency| dependency.end_with?("/*") }
11+
end
12+
13+
def resolve
14+
return explicit_dependencies.uniq if !view_paths || wildcard_dependencies.empty?
15+
16+
(explicit_dependencies + resolved_wildcard_dependencies).uniq
17+
end
18+
19+
private
20+
attr_reader :explicit_dependencies, :wildcard_dependencies, :view_paths
21+
22+
def resolved_wildcard_dependencies
23+
# Remove trailing "/*"
24+
prefixes = wildcard_dependencies.map { |query| query[0..-3] }
25+
26+
view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
27+
path.to_s if prefixes.include?(path.prefix)
28+
}.sort
29+
end
30+
end
31+
end
32+
end

actionview/lib/action_view/helpers/cache_helper.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ class UncacheableFragmentError < StandardError; end
9393
# render partial: 'attachments/attachment', collection: group_of_attachments
9494
# render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
9595
#
96+
# One last type of dependency can be determined implicitly:
97+
#
98+
# render "maintenance_tasks/runs/info/#{run.status}"
99+
#
100+
# Because the value passed to render ends in interpolation, Action View
101+
# will mark all partials within the "maintenace_tasks/runs/info" folder as
102+
# dependencies.
103+
#
96104
# === Explicit dependencies
97105
#
98106
# Sometimes you'll have template dependencies that can't be derived at all. This is typically

actionview/lib/action_view/render_parser/prism_render_parser.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,21 @@ def render_call_options(node)
9797
def render_call_template(node)
9898
object_template = false
9999
template =
100-
if node.is_a?(Prism::StringNode)
100+
case node.type
101+
when :string_node
101102
path = node.unescaped
102103
path.include?("/") ? path : "#{directory}/#{path}"
104+
when :interpolated_string_node
105+
node.parts.map do |node|
106+
case node.type
107+
when :string_node
108+
node.unescaped
109+
when :embedded_statements_node
110+
"*"
111+
else
112+
return
113+
end
114+
end.join("")
103115
else
104116
dependency =
105117
case node.type

actionview/lib/action_view/render_parser/ripper_render_parser.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,16 @@ def call_method_name
6666

6767
def to_string
6868
raise unless string?
69-
self[0][0][0]
69+
70+
# s(:string_literal, s(:string_content, map))
71+
self[0].map do |node|
72+
case node.type
73+
when :@tstring_content
74+
node[0]
75+
when :string_embexpr
76+
"*"
77+
end
78+
end.join("")
7079
end
7180

7281
def hash?

actionview/test/template/dependency_tracker_test.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,31 @@ def test_dependencies_with_interpolation
223223

224224
assert_equal ["single/\#{quote}"], tracker.dependencies
225225
end
226+
227+
def test_dependencies_with_interpolation_are_resolved_with_view_paths
228+
view_paths = ActionView::PathSet.new([File.expand_path("../fixtures/digestor", __dir__)])
229+
230+
template = FakeTemplate.new(%q{
231+
<%= render "events/#{quote}" %>
232+
}, :erb)
233+
234+
tracker = make_tracker("interpolation/_string", template, view_paths)
235+
236+
assert_equal ["events/_completed", "events/_event", "events/index"], tracker.dependencies
237+
end
226238
end
227239

228240
class ERBTrackerTest < ActiveSupport::TestCase
229241
include SharedTrackerTests
230242

231-
def make_tracker(name, template)
232-
ActionView::DependencyTracker::ERBTracker.new(name, template)
243+
def make_tracker(name, template, view_paths = nil)
244+
ActionView::DependencyTracker::ERBTracker.new(name, template, view_paths)
233245
end
234246
end
235247

236248
module RubyTrackerTests
237-
def make_tracker(name, template)
238-
ActionView::DependencyTracker::RubyTracker.new(name, template, parser_class: parser_class)
249+
def make_tracker(name, template, view_paths = nil)
250+
ActionView::DependencyTracker::RubyTracker.new(name, template, view_paths, parser_class: parser_class)
239251
end
240252

241253
def test_dependencies_skip_unknown_options

0 commit comments

Comments
 (0)