system_navigation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENCE.txt +19 -0
- data/README.md +70 -0
- data/VERSION +1 -0
- data/lib/system_navigation.rb +374 -0
- data/lib/system_navigation/ancestor_method_finder.rb +44 -0
- data/lib/system_navigation/array_refinement.rb +21 -0
- data/lib/system_navigation/compiled_method.rb +94 -0
- data/lib/system_navigation/expression_tree.rb +95 -0
- data/lib/system_navigation/instruction_stream.rb +33 -0
- data/lib/system_navigation/instruction_stream/decoder.rb +91 -0
- data/lib/system_navigation/instruction_stream/instruction.rb +180 -0
- data/lib/system_navigation/instruction_stream/instruction/attr_instruction.rb +90 -0
- data/lib/system_navigation/method_hash.rb +49 -0
- data/lib/system_navigation/method_query.rb +98 -0
- data/lib/system_navigation/module_refinement.rb +226 -0
- data/lib/system_navigation/ruby_environment.rb +75 -0
- metadata +117 -0
@@ -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
|