mirrors 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +2 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.txt +59 -0
- data/README.md +1 -0
- data/asdfasdf.rb +53 -0
- data/bin/bundler +17 -0
- data/bin/byebug +17 -0
- data/bin/testunit +8 -0
- data/circle.yml +6 -0
- data/dev.yml +6 -0
- data/lib/mirrors.rb +150 -0
- data/lib/mirrors/class_mirror.rb +197 -0
- data/lib/mirrors/class_mixin.rb +11 -0
- data/lib/mirrors/field_mirror.rb +24 -0
- data/lib/mirrors/field_mirror/class_variable_mirror.rb +23 -0
- data/lib/mirrors/field_mirror/constant_mirror.rb +34 -0
- data/lib/mirrors/field_mirror/instance_variable_mirror.rb +23 -0
- data/lib/mirrors/hook.rb +33 -0
- data/lib/mirrors/index/indexer.rb +6 -0
- data/lib/mirrors/index/marker.rb +40 -0
- data/lib/mirrors/invoke.rb +29 -0
- data/lib/mirrors/method_mirror.rb +206 -0
- data/lib/mirrors/mirror.rb +37 -0
- data/lib/mirrors/object_mirror.rb +25 -0
- data/lib/mirrors/package_inference.rb +164 -0
- data/lib/mirrors/package_inference/class_to_file_resolver.rb +66 -0
- data/lib/mirrors/package_mirror.rb +33 -0
- data/lib/mirrors/visitors/disasm_visitor.rb +11 -0
- data/lib/mirrors/visitors/iseq_visitor.rb +84 -0
- data/lib/mirrors/visitors/references_visitor.rb +58 -0
- data/lib/mirrors/visitors/yasmdata.rb +212 -0
- data/lol.rb +35 -0
- data/mirrors.gemspec +19 -0
- data/test/fixtures/class.rb +29 -0
- data/test/fixtures/field.rb +9 -0
- data/test/fixtures/method.rb +15 -0
- data/test/fixtures/object.rb +5 -0
- data/test/fixtures/reflect.rb +14 -0
- data/test/mirrors/class_mirror_test.rb +87 -0
- data/test/mirrors/field_mirror_test.rb +125 -0
- data/test/mirrors/iseq_visitor_test.rb +56 -0
- data/test/mirrors/marker_test.rb +48 -0
- data/test/mirrors/method_mirror_test.rb +62 -0
- data/test/mirrors/object_mirror_test.rb +16 -0
- data/test/mirrors/package_inference_test.rb +31 -0
- data/test/mirrors/references_visitor_test.rb +30 -0
- data/test/mirrors_test.rb +38 -0
- data/test/test_helper.rb +12 -0
- metadata +137 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Mirrors
|
2
|
+
# A mirror class. It is the most generic mirror and should be able
|
3
|
+
# to reflect on any object you can get at in a given system.
|
4
|
+
class ObjectMirror < Mirror
|
5
|
+
# @return [FieldMirror] the instance variables of the object
|
6
|
+
def variables
|
7
|
+
field_mirrors(@subject.instance_variables)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [ClassMirror] the a class mirror on the runtime class object
|
11
|
+
def target_class
|
12
|
+
Mirrors.reflect(@subject.class)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def field_mirrors(list, subject = @subject)
|
18
|
+
list.map { |name| field_mirror(subject, name) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def field_mirror(subject, name)
|
22
|
+
Mirrors.reflect(FieldMirror::Field.new(subject, name))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
require 'mirrors/hook'
|
5
|
+
require 'mirrors/package_inference/class_to_file_resolver'
|
6
|
+
|
7
|
+
module Mirrors
|
8
|
+
module PackageInference
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def infer_from(mod, resolver = ClassToFileResolver.new)
|
12
|
+
infer_from_key(Mirrors.module_instance_invoke(mod, :inspect), resolver)
|
13
|
+
end
|
14
|
+
|
15
|
+
def infer_from_toplevel(sym, resolver = ClassToFileResolver.new)
|
16
|
+
infer_from_key(sym.to_s, resolver)
|
17
|
+
end
|
18
|
+
|
19
|
+
def contents_of_package(pkg)
|
20
|
+
@inverse_cache[pkg]
|
21
|
+
end
|
22
|
+
|
23
|
+
def qualified_packages
|
24
|
+
@inverse_cache.keys
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def infer_from_key(key, resolver)
|
30
|
+
@inference_cache ||= {}
|
31
|
+
@inverse_cache ||= {}
|
32
|
+
|
33
|
+
cached = @inference_cache[key]
|
34
|
+
return cached if cached
|
35
|
+
|
36
|
+
pkg = uncached_infer_from(key, [], resolver)
|
37
|
+
@inference_cache[key] = pkg
|
38
|
+
@inverse_cache[pkg] ||= []
|
39
|
+
@inverse_cache[pkg] << key
|
40
|
+
|
41
|
+
pkg
|
42
|
+
end
|
43
|
+
|
44
|
+
# ruby --disable-gems -e 'puts Object.constants'
|
45
|
+
CORE = Set.new(%w(
|
46
|
+
Object Module Class BasicObject Kernel NilClass NIL Data TrueClass TRUE
|
47
|
+
FalseClass FALSE Encoding Comparable Enumerable String Symbol Exception
|
48
|
+
SystemExit SignalException Interrupt StandardError TypeError
|
49
|
+
ArgumentError IndexError KeyError RangeError ScriptError SyntaxError
|
50
|
+
LoadError NotImplementedError NameError NoMethodError RuntimeError
|
51
|
+
SecurityError NoMemoryError EncodingError SystemCallError Errno
|
52
|
+
UncaughtThrowError ZeroDivisionError FloatDomainError Numeric Integer
|
53
|
+
Fixnum Float Bignum Array Hash ENV Struct RegexpError Regexp MatchData
|
54
|
+
Marshal Range IOError EOFError IO STDIN STDOUT STDERR ARGF FileTest File
|
55
|
+
Dir Time Random Signal Proc LocalJumpError SystemStackError Method
|
56
|
+
UnboundMethod Binding Math GC ObjectSpace Enumerator StopIteration
|
57
|
+
RubyVM Thread TOPLEVEL_BINDING ThreadGroup ThreadError ClosedQueueError
|
58
|
+
Mutex Queue SizedQueue ConditionVariable Process Fiber FiberError
|
59
|
+
Rational Complex RUBY_VERSION RUBY_RELEASE_DATE RUBY_PLATFORM
|
60
|
+
RUBY_PATCHLEVEL RUBY_REVISION RUBY_DESCRIPTION RUBY_COPYRIGHT
|
61
|
+
RUBY_ENGINE RUBY_ENGINE_VERSION TracePoint ARGV DidYouMean
|
62
|
+
)).freeze
|
63
|
+
|
64
|
+
CORE_PACKAGE = 'core'.freeze
|
65
|
+
CORE_STDLIB_PACKAGE = 'core:stdlib'.freeze
|
66
|
+
APPLICATION_PACKAGE = 'application'.freeze
|
67
|
+
GEM_PACKAGE_PREFIX = 'gems:'.freeze
|
68
|
+
UNKNOWN_PACKAGE = 'unknown'.freeze
|
69
|
+
UNKNOWN_EVAL_PACKAGE = 'unknown:eval'.freeze
|
70
|
+
|
71
|
+
def uncached_infer_from(key, exclusions, resolver)
|
72
|
+
return CORE_PACKAGE if CORE.include?(nesting_first(key))
|
73
|
+
|
74
|
+
filename = determine_filename(key, resolver)
|
75
|
+
|
76
|
+
if filename.nil?
|
77
|
+
return try_harder(key, exclusions, resolver)
|
78
|
+
end
|
79
|
+
|
80
|
+
return APPLICATION_PACKAGE if filename.start_with?(Mirrors.project_root)
|
81
|
+
return CORE_STDLIB_PACKAGE if filename.start_with?(rubylibdir)
|
82
|
+
|
83
|
+
if pkg = try_rubygems(filename)
|
84
|
+
return pkg
|
85
|
+
end
|
86
|
+
|
87
|
+
if pkg = try_bundler(filename)
|
88
|
+
return pkg
|
89
|
+
end
|
90
|
+
|
91
|
+
return UNKNOWN_EVAL_PACKAGE if filename == '(eval)'
|
92
|
+
|
93
|
+
UNKNOWN_PACKAGE
|
94
|
+
end
|
95
|
+
|
96
|
+
def try_rubygems(filename)
|
97
|
+
if defined?(Gem)
|
98
|
+
gem_path.each do |path|
|
99
|
+
next unless filename.start_with?(path)
|
100
|
+
# extract e.g. 'bundler-1.13.6'
|
101
|
+
gem_with_version = filename[path.size..-1].sub(%r{/.*}, '')
|
102
|
+
if gem_with_version =~ /(.*)-(\d|[a-f0-9]+$)/
|
103
|
+
return GEM_PACKAGE_PREFIX + $1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def try_bundler(filename)
|
111
|
+
if defined?(Bundler)
|
112
|
+
path = bundle_path
|
113
|
+
if filename.start_with?(path)
|
114
|
+
gem_with_version = filename[path.size..-1].sub(%r{/.*}, '')
|
115
|
+
if gem_with_version =~ /(.*)-(\d|[a-f0-9]+$)/
|
116
|
+
return GEM_PACKAGE_PREFIX + $1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def determine_filename(key, resolver)
|
124
|
+
if fn = CLASS_DEFINITION_POINTS[key]
|
125
|
+
return fn
|
126
|
+
end
|
127
|
+
resolver.resolve(Object.const_get(key))
|
128
|
+
end
|
129
|
+
|
130
|
+
def try_harder(key, exclusions, resolver)
|
131
|
+
obj = Object.const_get(key)
|
132
|
+
return 'obj-not-module' unless obj.is_a?(Module)
|
133
|
+
exclusions << obj
|
134
|
+
|
135
|
+
obj.constants.each do |const|
|
136
|
+
child = obj.const_get(const)
|
137
|
+
next unless child.is_a?(Module)
|
138
|
+
|
139
|
+
next if exclusions.include?(child)
|
140
|
+
|
141
|
+
pkg = uncached_infer_from(Mirrors.module_instance_invoke(child, :inspect), exclusions, resolver)
|
142
|
+
return pkg unless pkg == 'unknown'
|
143
|
+
end
|
144
|
+
|
145
|
+
return 'unknown'
|
146
|
+
end
|
147
|
+
|
148
|
+
def nesting_first(n)
|
149
|
+
n.sub(/::.*/, '')
|
150
|
+
end
|
151
|
+
|
152
|
+
def rubylibdir
|
153
|
+
@rubylibdir ||= RbConfig::CONFIG['rubylibdir']
|
154
|
+
end
|
155
|
+
|
156
|
+
def gem_path
|
157
|
+
@gem_path ||= Gem.path.map { |p| "#{p}/gems/" }
|
158
|
+
end
|
159
|
+
|
160
|
+
def bundle_path
|
161
|
+
@bundle_path ||= "#{Bundler.bundle_path}/bundler/gems/"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Mirrors
|
2
|
+
module PackageInference
|
3
|
+
class ClassToFileResolver
|
4
|
+
def initialize
|
5
|
+
@files = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def resolve(klass)
|
9
|
+
return nil if klass.nil?
|
10
|
+
try_fast(klass, klass.name) ||
|
11
|
+
try_fast(klass.singleton_class, klass.name) ||
|
12
|
+
try_slow(klass) ||
|
13
|
+
try_slow(klass.singleton_class)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def try_fast(klass, class_name)
|
19
|
+
klass.instance_methods(false).each do |name|
|
20
|
+
meth = klass.instance_method(name)
|
21
|
+
|
22
|
+
file = begin
|
23
|
+
sl = meth.source_location
|
24
|
+
next unless sl
|
25
|
+
sl[0]
|
26
|
+
rescue MethodSource::SourceNotFoundError
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
contents = (@files[file] ||= File.open(file, 'r') { |f| f.readpartial(4096) })
|
31
|
+
n = class_name.sub(/.*::/, '') # last component of module name
|
32
|
+
return file if contents =~ /^\s+(class|module) ([\S]+::)?#{Regexp.quote(n)}\s/
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def try_slow(klass)
|
38
|
+
methods = klass
|
39
|
+
.instance_methods(false)
|
40
|
+
.map { |n| klass.instance_method(n) }
|
41
|
+
|
42
|
+
defined_directly_on_class = methods
|
43
|
+
.select do |meth|
|
44
|
+
# as a mostly-useful heuristic, we just eliminate everything that was
|
45
|
+
# defined using a template eval or define_method.
|
46
|
+
meth.source =~ /\A\s+def (self\.)?#{Regexp.quote(meth.name)}/
|
47
|
+
end
|
48
|
+
|
49
|
+
files = Hash.new(0)
|
50
|
+
|
51
|
+
defined_directly_on_class.each do |meth|
|
52
|
+
begin
|
53
|
+
sl = meth.source_location[0]
|
54
|
+
raise unless sl
|
55
|
+
files[sl[0]] += 1
|
56
|
+
rescue MethodSource::SourceNotFoundError
|
57
|
+
raise
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
file = files.max_by { |_k, v| v }
|
62
|
+
file ? file[0] : nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'mirrors/mirror'
|
2
|
+
require 'mirrors/package_inference'
|
3
|
+
|
4
|
+
module Mirrors
|
5
|
+
class PackageMirror < Mirror
|
6
|
+
def name
|
7
|
+
@subject.sub(/.*:/, '')
|
8
|
+
end
|
9
|
+
|
10
|
+
def fullname
|
11
|
+
@subject
|
12
|
+
end
|
13
|
+
|
14
|
+
def children
|
15
|
+
names = PackageInference.contents_of_package(@subject)
|
16
|
+
classes = (names || [])
|
17
|
+
.map { |n| Object.const_get(n) }
|
18
|
+
.select { |c| c.is_a?(Module) }
|
19
|
+
.sort_by(&:name)
|
20
|
+
class_mirrors = mirrors(classes)
|
21
|
+
|
22
|
+
# .map { |pkg| pkg.sub(/#{Regexp.quote(@subject)}:.*?:.*/) }
|
23
|
+
subpackages = PackageInference.qualified_packages
|
24
|
+
.select { |pkg| pkg.start_with?("#{@subject}:") }
|
25
|
+
.sort
|
26
|
+
|
27
|
+
puts subpackages.inspect
|
28
|
+
|
29
|
+
package_mirrors = subpackages.map { |pkg| PackageMirror.reflect(pkg) }
|
30
|
+
package_mirrors.concat(class_mirrors)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'mirrors/visitors/iseq_visitor'
|
2
|
+
|
3
|
+
module Mirrors
|
4
|
+
# DisasmVisitor prints a disassembled version of the bytecodes
|
5
|
+
# in a format similar to that used by the disasm() method.
|
6
|
+
class DisasmVisitor < Mirrors::ISeqVisitor
|
7
|
+
def visit(bytecode)
|
8
|
+
puts " #{'%03d' % @pc} #{bytecode} (#{@line})"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'mirrors/visitors/yasmdata'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
module Mirrors
|
5
|
+
# ISeqVisitor is an abstract class that knows how to walk methods and
|
6
|
+
# call the visit() method for each instruction. Internally it tracks the
|
7
|
+
# state of the current @pc, @line, and @label during the walk.
|
8
|
+
#
|
9
|
+
class ISeqVisitor
|
10
|
+
attr_reader :iseq, :field_refs, :method_refs, :class_refs
|
11
|
+
|
12
|
+
# visit all the instructions in the supplied method
|
13
|
+
def call(method)
|
14
|
+
@method = method
|
15
|
+
@iseq = method.native_code
|
16
|
+
|
17
|
+
# extract fields from iseq
|
18
|
+
@magic,
|
19
|
+
@major_version,
|
20
|
+
@minor_version,
|
21
|
+
@format_type,
|
22
|
+
@misc,
|
23
|
+
@label,
|
24
|
+
@path,
|
25
|
+
@absolute_path,
|
26
|
+
@first_lineno,
|
27
|
+
@type,
|
28
|
+
@locals,
|
29
|
+
@params,
|
30
|
+
@catch_table,
|
31
|
+
@bytecode = @iseq.to_a
|
32
|
+
|
33
|
+
# walk state
|
34
|
+
@pc = 0 # program counter
|
35
|
+
@label # current label
|
36
|
+
walk
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# iterator call once for each opcode
|
41
|
+
def visit(_bytecode)
|
42
|
+
raise NotImplementedError, "subclass responsibility"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# walk the opcodes
|
48
|
+
def walk
|
49
|
+
return unless @bytecode # C extensions have no bytecode
|
50
|
+
|
51
|
+
@pc = 0
|
52
|
+
@label = nil
|
53
|
+
@bytecode.each_with_index do |bc|
|
54
|
+
if (bc.class == Integer || bc.class == Fixnum)
|
55
|
+
@line = bc # bare line number
|
56
|
+
next # line numbers are not executable
|
57
|
+
elsif bc.class == Symbol
|
58
|
+
@label = bc
|
59
|
+
next # labels are not executable
|
60
|
+
elsif bc.class == Array
|
61
|
+
@opcode = VM::InstructionSequence::Instruction.id2insn_no(bc.first)
|
62
|
+
unrecognized_bytecode(bc) unless @opcode
|
63
|
+
visit(bc)
|
64
|
+
@pc += VM::InstructionSequence::Instruction.insn_no2size(@opcode)
|
65
|
+
else
|
66
|
+
unrecognized_bytecode(bc)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# emit diagnostics and signal that an unrecognized opcode was encountered
|
74
|
+
def unrecognized_bytecode(bc)
|
75
|
+
puts "-----------------bytecode ---------------------"
|
76
|
+
puts "bytecode=#{bc} clazz=#{bc.class}"
|
77
|
+
puts "---------------- disassembly ------------------"
|
78
|
+
puts @iseq.disasm
|
79
|
+
puts "---------------- bytecode ------------------"
|
80
|
+
pp @bytecode
|
81
|
+
raise "Urecognized bytecode:#{bc} at index:#{@pc}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'mirrors/visitors/iseq_visitor'
|
2
|
+
require 'mirrors/index/marker'
|
3
|
+
|
4
|
+
module Mirrors
|
5
|
+
# ReferencesVisitor examines opcodes and records references to
|
6
|
+
# classes, methods, and fields
|
7
|
+
|
8
|
+
class ReferencesVisitor < Mirrors::ISeqVisitor
|
9
|
+
attr_reader :markers
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
@markers = []
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def visit(bytecode)
|
19
|
+
case bytecode.first
|
20
|
+
when :getinstancevariable
|
21
|
+
@markers << field_marker(bytecode[1])
|
22
|
+
when :getconstant
|
23
|
+
@markers << class_marker(bytecode.last)
|
24
|
+
when :opt_send_without_block
|
25
|
+
@markers << method_marker(bytecode[1][:mid])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def class_marker(name)
|
32
|
+
Marker.new(
|
33
|
+
type: Mirrors::Marker::TYPE_CLASS_REFERENCE,
|
34
|
+
message: name,
|
35
|
+
file: @absolute_path,
|
36
|
+
line: @line
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def field_marker(name)
|
41
|
+
Marker.new(
|
42
|
+
type: Mirrors::Marker::TYPE_FIELD_REFERENCE,
|
43
|
+
message: name,
|
44
|
+
file: @absolute_path,
|
45
|
+
line: @line
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_marker(name)
|
50
|
+
Marker.new(
|
51
|
+
type: Mirrors::Marker::TYPE_METHOD_REFERENCE,
|
52
|
+
message: name,
|
53
|
+
file: @absolute_path,
|
54
|
+
line: @line
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|