actionview_precompiler 0.2.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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