mirrors 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +2 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +23 -0
  6. data/LICENSE.txt +59 -0
  7. data/README.md +1 -0
  8. data/asdfasdf.rb +53 -0
  9. data/bin/bundler +17 -0
  10. data/bin/byebug +17 -0
  11. data/bin/testunit +8 -0
  12. data/circle.yml +6 -0
  13. data/dev.yml +6 -0
  14. data/lib/mirrors.rb +150 -0
  15. data/lib/mirrors/class_mirror.rb +197 -0
  16. data/lib/mirrors/class_mixin.rb +11 -0
  17. data/lib/mirrors/field_mirror.rb +24 -0
  18. data/lib/mirrors/field_mirror/class_variable_mirror.rb +23 -0
  19. data/lib/mirrors/field_mirror/constant_mirror.rb +34 -0
  20. data/lib/mirrors/field_mirror/instance_variable_mirror.rb +23 -0
  21. data/lib/mirrors/hook.rb +33 -0
  22. data/lib/mirrors/index/indexer.rb +6 -0
  23. data/lib/mirrors/index/marker.rb +40 -0
  24. data/lib/mirrors/invoke.rb +29 -0
  25. data/lib/mirrors/method_mirror.rb +206 -0
  26. data/lib/mirrors/mirror.rb +37 -0
  27. data/lib/mirrors/object_mirror.rb +25 -0
  28. data/lib/mirrors/package_inference.rb +164 -0
  29. data/lib/mirrors/package_inference/class_to_file_resolver.rb +66 -0
  30. data/lib/mirrors/package_mirror.rb +33 -0
  31. data/lib/mirrors/visitors/disasm_visitor.rb +11 -0
  32. data/lib/mirrors/visitors/iseq_visitor.rb +84 -0
  33. data/lib/mirrors/visitors/references_visitor.rb +58 -0
  34. data/lib/mirrors/visitors/yasmdata.rb +212 -0
  35. data/lol.rb +35 -0
  36. data/mirrors.gemspec +19 -0
  37. data/test/fixtures/class.rb +29 -0
  38. data/test/fixtures/field.rb +9 -0
  39. data/test/fixtures/method.rb +15 -0
  40. data/test/fixtures/object.rb +5 -0
  41. data/test/fixtures/reflect.rb +14 -0
  42. data/test/mirrors/class_mirror_test.rb +87 -0
  43. data/test/mirrors/field_mirror_test.rb +125 -0
  44. data/test/mirrors/iseq_visitor_test.rb +56 -0
  45. data/test/mirrors/marker_test.rb +48 -0
  46. data/test/mirrors/method_mirror_test.rb +62 -0
  47. data/test/mirrors/object_mirror_test.rb +16 -0
  48. data/test/mirrors/package_inference_test.rb +31 -0
  49. data/test/mirrors/references_visitor_test.rb +30 -0
  50. data/test/mirrors_test.rb +38 -0
  51. data/test/test_helper.rb +12 -0
  52. 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