This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# PoC user code | |
require_relative 'call_check' | |
class CacheLike | |
include CallCheck | |
def initialize(store, log) | |
@store = store | |
@log = log | |
check_instance_calls! | |
end | |
def fetch(key, &block) | |
if @store.has_key?(key.strip) | |
@store[key.strip] | |
@log.puts "read" | |
else | |
@store[key.strip] = block.call | |
@log.puts "write" | |
end | |
end | |
end | |
CacheLike.new({}, STDOUT) | |
CacheLike.new(1, STDOUT) | |
#=> Instance @store (Integer) is expected to implement [:[]=, :has_key?] methods but doesn't implement (ArgumentError) | |
CacheLike.new({}, 2) | |
#=> Instance @log (Integer) is expected to implement [:puts] methods but doesn't implement (ArgumentError) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# PoC library code | |
class InstanceVariableTracer | |
def initialize(target_name) | |
@target_name = target_name | |
@stack = [] # true if this is the tracing target object | |
@calls = [] | |
end | |
def collect(iseq) | |
array = iseq.to_a | |
if array[0] == "YARVInstructionSequence/SimpleDataFormat" | |
array[13].each do |instruction| | |
add(instruction) | |
end | |
end | |
@calls | |
end | |
private | |
def add(instruction) | |
return unless instruction.is_a?(Array) | |
case instruction[0] | |
when :getinstancevariable | |
@stack << (instruction[1] == @target_name) | |
when :getlocal_OP__WC__0, :putstring | |
@stack << false | |
when :opt_send_without_block, :opt_aref, :opt_aset, :opt_aref_with | |
instruction[1][:orig_argc].times { @stack.pop } | |
if @stack.last | |
@calls << [instruction[1][:mid], instruction[1][:orig_argc]] | |
end | |
@stack.pop | |
@stack << false | |
end | |
end | |
end | |
module CallCheck | |
def check_instance_calls(name, user) | |
variable = user.instance_variable_get(name) | |
calls = [] | |
user.methods.each do |m| | |
iseq = RubyVM::InstructionSequence.of(user.method(m)) | |
calls.concat InstanceVariableTracer.new(name).collect(iseq) | |
end | |
not_implemented = calls.select do |(name,argc)| | |
p [variable, name] | |
!variable.respond_to?(name) | |
end | |
unless not_implemented.empty? | |
human_names = not_implemented.map {|(name,argc)| name }.uniq.sort | |
raise ArgumentError, "Instance #{name} (#{variable.class}) is expected to implement #{human_names.inspect} methods but doesn't implement" | |
end | |
end | |
def check_instance_calls!(*names) | |
if names.empty? | |
names = self.instance_variables | |
end | |
names.each do |name| | |
check_instance_calls(name, self) | |
end | |
nil | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment