Skip to content

Commit

Permalink
Support safe call (#46)
Browse files Browse the repository at this point in the history
* Support safe call

This copies the process_call_code method from ruby2ruby. That also
implements process_safe_call which calls process_call with a parameter.

* Remove extra &

---------

Co-authored-by: Oleh Fedorenko <[email protected]>
  • Loading branch information
ekohl and ofedoren authored Dec 11, 2023
1 parent 1ff4a45 commit 7897457
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 15 deletions.
47 changes: 32 additions & 15 deletions lib/safemode/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,22 @@ def parser=(parser)
end
end

def jail(str, parentheses = false)
str = parentheses ? "(#{str})." : "#{str}." if str
def jail(str, parentheses = false, safe_call: false)
str = if str
dot = safe_call ? "&." : "."
parentheses ? "(#{str})#{dot}" : "#{str}#{dot}"
end
"#{str}to_jail"
end

# split up #process_call. see below ...
def process_call(exp)
def process_call(exp, safe_call = false)
exp.shift # remove ":call" symbol
receiver = jail process_call_receiver(exp)
receiver = jail(process_call_receiver(exp), safe_call: safe_call)
name = exp.shift
args = process_call_args(exp)

process_call_code(receiver, name, args)
process_call_code(receiver, name, args, safe_call)
end

def process_fcall(exp)
Expand Down Expand Up @@ -159,25 +162,39 @@ def process_call_args(exp)
end
args << processed unless (processed.nil? or processed.empty?)
end
args.empty? ? nil : args.join(", ")
args
end

def process_call_code(receiver, name, args)
def process_call_code(receiver, name, args, safe_call)
case name
when :<=>, :==, "!=".to_sym, :<, :>, :<=, :>=, :-, :+, :*, :/, :%, :<<, :>>, :** then
"(#{receiver} #{name} #{args})"
when *BINARY then
if safe_call
"#{receiver}&.#{name}(#{args.join(", ")})"
elsif args.length > 1
"#{receiver}.#{name}(#{args.join(", ")})"
else
"(#{receiver} #{name} #{args.join(", ")})"
end
when :[] then
"#{receiver}[#{args}]"
receiver ||= "self"
"#{receiver}[#{args.join(", ")}]"
when :[]= then
receiver ||= "self"
rhs = args.pop
"#{receiver}[#{args.join(", ")}] = #{rhs}"
when :"!" then
"(not #{receiver})"
when :"-@" then
"-#{receiver}"
when :"+@" then
"+#{receiver}"
else
unless receiver.nil? then
"#{receiver}.#{name}#{args ? "(#{args})" : args}"
else
"#{name}#{args ? "(#{args})" : args}"
end
args = nil if args.empty?
args = "(#{args.join(", ")})" if args
receiver = "#{receiver}." if receiver and not safe_call
receiver = "#{receiver}&." if receiver and safe_call

"#{receiver}#{name}#{args}"
end
end

Expand Down
4 changes: 4 additions & 0 deletions test/test_safemode_eval.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def test_some_stuff_that_should_work
end
end

def test_safe_navigation_operator
assert_equal "1", @box.eval('x = 1; x&.to_s')
end

def test_unary_operators_on_instances_of_boolean_vars
assert @box.eval('not false')
assert @box.eval('!false')
Expand Down
10 changes: 10 additions & 0 deletions test/test_safemode_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ def test_ternary_should_be_usable_for_erb
assert_jailed "if true then\n 1\n else\n2\nend", "true ? 1 : 2"
end

def test_safe_call_simple
assert_jailed '@article&.to_jail&.method', '@article&.method'
end

def test_safe_call_with_complex_args
unsafe = "@article&.method_with_kwargs('positional')"
jailed = "@article&.to_jail&.method_with_kwargs(\"positional\")"
assert_jailed jailed, unsafe
end

def test_output_buffer_should_be_assignable
assert_nothing_raised do
jail('@output_buffer = ""')
Expand Down

0 comments on commit 7897457

Please sign in to comment.