typeprof 0.15.3 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -1
- data/Gemfile.lock +4 -4
- data/exe/typeprof +5 -1
- data/lib/typeprof/analyzer.rb +228 -54
- data/lib/typeprof/arguments.rb +1 -0
- data/lib/typeprof/builtin.rb +23 -23
- data/lib/typeprof/cli.rb +22 -2
- data/lib/typeprof/code-range.rb +177 -0
- data/lib/typeprof/config.rb +43 -18
- data/lib/typeprof/container-type.rb +3 -0
- data/lib/typeprof/export.rb +191 -15
- data/lib/typeprof/import.rb +25 -4
- data/lib/typeprof/iseq.rb +218 -16
- data/lib/typeprof/lsp.rb +865 -0
- data/lib/typeprof/method.rb +15 -11
- data/lib/typeprof/type.rb +46 -38
- data/lib/typeprof/utils.rb +18 -1
- data/lib/typeprof/version.rb +1 -1
- data/lib/typeprof.rb +3 -0
- data/typeprof-lsp +3 -0
- data/typeprof.gemspec +1 -1
- data/vscode/.gitignore +5 -0
- data/vscode/.vscode/launch.json +16 -0
- data/vscode/.vscodeignore +7 -0
- data/vscode/README.md +22 -0
- data/vscode/development.md +31 -0
- data/vscode/package-lock.json +2211 -0
- data/vscode/package.json +71 -0
- data/vscode/sandbox/test.rb +24 -0
- data/vscode/src/extension.ts +285 -0
- data/vscode/tsconfig.json +15 -0
- metadata +18 -5
data/lib/typeprof/iseq.rb
CHANGED
@@ -2,6 +2,17 @@ module TypeProf
|
|
2
2
|
class ISeq
|
3
3
|
# https://github.com/ruby/ruby/pull/4468
|
4
4
|
CASE_WHEN_CHECKMATCH = RubyVM::InstructionSequence.compile("case 1; when Integer; end").to_a.last.any? {|insn,| insn == :checkmatch }
|
5
|
+
# https://github.com/ruby/ruby/blob/v3_0_2/vm_core.h#L1206
|
6
|
+
VM_ENV_DATA_SIZE = 3
|
7
|
+
# Check if Ruby 3.1 or later
|
8
|
+
RICH_AST = begin RubyVM::AbstractSyntaxTree.parse("1", keep_script_lines: true).node_id; true; rescue; false; end
|
9
|
+
|
10
|
+
FileInfo = Struct.new(
|
11
|
+
:node_id2node,
|
12
|
+
:definition_table,
|
13
|
+
:caller_table,
|
14
|
+
:created_iseqs,
|
15
|
+
)
|
5
16
|
|
6
17
|
class << self
|
7
18
|
def compile(file)
|
@@ -20,17 +31,62 @@ module TypeProf
|
|
20
31
|
opt[:operands_unification] = false
|
21
32
|
opt[:coverage_enabled] = false
|
22
33
|
|
34
|
+
parse_opts = {}
|
35
|
+
parse_opts[:keep_script_lines] = true if RICH_AST
|
36
|
+
|
23
37
|
if str
|
38
|
+
node = RubyVM::AbstractSyntaxTree.parse(str, **parse_opts)
|
24
39
|
iseq = RubyVM::InstructionSequence.compile(str, path, **opt)
|
25
40
|
else
|
41
|
+
node = RubyVM::AbstractSyntaxTree.parse_file(path, **parse_opts)
|
26
42
|
iseq = RubyVM::InstructionSequence.compile_file(path, **opt)
|
27
43
|
end
|
28
44
|
|
29
|
-
|
45
|
+
node_id2node = {}
|
46
|
+
build_ast_node_id_table(node, node_id2node) if RICH_AST
|
47
|
+
|
48
|
+
file_info = FileInfo.new(node_id2node, CodeRangeTable.new, CodeRangeTable.new, [])
|
49
|
+
iseq_rb = new(iseq.to_a, file_info)
|
50
|
+
iseq_rb.collect_local_variable_info(file_info) if RICH_AST
|
51
|
+
file_info.created_iseqs.each do |iseq|
|
52
|
+
iseq.unify_instructions
|
53
|
+
end
|
54
|
+
|
55
|
+
return iseq_rb, file_info.definition_table, file_info.caller_table
|
56
|
+
end
|
57
|
+
|
58
|
+
private def build_ast_node_id_table(node, tbl = {})
|
59
|
+
tbl[node.node_id] = node
|
60
|
+
node.children.each do |child|
|
61
|
+
build_ast_node_id_table(child, tbl) if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
62
|
+
end
|
63
|
+
tbl
|
64
|
+
end
|
65
|
+
|
66
|
+
def code_range_from_node(node)
|
67
|
+
CodeRange.new(
|
68
|
+
CodeLocation.new(node.first_lineno, node.first_column),
|
69
|
+
CodeLocation.new(node.last_lineno, node.last_column),
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_node_by_id(node, id)
|
74
|
+
node = RubyVM::AbstractSyntaxTree.parse(node) if node.is_a?(String)
|
75
|
+
|
76
|
+
return node if id == node.node_id
|
77
|
+
|
78
|
+
node.children.each do |child|
|
79
|
+
if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
80
|
+
ret = find_node_by_id(child, id)
|
81
|
+
return ret if ret
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
nil
|
30
86
|
end
|
31
87
|
end
|
32
88
|
|
33
|
-
Insn = Struct.new(:insn, :operands, :lineno)
|
89
|
+
Insn = Struct.new(:insn, :operands, :lineno, :code_range, :definitions)
|
34
90
|
class Insn
|
35
91
|
def check?(insn_cmp, operands_cmp = nil)
|
36
92
|
return insn == insn_cmp && (!operands_cmp || operands == operands_cmp)
|
@@ -39,14 +95,19 @@ module TypeProf
|
|
39
95
|
|
40
96
|
ISEQ_FRESH_ID = [0]
|
41
97
|
|
42
|
-
def initialize(iseq)
|
98
|
+
def initialize(iseq, file_info)
|
99
|
+
file_info.created_iseqs << self
|
100
|
+
|
43
101
|
@id = (ISEQ_FRESH_ID[0] += 1)
|
44
102
|
|
45
|
-
_magic, _major_version, _minor_version, _format_type,
|
103
|
+
_magic, _major_version, _minor_version, _format_type, misc,
|
46
104
|
@name, @path, @absolute_path, @start_lineno, @type,
|
47
105
|
@locals, @fargs_format, catch_table, insns = *iseq
|
48
106
|
|
49
|
-
|
107
|
+
fl, fc, ll, lc = misc[:code_location]
|
108
|
+
@iseq_code_range = CodeRange.new(CodeLocation.new(fl, fc), CodeLocation.new(ll, lc))
|
109
|
+
|
110
|
+
convert_insns(insns, misc[:node_ids] || [], file_info)
|
50
111
|
|
51
112
|
add_body_start_marker(insns)
|
52
113
|
|
@@ -54,13 +115,13 @@ module TypeProf
|
|
54
115
|
|
55
116
|
labels = create_label_table(insns)
|
56
117
|
|
57
|
-
@insns = setup_insns(insns, labels)
|
118
|
+
@insns = setup_insns(insns, labels, file_info)
|
58
119
|
|
59
120
|
@fargs_format[:opt] = @fargs_format[:opt].map {|l| labels[l] } if @fargs_format[:opt]
|
60
121
|
|
61
122
|
@catch_table = []
|
62
123
|
catch_table.map do |type, iseq, first, last, cont, stack_depth|
|
63
|
-
iseq = iseq ? ISeq.new(iseq) : nil
|
124
|
+
iseq = iseq ? ISeq.new(iseq, file_info) : nil
|
64
125
|
target = labels[cont]
|
65
126
|
entry = [type, iseq, target, stack_depth]
|
66
127
|
labels[first].upto(labels[last]) do |i|
|
@@ -69,17 +130,78 @@ module TypeProf
|
|
69
130
|
end
|
70
131
|
end
|
71
132
|
|
133
|
+
def_node_id = misc[:def_node_id]
|
134
|
+
if def_node_id && file_info.node_id2node[def_node_id] && (@type == :method || @type == :block)
|
135
|
+
def_node = file_info.node_id2node[def_node_id]
|
136
|
+
method_name_token_range = extract_method_name_token_range(def_node)
|
137
|
+
if method_name_token_range
|
138
|
+
@callers = Utils::MutableSet.new
|
139
|
+
file_info.caller_table[method_name_token_range] = @callers
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
72
143
|
rename_insn_types
|
144
|
+
end
|
73
145
|
|
74
|
-
|
146
|
+
def extract_method_name_token_range(node)
|
147
|
+
case @type
|
148
|
+
when :method
|
149
|
+
regex = if node.type == :DEFS
|
150
|
+
/^def\s+(?:\w+)\s*\.\s*(\w+)/
|
151
|
+
else
|
152
|
+
/^def\s+(\w+)/
|
153
|
+
end
|
154
|
+
return nil unless node.source =~ regex
|
155
|
+
zero_loc = CodeLocation.new(1, 0)
|
156
|
+
name_start = $~.begin(1)
|
157
|
+
name_length = $~.end(1) - name_start
|
158
|
+
name_head_loc = zero_loc.advance_cursor(name_start, node.source)
|
159
|
+
name_tail_loc = name_head_loc.advance_cursor(name_length, node.source)
|
160
|
+
return CodeRange.new(
|
161
|
+
CodeLocation.new(
|
162
|
+
node.first_lineno + (name_head_loc.lineno - 1),
|
163
|
+
name_head_loc.lineno == 1 ? node.first_column + name_head_loc.column : name_head_loc.column
|
164
|
+
),
|
165
|
+
CodeLocation.new(
|
166
|
+
node.first_lineno + (name_tail_loc.lineno - 1),
|
167
|
+
name_tail_loc.lineno == 1 ? node.first_column + name_tail_loc.column : name_tail_loc.column
|
168
|
+
),
|
169
|
+
)
|
170
|
+
when :block
|
171
|
+
return ISeq.code_range_from_node(node)
|
172
|
+
end
|
75
173
|
end
|
76
174
|
|
77
175
|
def source_location(pc)
|
78
176
|
"#{ @path }:#{ @insns[pc].lineno }"
|
79
177
|
end
|
80
178
|
|
179
|
+
def detailed_source_location(pc)
|
180
|
+
code_range = @insns[pc].code_range
|
181
|
+
if code_range
|
182
|
+
[@path, code_range]
|
183
|
+
else
|
184
|
+
[@path]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def add_called_iseq(pc, callee_iseq)
|
189
|
+
if callee_iseq && @insns[pc].definitions
|
190
|
+
@insns[pc].definitions << [callee_iseq.path, callee_iseq.iseq_code_range]
|
191
|
+
end
|
192
|
+
if callee_iseq.callers
|
193
|
+
callee_iseq.callers << [@path, @insns[pc].code_range]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_def_loc(pc, detailed_loc)
|
198
|
+
if detailed_loc && @insns[pc].definitions
|
199
|
+
@insns[pc].definitions << detailed_loc
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
81
203
|
attr_reader :name, :path, :absolute_path, :start_lineno, :type, :locals, :fargs_format, :catch_table, :insns
|
82
|
-
attr_reader :id
|
204
|
+
attr_reader :id, :iseq_code_range, :callers
|
83
205
|
|
84
206
|
def pretty_print(q)
|
85
207
|
q.text "ISeq["
|
@@ -118,7 +240,7 @@ module TypeProf
|
|
118
240
|
end
|
119
241
|
|
120
242
|
# Remove lineno entry and convert instructions to Insn instances
|
121
|
-
def convert_insns(insns)
|
243
|
+
def convert_insns(insns, node_ids, file_info)
|
122
244
|
ninsns = []
|
123
245
|
lineno = 0
|
124
246
|
insns.each do |e|
|
@@ -129,7 +251,25 @@ module TypeProf
|
|
129
251
|
ninsns << e
|
130
252
|
when Array
|
131
253
|
insn, *operands = e
|
132
|
-
|
254
|
+
node_id = node_ids.shift
|
255
|
+
node = file_info.node_id2node[node_id]
|
256
|
+
if node
|
257
|
+
code_range = ISeq.code_range_from_node(node)
|
258
|
+
case insn
|
259
|
+
when :send, :invokesuper
|
260
|
+
opt, blk_iseq = operands
|
261
|
+
opt[:node_id] = node_id
|
262
|
+
if blk_iseq
|
263
|
+
misc = blk_iseq[4] # iseq's "misc" field
|
264
|
+
misc[:def_node_id] = node_id
|
265
|
+
end
|
266
|
+
when :definemethod, :definesmethod
|
267
|
+
iseq = operands[1]
|
268
|
+
misc = iseq[4] # iseq's "misc" field
|
269
|
+
misc[:def_node_id] = node_id
|
270
|
+
end
|
271
|
+
end
|
272
|
+
ninsns << Insn.new(insn, operands, lineno, code_range, nil)
|
133
273
|
else
|
134
274
|
raise "unknown iseq entry: #{ e }"
|
135
275
|
end
|
@@ -156,7 +296,7 @@ module TypeProf
|
|
156
296
|
i = insns.index(label) + 1
|
157
297
|
end
|
158
298
|
|
159
|
-
insns.insert(i, Insn.new(:_iseq_body_start, [], @start_lineno))
|
299
|
+
insns.insert(i, Insn.new(:_iseq_body_start, [], @start_lineno, nil, nil))
|
160
300
|
end
|
161
301
|
end
|
162
302
|
|
@@ -198,7 +338,7 @@ module TypeProf
|
|
198
338
|
labels
|
199
339
|
end
|
200
340
|
|
201
|
-
def setup_insns(insns, labels)
|
341
|
+
def setup_insns(insns, labels, file_info)
|
202
342
|
ninsns = []
|
203
343
|
insns.each do |e|
|
204
344
|
case e
|
@@ -208,7 +348,7 @@ module TypeProf
|
|
208
348
|
operands = (INSN_TABLE[e.insn] || []).zip(e.operands).map do |type, operand|
|
209
349
|
case type
|
210
350
|
when "ISEQ"
|
211
|
-
operand && ISeq.new(operand)
|
351
|
+
operand && ISeq.new(operand, file_info)
|
212
352
|
when "lindex_t", "rb_num_t", "VALUE", "ID", "GENTRY", "CALL_DATA"
|
213
353
|
operand
|
214
354
|
when "OFFSET"
|
@@ -221,7 +361,12 @@ module TypeProf
|
|
221
361
|
end
|
222
362
|
end
|
223
363
|
|
224
|
-
|
364
|
+
if e.code_range && should_collect_defs(e.insn)
|
365
|
+
definition = Utils::MutableSet.new
|
366
|
+
file_info.definition_table[e.code_range] = definition
|
367
|
+
end
|
368
|
+
|
369
|
+
ninsns << Insn.new(e.insn, operands, e.lineno, e.code_range, definition)
|
225
370
|
else
|
226
371
|
raise "unknown iseq entry: #{ e }"
|
227
372
|
end
|
@@ -229,6 +374,63 @@ module TypeProf
|
|
229
374
|
ninsns
|
230
375
|
end
|
231
376
|
|
377
|
+
def should_collect_defs(insn_kind)
|
378
|
+
case insn_kind
|
379
|
+
when :send, :getinstancevariable, :getconstant
|
380
|
+
return true
|
381
|
+
else
|
382
|
+
return false
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# Collect local variable use and definition info recursively
|
387
|
+
def collect_local_variable_info(file_info, absolute_level = 0, parent_variable_tables = {})
|
388
|
+
# e.g.
|
389
|
+
# variable_tables[abs_level][idx] = [[path, code_range]]
|
390
|
+
current_variables = []
|
391
|
+
variable_tables = parent_variable_tables.merge({
|
392
|
+
absolute_level => current_variables
|
393
|
+
})
|
394
|
+
|
395
|
+
dummy_def_range = CodeRange.new(
|
396
|
+
CodeLocation.new(@start_lineno, 0),
|
397
|
+
CodeLocation.new(@start_lineno, 1),
|
398
|
+
)
|
399
|
+
# Fill tail elements with parameters
|
400
|
+
(@fargs_format[:lead_num] || 0).times do |offset|
|
401
|
+
current_variables[VM_ENV_DATA_SIZE + @locals.length - offset - 1] ||= Utils::MutableSet.new
|
402
|
+
current_variables[VM_ENV_DATA_SIZE + @locals.length - offset - 1] << [@path, dummy_def_range]
|
403
|
+
end
|
404
|
+
|
405
|
+
@insns.each do |insn|
|
406
|
+
next unless insn.insn == :getlocal || insn.insn == :setlocal
|
407
|
+
|
408
|
+
idx = insn.operands[0]
|
409
|
+
# note: level is relative value to the current level
|
410
|
+
level = insn.operands[1]
|
411
|
+
target_abs_level = absolute_level - level
|
412
|
+
variable_tables[target_abs_level] ||= {}
|
413
|
+
variable_tables[target_abs_level][idx] ||= Utils::MutableSet.new
|
414
|
+
|
415
|
+
case insn.insn
|
416
|
+
when :setlocal
|
417
|
+
variable_tables[target_abs_level][idx] << [path, insn.code_range]
|
418
|
+
when :getlocal
|
419
|
+
file_info.definition_table[insn.code_range] = variable_tables[target_abs_level][idx]
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
@insns.each do |insn|
|
424
|
+
insn.operands.each do |operand|
|
425
|
+
next unless operand.is_a?(ISeq)
|
426
|
+
operand.collect_local_variable_info(
|
427
|
+
file_info, absolute_level + 1,
|
428
|
+
variable_tables
|
429
|
+
)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
232
434
|
def rename_insn_types
|
233
435
|
@insns.each do |insn|
|
234
436
|
case insn.insn
|
@@ -331,7 +533,7 @@ module TypeProf
|
|
331
533
|
break unless @insns[j + 1].check?(:putobject, [true])
|
332
534
|
break unless @insns[j + 2].check?(:getconstant) # TODO: support A::B::C
|
333
535
|
break unless @insns[j + 3].check?(:topn, [1])
|
334
|
-
break unless @insns[j + 4].check?(:send,
|
536
|
+
break unless @insns[j + 4].check?(:send) && @insns[j + 4].operands[0].slice(:mid, :flag, :orig_argc) == {:mid=>:===, :flag=>20, :orig_argc=>1}
|
335
537
|
break unless @insns[j + 5].check?(:branch)
|
336
538
|
target_pc = @insns[j + 5].operands[1]
|
337
539
|
break unless @insns[target_pc].check?(:pop, [])
|