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