actionview_precompiler 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f3734d67f86056784f7949e66fc5027be5cc29eb554fdc69a0b00f4692c0254
4
- data.tar.gz: 010ffab3de043d3d71a6d0d58c3e6a2789f164f8edd5b18c54ce5b7429a266ca
3
+ metadata.gz: 58e32739b081a1318cdd69ea8bb88286f7921c1028e3081738ecda419d68b25f
4
+ data.tar.gz: 6309ed0e14d1261ea9f184c8f0522d6cf38b1c3a045a09537f01b8a13841bcae
5
5
  SHA512:
6
- metadata.gz: ca66b59f8768cdfe813b17dd9277211e269da5d978013ae9dda7b3e07d588a2b0a8c412849f8f5c866d44b23b3eadf6327ce92366f7b29888c131c48f6585c3b
7
- data.tar.gz: dfb74620b2c5aaa56e008ce39172654cec82c98d951e3af57f63cea0d55f027d06489b48f412dfcee7efd46ba75ce6280d2456c82493dd9ad7094476c89ccc58
6
+ metadata.gz: f4476e8391491ca650bb1dbf3f946160c631a506a9abe7f20760e07ba6e1c94229e443579d7961b17301a6487a819d0c942f1c0261040b6110db45a6d3fda0ca
7
+ data.tar.gz: d7abd504555763b20cfbbd9afec57a67a1caf032fb1a4e3f571ca9d12f8fc58e72570c2437e42f26ec7e3f6d746cc383a35a90b38dc0758f4a0a3f919a37023f
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ActionviewPrecompiler
2
2
 
3
- Provides eager loading of ActionView templates.
3
+ Provides eager loading of Action View templates.
4
4
 
5
5
  This optimization aims to improve cold render times and to allow more memory to be shared via CoW on forking web servers.
6
6
 
@@ -31,7 +31,7 @@ We determine the locals passed to each template by parsing all templates looking
31
31
  Right now this assumes every template with the same `virtual_path` takes the same locals (there may be smarter options, we just aren't doing them).
32
32
  A curse/blessing/actually still a curse of this approach is that mis-predicting render calls doesn't cause any issues, it just wastes RAM.
33
33
 
34
- Templates are half-compiled using standard ActionView handlers, so this should work for erb/builder/haml/whatever.
34
+ Templates are half-compiled using standard Action View handlers, so this should work for erb/builder/haml/whatever.
35
35
  Parsing is done using either Ruby 2.6's `RubyVM::AbstractSyntaxTree` or JRuby's parser.
36
36
 
37
37
  ## Installation
@@ -1,5 +1,5 @@
1
1
  module ActionviewPrecompiler
2
- module ASTParser
2
+ module JRubyASTParser
3
3
  class Node
4
4
  def self.wrap(node)
5
5
  if org::jruby::ast::Node === node
@@ -25,14 +25,37 @@ module ActionviewPrecompiler
25
25
  def string?; org::jruby::ast::StrNode === @node; end
26
26
  def symbol?; org::jruby::ast::SymbolNode === @node; end
27
27
 
28
+ def variable_reference?
29
+ org::jruby::ast::InstVarNode === @node ||
30
+ org::jruby::ast::GlobalVarNode === @node ||
31
+ org::jruby::ast::ClassVarNode === @node
32
+ end
33
+
34
+ def vcall?
35
+ org::jruby::ast::VCallNode === @node;
36
+ end
37
+
38
+ def call?
39
+ org::jruby::ast::CallNode === @node;
40
+ end
41
+
42
+ def variable_name
43
+ self[0][0]
44
+ end
45
+
46
+ def call_method_name
47
+ self.last.first
48
+ end
49
+
28
50
  def argument_nodes
29
- @node.args_node.to_a[0...@node.args_node.size].map do |arg|
51
+ @node.args_node.children.to_a[0...@node.args_node.size].map do |arg|
30
52
  self.class.wrap(arg)
31
53
  end
32
54
  end
33
55
 
34
56
  def to_hash
35
57
  @node.pairs.each_with_object({}) do |pair, object|
58
+ return nil if pair.key == nil
36
59
  object[self.class.wrap(pair.key)] = self.class.wrap(pair.value)
37
60
  end
38
61
  end
@@ -41,6 +64,14 @@ module ActionviewPrecompiler
41
64
  @node.value
42
65
  end
43
66
 
67
+ def variable_name
68
+ @node.name.to_s
69
+ end
70
+
71
+ def call_method_name
72
+ @node.name.to_s
73
+ end
74
+
44
75
  def to_symbol
45
76
  @node.name
46
77
  end
@@ -53,8 +84,17 @@ module ActionviewPrecompiler
53
84
  end
54
85
  end
55
86
 
56
- def parse(code)
57
- Node.wrap(JRuby.parse(code))
87
+ extend self
88
+
89
+ METHODS_TO_PARSE = %i(render render_to_string layout)
90
+
91
+ def parse_render_nodes(code)
92
+ node = Node.wrap(JRuby.parse(code))
93
+
94
+ renders = extract_render_nodes(node)
95
+ renders.group_by(&:first).collect do |method, nodes|
96
+ [ method, nodes.collect { |v| v[1] } ]
97
+ end.to_h
58
98
  end
59
99
 
60
100
  def node?(node)
@@ -64,5 +104,19 @@ module ActionviewPrecompiler
64
104
  def fcall?(node, name)
65
105
  node.fcall_named?(name)
66
106
  end
107
+
108
+ def extract_render_nodes(node)
109
+ return [] unless node?(node)
110
+ renders = node.children.flat_map { |c| extract_render_nodes(c) }
111
+
112
+ method_name = render_call?(node)
113
+ renders << [method_name, node] if method_name
114
+
115
+ renders
116
+ end
117
+
118
+ def render_call?(node)
119
+ METHODS_TO_PARSE.detect { |m| fcall?(node, m) }
120
+ end
67
121
  end
68
122
  end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module ActionviewPrecompiler
6
+ module PrismASTParser
7
+ # This error is raised whenever an assumption we made wasn't met by the AST.
8
+ class CompilationError < StandardError
9
+ end
10
+
11
+ # Each call object is responsible for holding a list of arguments and should
12
+ # respond to a single #arguments_node method that returns an array of
13
+ # arguments.
14
+ class RenderCall
15
+ attr_reader :argument_nodes
16
+
17
+ def initialize(argument_nodes)
18
+ @argument_nodes = argument_nodes
19
+ end
20
+ end
21
+
22
+ # This class represents a node in the tree that is returned by the parser
23
+ # that corresponds to an argument to a render call, or a child of one of
24
+ # those nodes.
25
+ class RenderNode
26
+ attr_reader :node
27
+
28
+ def initialize(node)
29
+ @node = node
30
+ end
31
+
32
+ def call?
33
+ node.is_a?(Prism::CallNode)
34
+ end
35
+
36
+ def hash?
37
+ node.is_a?(Prism::HashNode) || node.is_a?(Prism::KeywordHashNode)
38
+ end
39
+
40
+ def string?
41
+ node.is_a?(Prism::StringNode)
42
+ end
43
+
44
+ def symbol?
45
+ node.is_a?(Prism::SymbolNode)
46
+ end
47
+
48
+ def variable_reference?
49
+ case node.type
50
+ when :class_variable_read_node, :instance_variable_read_node,
51
+ :global_variable_read_node, :local_variable_read_node
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ def vcall?
59
+ node.is_a?(Prism::CallNode) && node.variable_call?
60
+ end
61
+
62
+ def call_method_name
63
+ node.message
64
+ end
65
+
66
+ def variable_name
67
+ case node.type
68
+ when :class_variable_read_node, :instance_variable_read_node,
69
+ :global_variable_read_node, :local_variable_read_node
70
+ node.name.name
71
+ when :call_node
72
+ node.message
73
+ end
74
+ end
75
+
76
+ # Converts the node into a hash where the keys and values are nodes. This
77
+ # will raise an error if the hash doesn't match the format we expect or
78
+ # if the hash contains any splats.
79
+ def to_hash
80
+ if hash?
81
+ if node.elements.all? { |assoc| assoc.is_a?(Prism::AssocNode) && assoc.key.is_a?(Prism::SymbolNode) }
82
+ node.elements.to_h { |assoc| [RenderNode.new(assoc.key), RenderNode.new(assoc.value)] }
83
+ end
84
+ else
85
+ raise CompilationError, "Unexpected node type: #{node.inspect}"
86
+ end
87
+ end
88
+
89
+ # Converts the node into a string value. Only handles plain string
90
+ # content, and will raise an error if the node contains interpolation.
91
+ def to_string
92
+ if string?
93
+ node.unescaped
94
+ else
95
+ raise CompilationError, "Unexpected node type: #{node.inspect}"
96
+ end
97
+ end
98
+
99
+ # Converts the node into a symbol value. Only handles labels and plain
100
+ # symbols, and will raise an error if the node contains interpolation.
101
+ def to_symbol
102
+ if symbol?
103
+ node.unescaped.to_sym
104
+ else
105
+ raise CompilationError, "Unexpected node type: #{node.inspect}"
106
+ end
107
+ end
108
+ end
109
+
110
+ # This visitor is responsible for visiting the parsed tree and extracting
111
+ # out the render calls. After visiting the tree, the #render_calls method
112
+ # will return the hash expected by the #parse_render_nodes method.
113
+ class RenderVisitor < Prism::Visitor
114
+ MESSAGE = /\A(render|render_to_string|layout)\z/
115
+
116
+ attr_reader :render_calls
117
+
118
+ def initialize
119
+ @render_calls = Hash.new { |hash, key| hash[key] = [] }
120
+ end
121
+
122
+ def visit_call_node(node)
123
+ if node.name.match?(MESSAGE) && !node.receiver && node.arguments
124
+ args =
125
+ node.arguments.arguments.map do |arg|
126
+ if arg.is_a?(Prism::ParenthesesNode) && arg.body && arg.body.body.length == 1
127
+ RenderNode.new(arg.body.body.first)
128
+ else
129
+ RenderNode.new(arg)
130
+ end
131
+ end
132
+
133
+ render_calls[node.name.to_sym] << RenderCall.new(args)
134
+ end
135
+
136
+ super
137
+ end
138
+ end
139
+
140
+ # Main entrypoint into this AST parser variant. It's responsible for
141
+ # returning a hash of render calls. The keys are the method names, and the
142
+ # values are arrays of call objects.
143
+ def self.parse_render_nodes(code)
144
+ visitor = RenderVisitor.new
145
+ result = Prism.parse(code)
146
+
147
+ if result.success?
148
+ result.value.accept(visitor)
149
+ visitor.render_calls
150
+ else
151
+ raise CompilationError, "Unable to parse the template"
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module ActionviewPrecompiler
6
+ module RipperASTParser
7
+ class Node < ::Array
8
+ attr_reader :type
9
+
10
+ def initialize(type, arr, opts = {})
11
+ @type = type
12
+ super(arr)
13
+ end
14
+
15
+ def children
16
+ to_a
17
+ end
18
+
19
+ def inspect
20
+ typeinfo = type && type != :list ? ':' + type.to_s + ', ' : ''
21
+ 's(' + typeinfo + map(&:inspect).join(", ") + ')'
22
+ end
23
+
24
+ def fcall?
25
+ type == :command || type == :fcall
26
+ end
27
+
28
+ def fcall_named?(name)
29
+ fcall? &&
30
+ self[0].type == :@ident &&
31
+ self[0][0] == name
32
+ end
33
+
34
+ def argument_nodes
35
+ raise unless fcall?
36
+ return [] if self[1].nil?
37
+ if self[1].last == false || self[1].last.type == :vcall
38
+ self[1][0...-1]
39
+ else
40
+ self[1][0..-1]
41
+ end
42
+ end
43
+
44
+ def string?
45
+ type == :string_literal
46
+ end
47
+
48
+ def variable_reference?
49
+ type == :var_ref
50
+ end
51
+
52
+ def vcall?
53
+ type == :vcall
54
+ end
55
+
56
+ def call?
57
+ type == :call
58
+ end
59
+
60
+ def variable_name
61
+ self[0][0]
62
+ end
63
+
64
+ def call_method_name
65
+ self.last.first
66
+ end
67
+
68
+ def to_string
69
+ raise unless string?
70
+ raise "fixme" unless self[0].type == :string_content
71
+ raise "fixme" unless self[0][0].type == :@tstring_content
72
+ self[0][0][0]
73
+ end
74
+
75
+ def hash?
76
+ type == :bare_assoc_hash || type == :hash
77
+ end
78
+
79
+ def to_hash
80
+ if type == :bare_assoc_hash
81
+ hash_from_body(self[0])
82
+ elsif type == :hash && self[0] == nil
83
+ {}
84
+ elsif type == :hash && self[0].type == :assoclist_from_args
85
+ hash_from_body(self[0][0])
86
+ else
87
+ raise "not a hash? #{inspect}"
88
+ end
89
+ end
90
+
91
+ def hash_from_body(body)
92
+ body.map do |hash_node|
93
+ return nil if hash_node.type != :assoc_new
94
+
95
+ [hash_node[0], hash_node[1]]
96
+ end.to_h
97
+ end
98
+
99
+ def symbol?
100
+ type == :@label || type == :symbol_literal
101
+ end
102
+
103
+ def to_symbol
104
+ if type == :@label && self[0] =~ /\A(.+):\z/
105
+ $1.to_sym
106
+ elsif type == :symbol_literal && self[0].type == :symbol && self[0][0].type == :@ident
107
+ self[0][0][0].to_sym
108
+ else
109
+ raise "not a symbol?: #{self.inspect}"
110
+ end
111
+ end
112
+ end
113
+
114
+ class NodeParser < ::Ripper
115
+ PARSER_EVENTS.each do |event|
116
+ arity = PARSER_EVENT_TABLE[event]
117
+
118
+ if /_new\z/ =~ event.to_s && arity == 0
119
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
120
+ def on_#{event}(*args)
121
+ Node.new(:list, args, lineno: lineno(), column: column())
122
+ end
123
+ eof
124
+ elsif /_add(_.+)?\z/ =~ event.to_s
125
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
126
+ begin; undef on_#{event}; rescue NameError; end
127
+ def on_#{event}(list, item)
128
+ list.push(item)
129
+ list
130
+ end
131
+ eof
132
+ else
133
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
134
+ begin; undef on_#{event}; rescue NameError; end
135
+ def on_#{event}(*args)
136
+ Node.new(:#{event}, args, lineno: lineno(), column: column())
137
+ end
138
+ eof
139
+ end
140
+ end
141
+
142
+ SCANNER_EVENTS.each do |event|
143
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
144
+ def on_#{event}(tok)
145
+ Node.new(:@#{event}, [tok], lineno: lineno(), column: column())
146
+ end
147
+ End
148
+ end
149
+ end
150
+
151
+ class RenderCallParser < NodeParser
152
+ attr_reader :render_calls
153
+
154
+ METHODS_TO_PARSE = %w(render render_to_string layout)
155
+
156
+ def initialize(*args)
157
+ super
158
+
159
+ @render_calls = []
160
+ end
161
+
162
+ private
163
+
164
+ def on_fcall(name, *args)
165
+ on_render_call(super)
166
+ end
167
+
168
+ def on_command(name, *args)
169
+ on_render_call(super)
170
+ end
171
+
172
+ def on_render_call(node)
173
+ METHODS_TO_PARSE.each do |method|
174
+ if node.fcall_named?(method)
175
+ @render_calls << [method, node]
176
+ return node
177
+ end
178
+ end
179
+ node
180
+ end
181
+
182
+ def on_arg_paren(content)
183
+ content
184
+ end
185
+
186
+ def on_paren(content)
187
+ if (content.size == 1) && (content.is_a?(Array))
188
+ content.first
189
+ else
190
+ content
191
+ end
192
+ end
193
+ end
194
+
195
+ extend self
196
+
197
+ def parse_render_nodes(code)
198
+ parser = RenderCallParser.new(code)
199
+ parser.parse
200
+
201
+ parser.render_calls.group_by(&:first).collect do |method, nodes|
202
+ [ method.to_sym, nodes.collect { |v| v[1] } ]
203
+ end.to_h
204
+ end
205
+ end
206
+ end
@@ -1,5 +1,5 @@
1
1
  module ActionviewPrecompiler
2
- module ASTParser
2
+ module Ruby26ASTParser
3
3
  class Node
4
4
  def self.wrap(node)
5
5
  if RubyVM::AbstractSyntaxTree::Node === node
@@ -24,7 +24,13 @@ module ActionviewPrecompiler
24
24
  end
25
25
 
26
26
  def argument_nodes
27
- children[1].children[0...-1]
27
+ if children[1].array?
28
+ children[1].children[0...-1]
29
+ elsif children[1].block_pass?
30
+ children[1].children[0].children[0...-1]
31
+ else
32
+ raise "can't call argument_nodes on #{inspect}"
33
+ end
28
34
  end
29
35
 
30
36
  def array?
@@ -39,12 +45,36 @@ module ActionviewPrecompiler
39
45
  type == :HASH
40
46
  end
41
47
 
48
+ def block_pass?
49
+ type == :BLOCK_PASS
50
+ end
51
+
42
52
  def string?
43
53
  type == :STR && String === children[0]
44
54
  end
45
55
 
56
+ def variable_reference?
57
+ type == :IVAR || type == :GVAR || type == :CVAR
58
+ end
59
+
60
+ def variable_name
61
+ children[0].to_s
62
+ end
63
+
64
+ def vcall?
65
+ type == :VCALL
66
+ end
67
+
68
+ def call?
69
+ type == :CALL
70
+ end
71
+
72
+ def call_method_name
73
+ children[1].to_s
74
+ end
75
+
46
76
  def symbol?
47
- type == :LIT && Symbol === children[0]
77
+ (type == :SYM) || (type == :LIT && Symbol === children[0])
48
78
  end
49
79
 
50
80
  def to_hash
@@ -52,7 +82,9 @@ module ActionviewPrecompiler
52
82
  if list.nil?
53
83
  {}
54
84
  else
55
- list.children[0..-2].each_slice(2).to_h
85
+ hash = list.children[0..-2].each_slice(2).to_h
86
+ return nil if hash.key?(nil)
87
+ hash
56
88
  end
57
89
  end
58
90
 
@@ -68,7 +100,7 @@ module ActionviewPrecompiler
68
100
  fcall? &&
69
101
  children[0] == name &&
70
102
  children[1] &&
71
- children[1].array?
103
+ (children[1].array? || (children[1].block_pass? && children[1].children[0].array?))
72
104
  end
73
105
 
74
106
  private
@@ -78,6 +110,18 @@ module ActionviewPrecompiler
78
110
  end
79
111
  end
80
112
 
113
+ extend self
114
+
115
+ METHODS_TO_PARSE = %i(render render_to_string layout)
116
+
117
+ def parse_render_nodes(code)
118
+ renders = extract_render_nodes(parse(code))
119
+
120
+ renders.group_by(&:first).collect do |method, nodes|
121
+ [ method, nodes.collect { |v| v[1] } ]
122
+ end.to_h
123
+ end
124
+
81
125
  def parse(code)
82
126
  Node.wrap(RubyVM::AbstractSyntaxTree.parse(code))
83
127
  end
@@ -89,5 +133,25 @@ module ActionviewPrecompiler
89
133
  def fcall?(node, name)
90
134
  node.fcall_named?(name)
91
135
  end
136
+
137
+ def extract_render_nodes(root)
138
+ renders = []
139
+ queue = [root]
140
+
141
+ while node = queue.shift
142
+ node.children.each do |child|
143
+ queue << child if node?(child)
144
+ end
145
+
146
+ method_name = render_call?(node)
147
+ renders << [method_name, node] if method_name
148
+ end
149
+
150
+ renders
151
+ end
152
+
153
+ def render_call?(node)
154
+ METHODS_TO_PARSE.detect { |m| fcall?(node, m) }
155
+ end
92
156
  end
93
157
  end
@@ -1,5 +1,26 @@
1
- if RUBY_ENGINE == 'jruby'
2
- require "actionview_precompiler/ast_parser/jruby"
3
- else
4
- require "actionview_precompiler/ast_parser/ruby26"
1
+ module ActionviewPrecompiler
2
+ parser = ENV["PRECOMPILER_PARSER"]
3
+
4
+ begin
5
+ require "prism"
6
+ parser ||= "prism"
7
+ rescue LoadError
8
+ parser ||= "jruby" if RUBY_ENGINE == 'jruby'
9
+ parser ||= "rubyvm_ast" if RUBY_ENGINE == 'ruby'
10
+ end
11
+
12
+ case parser
13
+ when "rubyvm_ast"
14
+ require "actionview_precompiler/ast_parser/ruby26"
15
+ ASTParser = Ruby26ASTParser
16
+ when "jruby"
17
+ require "actionview_precompiler/ast_parser/jruby"
18
+ ASTParser = JRubyASTParser
19
+ when "prism"
20
+ require "actionview_precompiler/ast_parser/prism"
21
+ ASTParser = PrismASTParser
22
+ else
23
+ require "actionview_precompiler/ast_parser/ripper"
24
+ ASTParser = RipperASTParser
25
+ end
5
26
  end
@@ -0,0 +1,13 @@
1
+ module ActionviewPrecompiler
2
+ class ControllerParser
3
+ def initialize(filename)
4
+ @filename = filename
5
+ end
6
+
7
+ def render_calls
8
+ src = File.read(@filename)
9
+ return [] unless src.include?("render")
10
+ RenderParser.new(src, from_controller: true).render_calls
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ module ActionviewPrecompiler
2
+ class ControllerScanner
3
+ attr_reader :controller_dir
4
+
5
+ def initialize(controller_dir)
6
+ @controller_dir = controller_dir
7
+ end
8
+
9
+ def template_renders
10
+ template_renders = []
11
+
12
+ each_controller do |path, fullpath|
13
+ parser = ControllerParser.new(fullpath)
14
+
15
+ if path =~ /\A(.*)_controller\.rb\z/
16
+ controller_prefix = $1
17
+ end
18
+
19
+ parser.render_calls.each do |render_call|
20
+ virtual_path = render_call.virtual_path
21
+
22
+ unless virtual_path.include?("/")
23
+ next unless controller_prefix
24
+
25
+ virtual_path = "#{controller_prefix}/#{virtual_path}"
26
+ end
27
+
28
+ locals = render_call.locals_keys.map(&:to_s).sort
29
+
30
+ template_renders << [virtual_path, locals]
31
+ end
32
+ end
33
+
34
+ template_renders.uniq
35
+ end
36
+
37
+ private
38
+
39
+ def each_controller
40
+ Dir["**/*.rb", base: controller_dir].sort.map do |file|
41
+ fullpath = File.expand_path(file, controller_dir)
42
+ next if File.directory?(fullpath)
43
+
44
+ yield file, fullpath
45
+ end
46
+ end
47
+ end
48
+ end