Skip to content

Instantly share code, notes, and snippets.

@frsyuki
Created May 16, 2017 07:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frsyuki/c49fe20e7f2732736928be5e3d94b4b9 to your computer and use it in GitHub Desktop.
Save frsyuki/c49fe20e7f2732736928be5e3d94b4b9 to your computer and use it in GitHub Desktop.
# 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)
# 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