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.
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