Skip to content

Commit 68297f7

Browse files
authored
ERB transformation rework (#20)
* bug fixes * Reworked ERB transformation to perform a series of appending calls to a buffer' * rename buffers to prefix with joern__ to distinguish variables * Remove prefix and postfix tag calls. Add call to original function before lambda lowering * code cleanup * add unit test showing lowered ERB code
1 parent 465a3b0 commit 68297f7

File tree

3 files changed

+110
-59
lines changed

3 files changed

+110
-59
lines changed

lib/ruby_ast_gen.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ def self.parse_file(file_path, relative_input_path)
124124
file_content = File.read(file_path)
125125
get_erb_content(file_content)
126126
end
127-
puts code
128127
buffer = Parser::Source::Buffer.new(file_path)
129128
buffer.source = code
130129
parser = Parser::CurrentRuby.new

lib/ruby_ast_gen/erb_to_ruby_transformer.rb

Lines changed: 84 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,27 @@
44
class ErbToRubyTransformer
55
def initialize
66
@parser = Temple::ERB::Parser.new
7-
@indent_level = 0
8-
@current_line = []
97
@in_control_block = false
10-
@output_tmp_var = "tmp0"
11-
@is_first_output = true
8+
@output_tmp_var = "joern__buffer"
9+
@in_do_block = false
10+
@inner_buffer = "joern__inner_buffer"
11+
@current_counter = 0
12+
@current_lambda_vars = ""
1213
@output = []
13-
@no_control_struct = true
14-
@open_heredoc = false
14+
@static_buff = []
1515
end
1616

1717
def transform(input)
1818
ast = @parser.call(input)
19-
content = "#{@output_tmp_var} = \"\" \n#{visit(ast)}"
20-
if @in_control_block
19+
@output << "#{@output_tmp_var} = \"\""
20+
visit(ast)
21+
@output << "return #{@output_tmp_var}"
22+
23+
if @in_control_block || @in_do_block
2124
raise ::StandardError, "Invalid ERB Syntax"
2225
end
2326
<<~RUBY
24-
#{content}
25-
return #{@output_tmp_var}
27+
#{@output.join("\n")}
2628
RUBY
2729
end
2830

@@ -32,77 +34,104 @@ def visit(node)
3234
case node.first
3335
when :multi
3436
node[1..-1].each do |child|
35-
transformed = visit(child)
36-
unless transformed.strip.empty?
37-
if @is_first_output
38-
@open_heredoc = true
39-
@current_line << "#{@output_tmp_var} += <<-HEREDOC\n"
40-
@is_first_output = false
41-
end
42-
@current_line << transformed
43-
end
44-
end
45-
46-
if @open_heredoc
47-
@current_line << "\nHEREDOC\n"
48-
@open_heredoc = false
37+
visit(child)
4938
end
50-
51-
flush_current_line(@output) unless @current_line.empty?
52-
@output.join("\n")
5339
when :static
54-
"#{node[1].to_s}"
40+
unless node[1].to_s != nil && node[1].to_s.strip.empty?
41+
@static_buff << "\"#{node[1].to_s.gsub('"', '\"').strip}\""
42+
end
5543
when :dynamic
56-
"#{node[1].to_s}"
44+
unless node[1].to_s != nil && node[1].to_s.strip.empty?
45+
@output << "\"#{node[1].to_s.gsub('"', '\"')}\""
46+
end
5747
when :escape
48+
unless @static_buff.empty?
49+
buffer_to_use = if @in_do_block then "#{@inner_buffer}" else "#{@output_tmp_var}" end
50+
@output << "#{buffer_to_use} << \"#{@static_buff.join('\n').gsub(/(?<!\\)"/, '')}\""
51+
@static_buff = [] # clear static buffer
52+
end
53+
5854
escape_enabled = node[1]
5955
inner_node = node[2]
6056
code = inner_node[1].to_s.strip
61-
template_call = if escape_enabled then "joern__template_out_raw" else "joern__template_out_escape" end
62-
"\#{#{template_call}(#{code})}"
57+
58+
# Do block with variable found, lower
59+
if is_do_block(code)
60+
lower_do_block(code)
61+
elsif @in_do_block
62+
template_call = if escape_enabled then "joern__template_out_raw" else "joern__template_out_escape" end
63+
@output << "#{@inner_buffer} << #{template_call}(#{code})"
64+
else
65+
template_call = if escape_enabled then "joern__template_out_raw" else "joern__template_out_escape" end
66+
@output << "#{@output_tmp_var} << #{template_call}(#{code})"
67+
end
6368
when :code
69+
unless @static_buff.empty?
70+
buffer_to_use = if @in_do_block then "#{@inner_buffer}" else "buffer" end
71+
@output << "#{buffer_to_use} << \"#{@static_buff.join('\n').gsub(/(?<!\\)"/, '')}\""
72+
@static_buff = [] # clear static buffer
73+
end
6474
code = node[1].to_s.strip
6575
# Using this to determine if we should throw a StandardError for "invalid" ERB
6676
if is_control_struct_start(code)
6777
@in_control_block = true
78+
@output << code
6879
elsif code.start_with?("end")
69-
@in_control_block = false
70-
end
71-
72-
if @open_heredoc
73-
@open_heredoc = false
74-
@current_line << "\nHEREDOC"
80+
if @in_do_block
81+
@in_do_block = false
82+
@output << "#{@inner_buffer}"
83+
@output << "end"
84+
@output << "#{@output_tmp_var} << #{current_lambda}.call(#{@current_lambda_vars})"
85+
else
86+
@in_control_block = false
87+
@output << "end"
88+
end
89+
else
90+
if is_do_block(code)
91+
lower_do_block(code)
92+
end
7593
end
76-
77-
@current_line << "\n#{node[1].to_s.strip}\n"
78-
@is_first_output = true
79-
""
8094
when :newline
81-
""
8295
else
8396
RubyAstGen::Logger::debug("Invalid node type: #{node}")
84-
""
8597
end
8698
end
8799

88-
def indent
89-
" " * @indent_level
100+
101+
def is_control_struct_start(line)
102+
line.start_with?('if', 'unless', 'elsif', 'else', /@?\w+\.each\sdo/)
90103
end
91104

92-
def flush_current_line(output)
93-
unless @current_line.empty?
94-
line = @current_line.join.rstrip
95-
output << line unless line.empty?
96-
@current_line.clear
97-
end
105+
def lambda_incrementor()
106+
new_lambda = "rails_lambda_#{@current_counter}"
107+
@current_counter += 1
108+
new_lambda
98109
end
99110

100-
def is_control_struct_start(line)
101-
line.start_with?('if', 'unless', 'elsif', 'else', /@?\w+\.each\sdo/)
111+
def current_lambda()
112+
"rails_lambda_#{@current_counter-1}"
113+
end
114+
115+
def lower_do_block(code)
116+
if (code_match = code.match(/do\s*(?:\|([^|]*)\|)?/))
117+
@current_lambda_vars = code_match[1]
118+
before_do, _ = code.split(/\bdo\b/)
119+
unless before_do.nil?
120+
method_call = before_do.strip
121+
call_name, rest = method_call.split(' ', 2)
122+
if rest != nil && !rest.start_with?('(') && !rest.end_with?(')')
123+
method_call = "#{call_name}(#{rest})"
124+
end
125+
@output << "#{@output_tmp_var} << #{method_call}"
126+
end
127+
@in_do_block = true
128+
@output << "#{lambda_incrementor} = lambda do |#{@current_lambda_vars}|"
129+
@output << "#{@inner_buffer} = \"\""
130+
end
102131
end
103132

104-
def is_control_struct(line)
105-
is_control_struct_start(line) || line.start_with?('end')
133+
def is_do_block(code)
134+
code.match(/do\s*(?:\|([^|]*)\|)?/)
106135
end
107136
end
108137

spec/ruby_ast_gen_spec.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def foo(a, bar: "default")
103103
end
104104

105105
it "should parse ERB structure with no ruby expressions" do
106-
code(<<-CODE)
106+
erb_code(<<-CODE)
107107
app_name: <%= ENV['APP_NAME'] %>
108108
version: <%= ENV['APP_VERSION'] %>
109109
@@ -116,7 +116,7 @@ def foo(a, bar: "default")
116116
end
117117

118118
it "should parse ERB structure with expressions" do
119-
code(<<-CODE)
119+
erb_code(<<-CODE)
120120
app_name: <%= ENV['APP_NAME'] %>
121121
version: <%= ENV['APP_VERSION'] %>
122122
@@ -135,7 +135,7 @@ def foo(a, bar: "default")
135135
end
136136

137137
it "should still return some AST even if the ERB is invalid" do
138-
code(<<-CODE)
138+
erb_code(<<-CODE)
139139
app_name: <%= ENV['APP_NAME'] %>
140140
version: <%= ENV['APP_VERSION'] %>
141141
@@ -151,4 +151,27 @@ def foo(a, bar: "default")
151151
ast = RubyAstGen::parse_file(temp_erb_file.path, temp_name)
152152
expect(ast).not_to be_nil
153153
end
154+
155+
it "should lower ERB code" do
156+
erb_code(<<-CODE)
157+
<%= form_with url: some_url do |form| %>
158+
<%= form.text_field :name %>
159+
<% end %>
160+
CODE
161+
162+
file_content = File.read(temp_erb_file.path)
163+
code = RubyAstGen::get_erb_content(file_content)
164+
expected = <<-HEREDOC
165+
joern__buffer = ""
166+
joern__buffer << form_with(url: some_url)
167+
rails_lambda_0 = lambda do |form|
168+
joern__inner_buffer = ""
169+
joern__inner_buffer << joern__template_out_escape(form.text_field :name)
170+
joern_inner_buffer
171+
end
172+
joern__buffer << rails_lambda_0.call(form)
173+
return joern__buffer
174+
HEREDOC
175+
expect(code).equal?(expected)
176+
end
154177
end

0 commit comments

Comments
 (0)