actionview_precompiler 0.2.3 → 0.3.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: 48e7feef8ebfac2d8a2d18188dcfe98ce8e156e84567a8c632ebd713facb9a6e
4
+ data.tar.gz: 4b5efa2f1ce1a221bbf1f1729ec4548a0dc6250a4086a5e5dc1e1ef485cd59aa
5
5
  SHA512:
6
- metadata.gz: ca66b59f8768cdfe813b17dd9277211e269da5d978013ae9dda7b3e07d588a2b0a8c412849f8f5c866d44b23b3eadf6327ce92366f7b29888c131c48f6585c3b
7
- data.tar.gz: dfb74620b2c5aaa56e008ce39172654cec82c98d951e3af57f63cea0d55f027d06489b48f412dfcee7efd46ba75ce6280d2456c82493dd9ad7094476c89ccc58
6
+ metadata.gz: 84eccaa58fb87a5a9dd06183e9f2fda66e258eaeaf57e0abbe27a4e8eed691e7cf1981a3d0ab4e55dbe6e9df841ea7f75ab924938ab8458ac8c7a2068ffa8c8f
7
+ data.tar.gz: bc95591c0489a70a3c1de6975c3ab0deb7890ee7d0ce4be018b4283098d3dabcb65eddaca299e50220673dbdb425d795ce20fabec178a5f20a6a6021038766fe
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,202 @@
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
+ content
188
+ end
189
+ end
190
+
191
+ extend self
192
+
193
+ def parse_render_nodes(code)
194
+ parser = RenderCallParser.new(code)
195
+ parser.parse
196
+
197
+ parser.render_calls.group_by(&:first).collect do |method, nodes|
198
+ [ method.to_sym, nodes.collect { |v| v[1] } ]
199
+ end.to_h
200
+ end
201
+ end
202
+ 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,10 +45,34 @@ 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
77
  type == :LIT && Symbol === children[0]
48
78
  end
@@ -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
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ module ActionviewPrecompiler
6
+ module SyntaxTreeASTParser
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?(SyntaxTree::Call)
34
+ end
35
+
36
+ def hash?
37
+ node.is_a?(SyntaxTree::HashLiteral) || node.is_a?(SyntaxTree::BareAssocHash)
38
+ end
39
+
40
+ def string?
41
+ node.is_a?(SyntaxTree::StringLiteral)
42
+ end
43
+
44
+ def symbol?
45
+ node.is_a?(SyntaxTree::Label) || node.is_a?(SyntaxTree::SymbolLiteral)
46
+ end
47
+
48
+ def variable_reference?
49
+ node.is_a?(SyntaxTree::VarRef)
50
+ end
51
+
52
+ def vcall?
53
+ node.is_a?(SyntaxTree::VCall)
54
+ end
55
+
56
+ # Converts the node into a hash where the keys and values are nodes. This
57
+ # will raise an error if the hash doesn't match the format we expect or
58
+ # if the hash contains any splats.
59
+ def to_hash
60
+ case node
61
+ in SyntaxTree::HashLiteral[assocs:]
62
+ in SyntaxTree::BareAssocHash[assocs:]
63
+ else
64
+ raise CompilationError, "Unexpected node type: #{node.class.name}"
65
+ end
66
+
67
+ assocs.to_h do |assoc|
68
+ case assoc
69
+ in SyntaxTree::Assoc[key:, value:]
70
+ [RenderNode.new(key), RenderNode.new(value)]
71
+ else
72
+ raise CompilationError, "Unexpected node type: #{node.class.name}"
73
+ end
74
+ end
75
+ end
76
+
77
+ # Converts the node into a string value. Only handles plain string
78
+ # content, and will raise an error if the node contains interpolation.
79
+ def to_string
80
+ case node
81
+ in SyntaxTree::StringLiteral[parts: [SyntaxTree::TStringContent[value:]]]
82
+ value
83
+ in SyntaxTree::StringLiteral[parts:]
84
+ raise CompilationError, "Unexpected string parts type: #{parts.inspect}"
85
+ else
86
+ raise CompilationError, "Unexpected node type: #{node.class.name}"
87
+ end
88
+ end
89
+
90
+ # Converts the node into a symbol value. Only handles labels and plain
91
+ # symbols, and will raise an error if the node contains interpolation.
92
+ def to_symbol
93
+ case node
94
+ in SyntaxTree::Label[value:]
95
+ value.chomp(":").to_sym
96
+ in SyntaxTree::SymbolLiteral[value: SyntaxTree::Ident[value:]]
97
+ value.to_sym
98
+ in SyntaxTree::SymbolLiteral[value:]
99
+ raise CompilationError, "Unexpected symbol value type: #{value.inspect}"
100
+ else
101
+ raise CompilationError, "Unexpected node type: #{node.class.name}"
102
+ end
103
+ end
104
+ end
105
+
106
+ # This visitor is responsible for visiting the parsed tree and extracting
107
+ # out the render calls. After visiting the tree, the #render_calls method
108
+ # will return the hash expected by the #parse_render_nodes method.
109
+ class RenderVisitor < SyntaxTree::Visitor
110
+ MESSAGE = /\A(render|render_to_string|layout)\z/
111
+
112
+ attr_reader :render_calls
113
+
114
+ def initialize
115
+ @render_calls = Hash.new { |hash, key| hash[key] = [] }
116
+ end
117
+
118
+ visit_method def visit_command(node)
119
+ case node
120
+ in SyntaxTree::Command[
121
+ message: SyntaxTree::Ident[value: MESSAGE],
122
+ arguments: SyntaxTree::Args[parts:]
123
+ ]
124
+ argument_nodes = parts.map { |part| RenderNode.new(part) }
125
+ render_calls[$1.to_sym] << RenderCall.new(argument_nodes)
126
+ else
127
+ end
128
+
129
+ super
130
+ end
131
+
132
+ visit_method def visit_fcall(node)
133
+ case node
134
+ in SyntaxTree::FCall[
135
+ value: SyntaxTree::Ident[value: MESSAGE],
136
+ arguments: SyntaxTree::ArgParen[arguments: SyntaxTree::Args[parts:]]
137
+ ]
138
+ argument_nodes = parts.map { |part| RenderNode.new(part) }
139
+ render_calls[$1.to_sym] << RenderCall.new(argument_nodes)
140
+ else
141
+ end
142
+
143
+ super
144
+ end
145
+ end
146
+
147
+ # Main entrypoint into this AST parser variant. It's responsible for
148
+ # returning a hash of render calls. The keys are the method names, and the
149
+ # values are arrays of call objects.
150
+ def self.parse_render_nodes(code)
151
+ visitor = RenderVisitor.new
152
+ SyntaxTree.parse(code).accept(visitor)
153
+ visitor.render_calls
154
+ rescue SyntaxTree::Parser::ParseError
155
+ raise CompilationError, "Unable to parse the template"
156
+ end
157
+ end
158
+ end
@@ -1,5 +1,20 @@
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
+ parser ||= "jruby" if RUBY_ENGINE == 'jruby'
4
+ parser ||= "rubyvm_ast" if RUBY_ENGINE == 'ruby'
5
+
6
+ case parser
7
+ when "rubyvm_ast"
8
+ require "actionview_precompiler/ast_parser/ruby26"
9
+ ASTParser = Ruby26ASTParser
10
+ when "jruby"
11
+ require "actionview_precompiler/ast_parser/jruby"
12
+ ASTParser = JRubyASTParser
13
+ when "syntax_tree"
14
+ require "actionview_precompiler/ast_parser/syntax_tree"
15
+ ASTParser = SyntaxTreeASTParser
16
+ else
17
+ require "actionview_precompiler/ast_parser/ripper"
18
+ ASTParser = RipperASTParser
19
+ end
5
20
  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