system_navigation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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