Skip to content

Commit 001f7a4

Browse files
authored
Merge pull request rails#49438 from kddnewton/prism-render-parser
Use prism for parsing renders when available
2 parents b68cc94 + d743a2b commit 001f7a4

File tree

6 files changed

+309
-183
lines changed

6 files changed

+309
-183
lines changed

actionview/lib/action_view/dependency_tracker.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class DependencyTracker # :nodoc:
99
extend ActiveSupport::Autoload
1010

1111
autoload :ERBTracker
12-
autoload :RipperTracker
12+
autoload :RubyTracker
1313

1414
@trackers = Concurrent::Map.new
1515

actionview/lib/action_view/dependency_tracker/ripper_tracker.rb renamed to actionview/lib/action_view/dependency_tracker/ruby_tracker.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module ActionView
44
class DependencyTracker # :nodoc:
5-
class RipperTracker # :nodoc:
5+
class RubyTracker # :nodoc:
66
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
77

88
def self.call(name, template, view_paths = nil)
Lines changed: 25 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,19 @@
11
# frozen_string_literal: true
22

3-
require "action_view/ripper_ast_parser"
4-
53
module ActionView
6-
class RenderParser # :nodoc:
7-
def initialize(name, code)
8-
@name = name
9-
@code = code
10-
@parser = RipperASTParser
11-
end
12-
13-
def render_calls
14-
render_nodes = @parser.parse_render_nodes(@code)
15-
16-
render_nodes.map do |method, nodes|
17-
nodes.map { |n| send(:parse_render, n) }
18-
end.flatten.compact
19-
end
20-
21-
private
22-
def directory
23-
File.dirname(@name)
24-
end
25-
26-
def resolve_path_directory(path)
27-
if path.include?("/")
28-
path
29-
else
30-
"#{directory}/#{path}"
31-
end
32-
end
33-
34-
# Convert
35-
# render("foo", ...)
36-
# into either
37-
# render(template: "foo", ...)
38-
# or
39-
# render(partial: "foo", ...)
40-
def normalize_args(string, options_hash)
41-
if options_hash
42-
{ partial: string, locals: options_hash }
43-
else
44-
{ partial: string }
45-
end
46-
end
47-
48-
def parse_render(node)
49-
node = node.argument_nodes
50-
51-
if (node.length == 1 || node.length == 2) && !node[0].hash?
52-
if node.length == 1
53-
options = normalize_args(node[0], nil)
54-
elsif node.length == 2
55-
options = normalize_args(node[0], node[1])
56-
end
57-
58-
return nil unless options
4+
module RenderParser # :nodoc:
5+
ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
6+
RENDER_TYPE_KEYS = [:partial, :template, :layout]
597

60-
parse_render_from_options(options)
61-
elsif node.length == 1 && node[0].hash?
62-
options = parse_hash_to_symbols(node[0])
63-
64-
return nil unless options
65-
66-
parse_render_from_options(options)
67-
else
68-
nil
69-
end
70-
end
71-
72-
def parse_hash(node)
73-
node.hash? && node.to_hash
74-
end
75-
76-
def parse_hash_to_symbols(node)
77-
hash = parse_hash(node)
78-
79-
return unless hash
80-
81-
hash.transform_keys do |key_node|
82-
key = parse_sym(key_node)
83-
84-
return unless key
85-
86-
key
87-
end
88-
end
89-
90-
ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
91-
92-
RENDER_TYPE_KEYS =
93-
[:partial, :template, :layout]
94-
95-
def parse_render_from_options(options_hash)
96-
renders = []
97-
keys = options_hash.keys
98-
99-
if (keys & RENDER_TYPE_KEYS).size < 1
100-
# Must have at least one of render keys
101-
return nil
102-
end
103-
104-
if (keys - ALL_KNOWN_KEYS).any?
105-
# de-opt in case of unknown option
106-
return nil
107-
end
108-
109-
render_type = (keys & RENDER_TYPE_KEYS)[0]
110-
111-
node = options_hash[render_type]
112-
113-
if node.string?
114-
template = resolve_path_directory(node.to_string)
115-
else
116-
if node.variable_reference?
117-
dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
118-
elsif node.vcall?
119-
dependency = node.variable_name
120-
elsif node.call?
121-
dependency = node.call_method_name
122-
else
123-
return
124-
end
125-
126-
object_template = true
127-
template = "#{dependency.pluralize}/#{dependency.singularize}"
128-
end
129-
130-
return unless template
131-
132-
if spacer_template = render_template_with_spacer?(options_hash)
133-
virtual_path = partial_to_virtual_path(:partial, spacer_template)
134-
renders << virtual_path
135-
end
136-
137-
if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
138-
return nil if options_hash.key?(:object) && options_hash.key?(:collection)
139-
return nil unless options_hash.key?(:partial)
140-
end
141-
142-
virtual_path = partial_to_virtual_path(render_type, template)
143-
renders << virtual_path
144-
145-
# Support for rendering multiple templates (i.e. a partial with a layout)
146-
if layout_template = render_template_with_layout?(render_type, options_hash)
147-
virtual_path = partial_to_virtual_path(:layout, layout_template)
148-
149-
renders << virtual_path
150-
end
151-
152-
renders
153-
end
154-
155-
def parse_str(node)
156-
node.string? && node.to_string
157-
end
158-
159-
def parse_sym(node)
160-
node.symbol? && node.to_symbol
8+
class Base # :nodoc:
9+
def initialize(name, code)
10+
@name = name
11+
@code = code
16112
end
16213

16314
private
164-
def render_template_with_layout?(render_type, options_hash)
165-
if render_type != :layout && options_hash.key?(:layout)
166-
parse_str(options_hash[:layout])
167-
end
168-
end
169-
170-
def render_template_with_spacer?(options_hash)
171-
if options_hash.key?(:spacer_template)
172-
parse_str(options_hash[:spacer_template])
173-
end
15+
def directory
16+
File.dirname(@name)
17417
end
17518

17619
def partial_to_virtual_path(render_type, partial_path)
@@ -180,9 +23,22 @@ def partial_to_virtual_path(render_type, partial_path)
18023
partial_path
18124
end
18225
end
26+
end
18327

184-
def layout_to_virtual_path(layout_path)
185-
"layouts/#{layout_path}"
186-
end
28+
# Check if prism is available. If it is, use it. Otherwise, use ripper.
29+
begin
30+
require "prism"
31+
rescue LoadError
32+
require "ripper"
33+
require_relative "render_parser/ripper_render_parser"
34+
Parser = RipperRenderParser
35+
else
36+
require_relative "render_parser/prism_render_parser"
37+
Parser = PrismRenderParser
38+
end
39+
40+
def self.new(name, code)
41+
Parser.new(name, code)
42+
end
18743
end
18844
end
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# frozen_string_literal: true
2+
3+
module ActionView
4+
module RenderParser
5+
class PrismRenderParser < Base # :nodoc:
6+
def render_calls
7+
queue = [Prism.parse(@code).value]
8+
templates = []
9+
10+
while (node = queue.shift)
11+
queue.concat(node.compact_child_nodes)
12+
next unless node.is_a?(Prism::CallNode)
13+
14+
options = render_call_options(node)
15+
next unless options
16+
17+
render_type = (options.keys & RENDER_TYPE_KEYS)[0]
18+
template, object_template = render_call_template(options[render_type])
19+
next unless template
20+
21+
if options.key?(:object) || options.key?(:collection) || object_template
22+
next if options.key?(:object) && options.key?(:collection)
23+
next unless options.key?(:partial)
24+
end
25+
26+
if options[:spacer_template].is_a?(Prism::StringNode)
27+
templates << partial_to_virtual_path(:partial, options[:spacer_template].unescaped)
28+
end
29+
30+
templates << partial_to_virtual_path(render_type, template)
31+
32+
if render_type != :layout && options[:layout].is_a?(Prism::StringNode)
33+
templates << partial_to_virtual_path(:layout, options[:layout].unescaped)
34+
end
35+
end
36+
37+
templates
38+
end
39+
40+
private
41+
# Accept a call node and return a hash of options for the render call.
42+
# If it doesn't match the expected format, return nil.
43+
def render_call_options(node)
44+
# We are only looking for calls to render or render_to_string.
45+
name = node.name.to_sym
46+
return if name != :render && name != :render_to_string
47+
48+
# We are only looking for calls with arguments.
49+
arguments = node.arguments
50+
return unless arguments
51+
52+
arguments = arguments.arguments
53+
length = arguments.length
54+
55+
# Get rid of any parentheses to get directly to the contents.
56+
arguments.map! do |argument|
57+
current = argument
58+
59+
while current.is_a?(Prism::ParenthesesNode) &&
60+
current.body.is_a?(Prism::StatementsNode) &&
61+
current.body.body.length == 1
62+
current = current.body.body.first
63+
end
64+
65+
current
66+
end
67+
68+
# We are only looking for arguments that are either a string with an
69+
# array of locals or a keyword hash with symbol keys.
70+
options =
71+
if (length == 1 || length == 2) && !arguments[0].is_a?(Prism::KeywordHashNode)
72+
{ partial: arguments[0], locals: arguments[1] }
73+
elsif length == 1 &&
74+
arguments[0].is_a?(Prism::KeywordHashNode) &&
75+
arguments[0].elements.all? do |element|
76+
element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)
77+
end
78+
arguments[0].elements.to_h do |element|
79+
[element.key.unescaped.to_sym, element.value]
80+
end
81+
end
82+
83+
return unless options
84+
85+
# Here we validate that the options have the keys we expect.
86+
keys = options.keys
87+
return if (keys & RENDER_TYPE_KEYS).empty?
88+
return if (keys - ALL_KNOWN_KEYS).any?
89+
90+
# Finally, we can return a valid set of options.
91+
options
92+
end
93+
94+
# Accept the node that is being passed in the position of the template
95+
# and return the template name and whether or not it is an object
96+
# template.
97+
def render_call_template(node)
98+
object_template = false
99+
template =
100+
if node.is_a?(Prism::StringNode)
101+
path = node.unescaped
102+
path.include?("/") ? path : "#{directory}/#{path}"
103+
else
104+
dependency =
105+
case node.type
106+
when :class_variable_read_node
107+
node.slice[2..]
108+
when :instance_variable_read_node
109+
node.slice[1..]
110+
when :global_variable_read_node
111+
node.slice[1..]
112+
when :local_variable_read_node
113+
node.slice
114+
when :call_node
115+
node.name
116+
else
117+
return
118+
end
119+
120+
"#{dependency.pluralize}/#{dependency.singularize}"
121+
end
122+
123+
[template, object_template]
124+
end
125+
end
126+
end
127+
end

0 commit comments

Comments
 (0)