system_navigation 0.1.0

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.
@@ -0,0 +1,44 @@
1
+ class SystemNavigation
2
+ class AncestorMethodFinder
3
+ using ArrayRefinement
4
+
5
+ def self.find_all_ancestors(of:)
6
+ finder = self.new(of)
7
+ singleton_finder = self.new(of.singleton_class)
8
+
9
+ ancestor_list = []
10
+
11
+ ancestor_list.concat(finder.find_all_methods)
12
+ ancestor_list.concat(singleton_finder.find_all_methods)
13
+
14
+ ancestor_list
15
+ end
16
+
17
+ def initialize(behavior)
18
+ @behavior = behavior
19
+ @ancestor_list = behavior.ancestors - [behavior]
20
+ end
21
+
22
+ def find_all_methods
23
+ self.find_closest_ancestors(@ancestor_list).flat_map do |ancestor|
24
+ selectors = MethodHash.create(based_on: ancestor, include_super: false)
25
+ # No inheritance for singletons in Ruby
26
+ [:public, :private, :protected].each { |t| selectors[t][:singleton] = [] }
27
+
28
+ MethodQuery.execute(collection: selectors,
29
+ query: :convert_to_methods,
30
+ behavior: @behavior)
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def find_closest_ancestors(ancestors)
37
+ if @behavior.is_a?(Class)
38
+ ancestors.split(@behavior.superclass).first || []
39
+ else
40
+ ancestors
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ class SystemNavigation
2
+ module ArrayRefinement
3
+ refine Array do
4
+ def split(value)
5
+ results, arr = [[]], self.dup
6
+
7
+ until arr.empty?
8
+ if (idx = arr.index(value))
9
+ results.last.concat(arr.shift(idx))
10
+ arr.shift
11
+ results << []
12
+ else
13
+ results.last.concat(arr.shift(arr.size))
14
+ end
15
+ end
16
+
17
+ results
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,94 @@
1
+ class SystemNavigation
2
+ class CompiledMethod
3
+ def self.compile(method)
4
+ self.new(method).compile
5
+ end
6
+
7
+ def initialize(method)
8
+ @method = method
9
+ @scanner = SystemNavigation::InstructionStream.on(method)
10
+ @decoder = InstructionStream::Decoder.new(@scanner)
11
+
12
+ begin
13
+ @source = FastMethodSource.source_for(@method)
14
+ rescue FastMethodSource::SourceNotFoundError, IOError
15
+ @source = ''
16
+ end
17
+
18
+ begin
19
+ @comment = FastMethodSource.comment_for(@method)
20
+ rescue FastMethodSource::SourceNotFoundError, IOError
21
+ @comment = ''
22
+ end
23
+ end
24
+
25
+ def compile
26
+ @scanner.decode
27
+
28
+ self
29
+ end
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ @method.send(method_name, *args, &block)
33
+ end
34
+
35
+ def unwrap
36
+ @method
37
+ end
38
+
39
+ # Literals that are referenced by the receiver as described in
40
+ # `doc/syntax/literals.rdoc` in your Ruby, installation minus procs and
41
+ # backticks.
42
+ def has_literal?(literal)
43
+ return true if self.scan_for { @decoder.literal_scan(literal) }
44
+ return false if self.c_method?
45
+
46
+ exptree = ExpressionTree.of(method: @method, source: @source)
47
+ exptree.includes?(literal)
48
+ end
49
+
50
+ def reads_field?(ivar)
51
+ self.scan_for { @decoder.ivar_read_scan(ivar) }
52
+ end
53
+
54
+ def writes_field?(ivar)
55
+ self.scan_for { @decoder.ivar_write_scan(ivar) }
56
+ end
57
+
58
+ def sends_message?(message)
59
+ self.scan_for { @decoder.msg_send_scan(message) }
60
+ end
61
+
62
+ def source_contains?(string, match_case)
63
+ string = string.dup
64
+ code_and_comment = @source + @comment
65
+ code_and_comment.downcase! && string.downcase! unless match_case
66
+ !!code_and_comment.match(string)
67
+ end
68
+
69
+ def c_method?
70
+ @method.source_location.nil?
71
+ end
72
+
73
+ def rb_method?
74
+ !self.c_method?
75
+ end
76
+
77
+ def sent_messages
78
+ @decoder.scan_for_sent_messages
79
+ end
80
+
81
+ protected
82
+
83
+ def scan_for
84
+ @scanner.scan_for(yield)
85
+ end
86
+
87
+ def scan_for_literal(literal)
88
+ return false if self.c_method?
89
+
90
+ exptree = ExpressionTree.of(method: @method, source: @source)
91
+ exptree.includes?(literal)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,95 @@
1
+ class SystemNavigation
2
+ class ExpressionTree
3
+ def self.of(method:, source:)
4
+ tree = self.new(method: method, source: source)
5
+ tree.parse
6
+ tree
7
+ end
8
+
9
+ def initialize(method: nil, source: nil)
10
+ @method = method
11
+ @source = source
12
+ @keywords = []
13
+ @hashes = []
14
+ @arrays = []
15
+ @ranges = []
16
+ @tree = nil
17
+ end
18
+
19
+ def parse
20
+ return false if [@method, @source].compact.empty?
21
+
22
+ @tree = Ripper.sexp(@source)
23
+ return false unless @tree
24
+
25
+ parsed = @tree[1][0]
26
+ return false unless parsed.first == :def
27
+
28
+ method_body_tree = parsed[3][1]
29
+
30
+ self.walk(method_body_tree)
31
+
32
+ true
33
+ end
34
+
35
+ def includes?(obj)
36
+ built = Ripper.sexp(obj.inspect)
37
+ built_obj = built[1][0][1]
38
+
39
+ collection = case obj
40
+ when Array then @arrays
41
+ when Hash then @hashes
42
+ when Range then @ranges
43
+ else
44
+ []
45
+ end
46
+ !!find_includes(collection, built_obj) || @keywords.include?(obj.inspect)
47
+ end
48
+
49
+ protected
50
+
51
+ def find_includes(collection, obj)
52
+ collection.find do |item|
53
+ item = item[1]
54
+
55
+ unify(item)
56
+ unify(obj)
57
+
58
+ item == obj
59
+ end
60
+ end
61
+
62
+ def unify(node)
63
+ node.each do |n|
64
+ if n.instance_of?(Array)
65
+ if n.size == 2 && n.all? { |num| num.instance_of?(Fixnum) }
66
+ n[0] = 0
67
+ n[1] = 0
68
+ end
69
+
70
+ unify(n)
71
+ end
72
+ end
73
+ end
74
+
75
+ def walk(tree)
76
+ tree.each do |node|
77
+ walk_node(node)
78
+ end
79
+ end
80
+
81
+ def walk_node(node)
82
+ node.each_with_index do |n, i|
83
+ case n
84
+ when Array
85
+ walk_node(n)
86
+ when Symbol
87
+ @keywords << node[i + 1] if n == :@kw
88
+ @hashes << node if n == :hash
89
+ @arrays << node if n == :array
90
+ @ranges << node if n == :dot2 || n == :dot3
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,33 @@
1
+ class SystemNavigation
2
+ class InstructionStream
3
+ def self.on(method)
4
+ self.new(method: method)
5
+ end
6
+
7
+ attr_reader :method
8
+
9
+ def initialize(method: nil, iseq: nil)
10
+ @method = method
11
+ @iseq = iseq
12
+ end
13
+
14
+ def decode
15
+ @iseq ||= RubyVM::InstructionSequence.disasm(@method) || ''
16
+ end
17
+
18
+ def scan_for(selected_instructs)
19
+ selected_instructs.any?
20
+ end
21
+
22
+ def iseqs(sym)
23
+ iseqs = @iseq.split("\n")
24
+
25
+ if iseqs.any?
26
+ instructions = iseqs.map { |instruction| Instruction.parse(instruction) }
27
+ instructions.compact.select(&:vm_operative?)
28
+ else
29
+ Instruction::AttrInstruction.parse(@method, sym) || []
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,91 @@
1
+ class SystemNavigation
2
+ class InstructionStream
3
+ class Decoder
4
+ def initialize(scanner)
5
+ @scanner = scanner
6
+ end
7
+
8
+ def select_instructions(literal:, method_name: nil, &block)
9
+ instructions = @scanner.iseqs(method_name || literal)
10
+
11
+ instructions.select.with_index do |instruction, i|
12
+ prev = instructions[i - 1]
13
+ prev_prev = instructions[i - 2]
14
+
15
+ returned = block.call(prev_prev, prev, instruction)
16
+ next instruction if returned
17
+
18
+ next if !(instruction.evals? && prev.putstrings?(literal))
19
+
20
+ self.class.new(iseq_from_eval(prev, @scanner.method)).
21
+ __send__(block.binding.eval('__method__'), literal).any?
22
+ end
23
+ end
24
+
25
+ def ivar_read_scan(ivar)
26
+ self.select_instructions(literal: ivar) do |_prev_prev, prev, instruction|
27
+ next instruction if instruction.reads_ivar?(ivar)
28
+
29
+ if instruction.dynamically_reads_ivar? && prev.putobjects?(ivar)
30
+ next instruction
31
+ end
32
+ end
33
+ end
34
+
35
+ def ivar_write_scan(ivar)
36
+ self.select_instructions(literal: ivar) do |prev_prev, prev, instruction|
37
+ next instruction if instruction.writes_ivar?(ivar)
38
+
39
+ if instruction.dynamically_writes_ivar? && prev_prev.putobjects?(ivar)
40
+ next instruction
41
+ end
42
+ end
43
+ end
44
+
45
+ def literal_scan(literal)
46
+ name = @scanner.method.original_name
47
+
48
+ self.select_instructions(method_name: name, literal: literal) do |_prev_prev, prev, instruction|
49
+ if instruction.putobjects?(literal) ||
50
+ instruction.putnils?(literal) ||
51
+ instruction.duparrays?(literal) ||
52
+ instruction.putstrings?(literal)
53
+ next instruction
54
+ end
55
+ end
56
+ end
57
+
58
+ def msg_send_scan(message)
59
+ self.select_instructions(literal: message) do |_prev_prev, prev, instruction|
60
+ next instruction if instruction.sends_msg?(message)
61
+ end
62
+ end
63
+
64
+ def scan_for_sent_messages
65
+ @scanner.iseqs(nil).map do |instruction|
66
+ instruction.find_message
67
+ end.compact
68
+ end
69
+
70
+ private
71
+
72
+ def iseq_from_eval(instruction, method = nil)
73
+ # Avoid segfault if evaling_str is nil.
74
+ # See: https://bugs.ruby-lang.org/issues/11159
75
+ uncompiled = unwind_eval(instruction.evaling_str || nil.to_s)
76
+ uncompiled.gsub!(/\\n/, ?\n)
77
+
78
+ iseq = RubyVM::InstructionSequence.compile(uncompiled).disasm
79
+ InstructionStream.new(method: method, iseq: iseq)
80
+ end
81
+
82
+ def unwind_eval(eval_string)
83
+ eval_string.sub(/\A(eval\(\\?["'])*/, '').sub(/(\\?["']\))*\z/, '')
84
+ end
85
+
86
+ def sanitize_newlines(eval_string)
87
+ eval_string.gsub(/\\n/, ?\n)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,180 @@
1
+ class SystemNavigation
2
+ class InstructionStream
3
+ class Instruction
4
+ def self.parse(str)
5
+ self.new(str).parse
6
+ end
7
+
8
+ def initialize(str)
9
+ @raw = StringScanner.new(str)
10
+ @pos = nil
11
+ @opcode = ''
12
+ @operand = nil
13
+ @evaling_str = nil
14
+ @lineno = nil
15
+ @op_id = nil
16
+ @ivar = nil
17
+ @service_instruction = false
18
+ end
19
+
20
+ def parse
21
+ return if parse_service_instruction
22
+
23
+ parse_position
24
+ parse_opcode
25
+ parse_operand
26
+ parse_lineno
27
+ parse_op_id
28
+ parse_ivar
29
+
30
+ self
31
+ end
32
+
33
+ def parse_service_instruction
34
+ if @raw.peek(2) == '==' || @raw.peek(6) !~ /[0-9]{4,6} /
35
+ @service_instruction = true
36
+ end
37
+ end
38
+
39
+ def parse_position
40
+ n = @raw.scan(/[0-9]{4,6}/)
41
+ @pos = n.to_i if n
42
+ @raw.skip(/\s*/)
43
+ end
44
+
45
+ def parse_opcode
46
+ @opcode = @raw.scan(/[a-zA-Z0-9_]+/)
47
+ @raw.skip(/\s*/)
48
+ end
49
+
50
+ def parse_operand
51
+ if @raw.check(/</)
52
+ @operand = @raw.scan(/<.+>/)
53
+ elsif @raw.check(/\[/)
54
+ @operand = @raw.scan(/\[.*\]/)
55
+ elsif @raw.check(/"/)
56
+ @operand = @raw.scan(/".*"/)
57
+ elsif @raw.check(%r{/})
58
+ @operand = @raw.scan(%r{/.*/})
59
+ else
60
+ @operand = @raw.scan(/-?[0-9a-zA-Z:@_=.]+/)
61
+
62
+ if @raw.peek(1) == ','
63
+ @operand << @raw.scan(/[^\(]*/).rstrip
64
+ end
65
+ end
66
+
67
+ @raw.skip(/\s*\(?/)
68
+ @raw.skip(/\s*/)
69
+ end
70
+
71
+ def parse_lineno
72
+ n = @raw.scan(/[0-9]+/)
73
+ @lineno = n.to_i if n
74
+ @raw.skip(/\)/)
75
+ end
76
+
77
+ def parse_op_id
78
+ return unless sending?
79
+
80
+ callinfo = StringScanner.new(@operand)
81
+ callinfo.skip(/<callinfo!mid:/)
82
+ @op_id = callinfo.scan(/\S+(?=,)/)
83
+ callinfo.terminate
84
+ end
85
+
86
+ def parse_ivar
87
+ return unless accessing_ivar?
88
+
89
+ ivar = StringScanner.new(@operand)
90
+ @ivar = ivar.scan(/:[^,]+/)[1..-1].to_sym
91
+ ivar.terminate
92
+ end
93
+
94
+ def accessing_ivar?
95
+ @opcode == 'getinstancevariable' || @opcode == 'setinstancevariable'
96
+ end
97
+
98
+ def vm_operative?
99
+ @service_instruction == false
100
+ end
101
+
102
+ def reads_ivar?(ivar)
103
+ @opcode == 'getinstancevariable' && @ivar == ivar
104
+ end
105
+
106
+ def writes_ivar?(ivar)
107
+ @opcode == 'setinstancevariable' && @ivar == ivar
108
+ end
109
+
110
+ def dynamically_reads_ivar?
111
+ @op_id == 'instance_variable_get'
112
+ end
113
+
114
+ def dynamically_writes_ivar?
115
+ @op_id == 'instance_variable_set'
116
+ end
117
+
118
+ def evals?
119
+ @op_id == 'eval'
120
+ end
121
+
122
+ def putstrings?(str)
123
+ return false unless @opcode == 'putstring'
124
+
125
+ s = str.inspect
126
+
127
+ return true if @operand == s || @operand == %|"#{s}"|
128
+ if @operand.match(/(eval\()?.*:?#{str}[^[\w;]].*\)?/)
129
+ return true
130
+ else
131
+
132
+ end
133
+
134
+ false
135
+ end
136
+
137
+ def putobjects?(str)
138
+ return false unless @opcode == 'putobject'
139
+
140
+ return true if @operand.match(/(?::#{str}\z|\[.*:#{str},.*\])/)
141
+ return true if @operand == str.inspect
142
+
143
+ false
144
+ end
145
+
146
+ def putnils?(str)
147
+ return false unless @opcode == 'putnil'
148
+ @operand == str.inspect
149
+ end
150
+
151
+ def duparrays?(str)
152
+ !!(@opcode == 'duparray' && @operand.match(/:#{str}[,\]]/))
153
+ end
154
+
155
+ def sends_msg?(message)
156
+ !!(sending? && @op_id == message.to_s)
157
+ end
158
+
159
+ def operand
160
+ @operand
161
+ end
162
+
163
+ def evaling_str
164
+ @evaling_str ||= @operand.sub!(/\A"(.+)"/, '\1')
165
+ end
166
+
167
+ def find_message
168
+ return unless sending?
169
+
170
+ @op_id
171
+ end
172
+
173
+ private
174
+
175
+ def sending?
176
+ @opcode == 'opt_send_without_block' || @opcode == 'send'
177
+ end
178
+ end
179
+ end
180
+ end