mirrors 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|