debug 1.0.0.beta4 → 1.0.0.beta8
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 +4 -4
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +1 -0
- data/CONTRIBUTING.md +239 -0
- data/Gemfile +2 -1
- data/README.md +424 -215
- data/Rakefile +2 -1
- data/TODO.md +0 -6
- data/bin/gentest +22 -0
- data/debug.gemspec +2 -0
- data/exe/rdbg +14 -11
- data/ext/debug/debug.c +9 -8
- data/lib/debug.rb +3 -0
- data/lib/debug/breakpoint.rb +101 -43
- data/lib/debug/client.rb +55 -13
- data/lib/debug/color.rb +76 -0
- data/lib/debug/config.rb +130 -39
- data/lib/debug/console.rb +24 -3
- data/lib/debug/frame_info.rb +63 -31
- data/lib/debug/open.rb +4 -1
- data/lib/debug/open_nonstop.rb +15 -0
- data/lib/debug/server.rb +90 -32
- data/lib/debug/server_dap.rb +607 -0
- data/lib/debug/session.rb +461 -174
- data/lib/debug/source_repository.rb +55 -33
- data/lib/debug/start.rb +5 -0
- data/lib/debug/thread_client.rb +176 -59
- data/lib/debug/version.rb +3 -1
- data/misc/README.md.erb +351 -204
- metadata +23 -4
- data/lib/debug/run.rb +0 -2
data/lib/debug/console.rb
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console/size'
|
|
2
4
|
|
|
3
5
|
module DEBUGGER__
|
|
4
|
-
class UI_Console
|
|
6
|
+
class UI_Console < UI_Base
|
|
5
7
|
def initialize
|
|
8
|
+
unless CONFIG[:no_sigint_hook]
|
|
9
|
+
@prev_handler = trap(:SIGINT){
|
|
10
|
+
ThreadClient.current.on_trap :SIGINT
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def close
|
|
16
|
+
if @prev_handler
|
|
17
|
+
trap(:SIGINT, @prev_handler)
|
|
18
|
+
end
|
|
6
19
|
end
|
|
7
20
|
|
|
8
21
|
def remote?
|
|
9
22
|
false
|
|
10
23
|
end
|
|
11
24
|
|
|
25
|
+
def width
|
|
26
|
+
if (w = IO.console_size[1]) == 0 # for tests PTY
|
|
27
|
+
80
|
|
28
|
+
else
|
|
29
|
+
w
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
12
33
|
def quit n
|
|
13
34
|
exit n
|
|
14
35
|
end
|
|
@@ -16,7 +37,7 @@ module DEBUGGER__
|
|
|
16
37
|
def ask prompt
|
|
17
38
|
setup_interrupt do
|
|
18
39
|
print prompt
|
|
19
|
-
(gets || '').strip
|
|
40
|
+
($stdin.gets || '').strip
|
|
20
41
|
end
|
|
21
42
|
end
|
|
22
43
|
|
data/lib/debug/frame_info.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
|
|
2
3
|
module DEBUGGER__
|
|
3
4
|
FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :frame_depth,
|
|
4
|
-
:has_return_value, :return_value,
|
|
5
|
-
|
|
5
|
+
:has_return_value, :return_value,
|
|
6
|
+
:has_raised_exception, :raised_exception,
|
|
7
|
+
:show_line)
|
|
6
8
|
|
|
7
9
|
# extend FrameInfo with debug.so
|
|
8
10
|
if File.exist? File.join(__dir__, 'debug.so')
|
|
9
11
|
require_relative 'debug.so'
|
|
10
12
|
else
|
|
11
|
-
|
|
13
|
+
require_relative 'debug'
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
class FrameInfo
|
|
@@ -39,33 +41,73 @@ module DEBUGGER__
|
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def name
|
|
45
|
+
# p frame_type: frame_type, self: self
|
|
46
|
+
case frame_type
|
|
47
|
+
when :block
|
|
48
|
+
level, block_loc, _args = block_identifier
|
|
49
|
+
"block in #{block_loc}#{level}"
|
|
50
|
+
when :method
|
|
51
|
+
ci, _args = method_identifier
|
|
52
|
+
"#{ci}"
|
|
53
|
+
when :c
|
|
54
|
+
c_identifier
|
|
55
|
+
when :other
|
|
56
|
+
other_identifier
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
42
60
|
def file_lines
|
|
43
|
-
SESSION.source(
|
|
61
|
+
SESSION.source(self.iseq)
|
|
44
62
|
end
|
|
45
63
|
|
|
46
|
-
def
|
|
64
|
+
def frame_type
|
|
47
65
|
if binding && iseq
|
|
48
66
|
if iseq.type == :block
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
location.label.sub('block'){ "block#{args_str}" }
|
|
55
|
-
elsif (callee = binding.eval('__callee__', __FILE__, __LINE__)) && (argc = iseq.argc) > 0
|
|
56
|
-
args = parameters_info iseq.locals[0...argc]
|
|
57
|
-
"#{klass_sig}#{callee}(#{args})"
|
|
67
|
+
:block
|
|
68
|
+
elsif callee
|
|
69
|
+
:method
|
|
58
70
|
else
|
|
59
|
-
|
|
71
|
+
:other
|
|
60
72
|
end
|
|
61
73
|
else
|
|
62
|
-
|
|
74
|
+
:c
|
|
63
75
|
end
|
|
64
76
|
end
|
|
65
77
|
|
|
78
|
+
BLOCK_LABL_REGEXP = /\Ablock( \(\d+ levels\))* in (.+)\z/
|
|
79
|
+
|
|
80
|
+
def block_identifier
|
|
81
|
+
return unless frame_type == :block
|
|
82
|
+
args = parameters_info(iseq.argc)
|
|
83
|
+
_, level, block_loc = location.label.match(BLOCK_LABL_REGEXP).to_a
|
|
84
|
+
[level || "", block_loc, args]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def method_identifier
|
|
88
|
+
return unless frame_type == :method
|
|
89
|
+
args = parameters_info(iseq.argc)
|
|
90
|
+
ci = "#{klass_sig}#{callee}"
|
|
91
|
+
[ci, args]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def c_identifier
|
|
95
|
+
return unless frame_type == :c
|
|
96
|
+
"[C] #{klass_sig}#{location.base_label}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def other_identifier
|
|
100
|
+
return unless frame_type == :other
|
|
101
|
+
location.label
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def callee
|
|
105
|
+
@callee ||= binding&.eval('__callee__', __FILE__, __LINE__)
|
|
106
|
+
end
|
|
107
|
+
|
|
66
108
|
def return_str
|
|
67
109
|
if binding && iseq && has_return_value
|
|
68
|
-
short_inspect(return_value)
|
|
110
|
+
DEBUGGER__.short_inspect(return_value)
|
|
69
111
|
end
|
|
70
112
|
end
|
|
71
113
|
|
|
@@ -75,31 +117,21 @@ module DEBUGGER__
|
|
|
75
117
|
|
|
76
118
|
private
|
|
77
119
|
|
|
78
|
-
SHORT_INSPECT_LENGTH = 40
|
|
79
|
-
|
|
80
|
-
def short_inspect obj
|
|
81
|
-
str = obj.inspect
|
|
82
|
-
if str.length > SHORT_INSPECT_LENGTH
|
|
83
|
-
str[0...SHORT_INSPECT_LENGTH] + '...'
|
|
84
|
-
else
|
|
85
|
-
str
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
120
|
def get_singleton_class obj
|
|
90
121
|
obj.singleton_class # TODO: don't use it
|
|
91
122
|
rescue TypeError
|
|
92
123
|
nil
|
|
93
124
|
end
|
|
94
125
|
|
|
95
|
-
def parameters_info
|
|
126
|
+
def parameters_info(argc)
|
|
127
|
+
vars = iseq.locals[0...argc]
|
|
96
128
|
vars.map{|var|
|
|
97
129
|
begin
|
|
98
|
-
|
|
130
|
+
{ name: var, value: DEBUGGER__.short_inspect(binding.local_variable_get(var)) }
|
|
99
131
|
rescue NameError, TypeError
|
|
100
132
|
nil
|
|
101
133
|
end
|
|
102
|
-
}.compact
|
|
134
|
+
}.compact
|
|
103
135
|
end
|
|
104
136
|
|
|
105
137
|
def klass_sig
|
data/lib/debug/open.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
#
|
|
2
3
|
# Open the door for the debugger to connect.
|
|
3
4
|
# Users can connect to debuggee program with "rdbg --attach" option.
|
|
@@ -6,5 +7,7 @@
|
|
|
6
7
|
# Otherwise, UNIX domain socket is used.
|
|
7
8
|
#
|
|
8
9
|
|
|
9
|
-
require_relative '
|
|
10
|
+
require_relative 'session'
|
|
11
|
+
return unless defined?(DEBUGGER__)
|
|
12
|
+
|
|
10
13
|
DEBUGGER__.open
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Open the door for the debugger to connect.
|
|
4
|
+
# Unlike debug/open, it does not stop at the beginning of the program.
|
|
5
|
+
# Users can connect to debuggee program with "rdbg --attach" option or
|
|
6
|
+
# VSCode attach type.
|
|
7
|
+
#
|
|
8
|
+
# If RUBY_DEBUG_PORT envval is provided (digits), open TCP/IP port.
|
|
9
|
+
# Otherwise, UNIX domain socket is used.
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
require_relative 'session'
|
|
13
|
+
return unless defined?(DEBUGGER__)
|
|
14
|
+
|
|
15
|
+
DEBUGGER__.open(nonstop: true)
|
data/lib/debug/server.rb
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'socket'
|
|
2
|
-
require_relative 'session'
|
|
3
4
|
require_relative 'config'
|
|
5
|
+
require_relative 'version'
|
|
4
6
|
|
|
5
7
|
module DEBUGGER__
|
|
6
|
-
class UI_ServerBase
|
|
8
|
+
class UI_ServerBase < UI_Base
|
|
7
9
|
def initialize
|
|
8
10
|
@sock = nil
|
|
9
11
|
@accept_m = Mutex.new
|
|
10
12
|
@accept_cv = ConditionVariable.new
|
|
11
13
|
@client_addr = nil
|
|
12
|
-
@q_msg =
|
|
13
|
-
@q_ans =
|
|
14
|
+
@q_msg = nil
|
|
15
|
+
@q_ans = nil
|
|
14
16
|
@unsent_messages = []
|
|
17
|
+
@width = 80
|
|
15
18
|
|
|
16
19
|
@reader_thread = Thread.new do
|
|
20
|
+
# An error on this thread should break the system.
|
|
21
|
+
Thread.current.abort_on_exception = true
|
|
22
|
+
|
|
17
23
|
accept do |server|
|
|
18
|
-
DEBUGGER__.
|
|
24
|
+
DEBUGGER__.warn "Connected."
|
|
19
25
|
|
|
20
26
|
@accept_m.synchronize{
|
|
21
27
|
@sock = server
|
|
28
|
+
greeting
|
|
29
|
+
|
|
22
30
|
@accept_cv.signal
|
|
23
31
|
|
|
24
32
|
# flush unsent messages
|
|
@@ -26,32 +34,71 @@ module DEBUGGER__
|
|
|
26
34
|
@sock.puts m
|
|
27
35
|
}
|
|
28
36
|
@unsent_messages.clear
|
|
37
|
+
|
|
38
|
+
@q_msg = Queue.new
|
|
39
|
+
@q_ans = Queue.new
|
|
29
40
|
}
|
|
30
|
-
@q_msg = Queue.new
|
|
31
|
-
@q_ans = Queue.new
|
|
32
41
|
|
|
33
42
|
setup_interrupt do
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
while line = @sock.gets
|
|
37
|
-
case line
|
|
38
|
-
when /\Apause/
|
|
39
|
-
pause
|
|
40
|
-
when /\Acommand ?(.+)/
|
|
41
|
-
@q_msg << $1
|
|
42
|
-
when /\Aanswer (.*)/
|
|
43
|
-
@q_ans << $1
|
|
44
|
-
else
|
|
45
|
-
STDERR.puts "unsupported: #{line}"
|
|
46
|
-
exit!
|
|
47
|
-
end
|
|
48
|
-
end
|
|
43
|
+
process
|
|
49
44
|
end
|
|
45
|
+
|
|
46
|
+
rescue => e
|
|
47
|
+
DEBUGGER__.warn "ReaderThreadError: #{e}", :error
|
|
50
48
|
ensure
|
|
51
|
-
DEBUGGER__.
|
|
49
|
+
DEBUGGER__.warn "Disconnected."
|
|
52
50
|
@sock = nil
|
|
53
51
|
@q_msg.close
|
|
52
|
+
@q_msg = nil
|
|
54
53
|
@q_ans.close
|
|
54
|
+
@q_ans = nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def greeting
|
|
60
|
+
case g = @sock.gets
|
|
61
|
+
when /^version:\s+(.+)\s+width: (\d+) cookie:\s+(.*)$/
|
|
62
|
+
v, w, c = $1, $2, $3
|
|
63
|
+
# TODO: protocol version
|
|
64
|
+
if v != VERSION
|
|
65
|
+
raise "Incompatible version (#{VERSION} client:#{$1})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
cookie = CONFIG[:cookie]
|
|
69
|
+
if cookie && cookie != c
|
|
70
|
+
raise "Cookie mismatch (#{$2.inspect} was sent)"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@width = w.to_i
|
|
74
|
+
|
|
75
|
+
when /^Content-Length: (\d+)/
|
|
76
|
+
require_relative 'server_dap'
|
|
77
|
+
|
|
78
|
+
raise unless @sock.read(2) == "\r\n"
|
|
79
|
+
self.extend(UI_DAP)
|
|
80
|
+
dap_setup @sock.read($1.to_i)
|
|
81
|
+
else
|
|
82
|
+
raise "Greeting message error: #{g}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def process
|
|
87
|
+
pause
|
|
88
|
+
|
|
89
|
+
while line = @sock.gets
|
|
90
|
+
case line
|
|
91
|
+
when /\Apause/
|
|
92
|
+
pause
|
|
93
|
+
when /\Acommand ?(.+)/
|
|
94
|
+
@q_msg << $1
|
|
95
|
+
when /\Aanswer (.*)/
|
|
96
|
+
@q_ans << $1
|
|
97
|
+
when /\Awidth (.+)/
|
|
98
|
+
@width = $1.to_i
|
|
99
|
+
else
|
|
100
|
+
STDERR.puts "unsupported: #{line}"
|
|
101
|
+
exit!
|
|
55
102
|
end
|
|
56
103
|
end
|
|
57
104
|
end
|
|
@@ -60,6 +107,10 @@ module DEBUGGER__
|
|
|
60
107
|
true
|
|
61
108
|
end
|
|
62
109
|
|
|
110
|
+
def width
|
|
111
|
+
@width
|
|
112
|
+
end
|
|
113
|
+
|
|
63
114
|
def setup_interrupt
|
|
64
115
|
prev_handler = trap(:SIGINT) do
|
|
65
116
|
# $stderr.puts "trapped SIGINT"
|
|
@@ -90,12 +141,20 @@ module DEBUGGER__
|
|
|
90
141
|
if s = @sock # already connection
|
|
91
142
|
# ok
|
|
92
143
|
elsif skip == true # skip process
|
|
93
|
-
|
|
144
|
+
no_sock = true
|
|
145
|
+
r = @accept_m.synchronize do
|
|
146
|
+
if @sock
|
|
147
|
+
no_sock = false
|
|
148
|
+
else
|
|
149
|
+
yield nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
return r if no_sock
|
|
94
153
|
else # wait for connection
|
|
95
154
|
until s = @sock
|
|
96
155
|
@accept_m.synchronize{
|
|
97
156
|
unless @sock
|
|
98
|
-
DEBUGGER__.
|
|
157
|
+
DEBUGGER__.warn "wait for debuger connection..."
|
|
99
158
|
@accept_cv.wait(@accept_m)
|
|
100
159
|
end
|
|
101
160
|
}
|
|
@@ -139,6 +198,7 @@ module DEBUGGER__
|
|
|
139
198
|
def readline
|
|
140
199
|
(sock do |s|
|
|
141
200
|
s.puts "input"
|
|
201
|
+
sleep 0.01 until @q_msg
|
|
142
202
|
@q_msg.pop
|
|
143
203
|
end || 'continue').strip
|
|
144
204
|
end
|
|
@@ -158,7 +218,7 @@ module DEBUGGER__
|
|
|
158
218
|
|
|
159
219
|
class UI_TcpServer < UI_ServerBase
|
|
160
220
|
def initialize host: nil, port: nil
|
|
161
|
-
@host = host || ::DEBUGGER__::CONFIG[:host] || '
|
|
221
|
+
@host = host || ::DEBUGGER__::CONFIG[:host] || '127.0.0.1'
|
|
162
222
|
@port = port || begin
|
|
163
223
|
port_str = ::DEBUGGER__::CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
|
|
164
224
|
if /\A\d+\z/ !~ port_str
|
|
@@ -173,7 +233,7 @@ module DEBUGGER__
|
|
|
173
233
|
|
|
174
234
|
def accept
|
|
175
235
|
Socket.tcp_server_sockets @host, @port do |socks|
|
|
176
|
-
::DEBUGGER__.
|
|
236
|
+
::DEBUGGER__.warn "Debugger can attach via TCP/IP (#{socks.map{|e| e.local_address.inspect}})"
|
|
177
237
|
Socket.accept_loop(socks) do |sock, client|
|
|
178
238
|
@client_addr = client
|
|
179
239
|
yield sock
|
|
@@ -203,15 +263,13 @@ module DEBUGGER__
|
|
|
203
263
|
@sock_path = DEBUGGER__.create_unix_domain_socket_name(@sock_dir)
|
|
204
264
|
end
|
|
205
265
|
|
|
206
|
-
::DEBUGGER__.
|
|
266
|
+
::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
|
|
207
267
|
Socket.unix_server_loop @sock_path do |sock, client|
|
|
208
268
|
@client_addr = client
|
|
209
269
|
yield sock
|
|
270
|
+
ensure
|
|
271
|
+
sock.close
|
|
210
272
|
end
|
|
211
273
|
end
|
|
212
274
|
end
|
|
213
|
-
|
|
214
|
-
def self.message msg
|
|
215
|
-
$stderr.puts "DEBUGGER: #{msg}"
|
|
216
|
-
end
|
|
217
275
|
end
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module DEBUGGER__
|
|
6
|
+
module UI_DAP
|
|
7
|
+
SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
|
|
8
|
+
|
|
9
|
+
def dap_setup bytes
|
|
10
|
+
DEBUGGER__.set_config(no_color: true)
|
|
11
|
+
@seq = 0
|
|
12
|
+
|
|
13
|
+
$stderr.puts '[>]' + bytes if SHOW_PROTOCOL
|
|
14
|
+
req = JSON.load(bytes)
|
|
15
|
+
|
|
16
|
+
# capability
|
|
17
|
+
send_response(req,
|
|
18
|
+
## Supported
|
|
19
|
+
supportsConfigurationDoneRequest: true,
|
|
20
|
+
supportsFunctionBreakpoints: true,
|
|
21
|
+
supportsConditionalBreakpoints: true,
|
|
22
|
+
supportTerminateDebuggee: true,
|
|
23
|
+
supportsTerminateRequest: true,
|
|
24
|
+
exceptionBreakpointFilters: [
|
|
25
|
+
{
|
|
26
|
+
filter: 'any',
|
|
27
|
+
label: 'rescue any exception',
|
|
28
|
+
#supportsCondition: true,
|
|
29
|
+
#conditionDescription: '',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
filter: 'RuntimeError',
|
|
33
|
+
label: 'rescue RuntimeError',
|
|
34
|
+
default: true,
|
|
35
|
+
#supportsCondition: true,
|
|
36
|
+
#conditionDescription: '',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
supportsExceptionFilterOptions: true,
|
|
40
|
+
|
|
41
|
+
## Will be supported
|
|
42
|
+
# supportsExceptionOptions: true,
|
|
43
|
+
# supportsHitConditionalBreakpoints:
|
|
44
|
+
# supportsEvaluateForHovers:
|
|
45
|
+
# supportsSetVariable: true,
|
|
46
|
+
# supportSuspendDebuggee:
|
|
47
|
+
# supportsLogPoints:
|
|
48
|
+
# supportsLoadedSourcesRequest:
|
|
49
|
+
# supportsDataBreakpoints:
|
|
50
|
+
# supportsBreakpointLocationsRequest:
|
|
51
|
+
|
|
52
|
+
## Possible?
|
|
53
|
+
# supportsStepBack:
|
|
54
|
+
# supportsRestartFrame:
|
|
55
|
+
# supportsCompletionsRequest:
|
|
56
|
+
# completionTriggerCharacters:
|
|
57
|
+
# supportsModulesRequest:
|
|
58
|
+
# additionalModuleColumns:
|
|
59
|
+
# supportedChecksumAlgorithms:
|
|
60
|
+
# supportsRestartRequest:
|
|
61
|
+
# supportsValueFormattingOptions:
|
|
62
|
+
# supportsExceptionInfoRequest:
|
|
63
|
+
# supportsDelayedStackTraceLoading:
|
|
64
|
+
# supportsTerminateThreadsRequest:
|
|
65
|
+
# supportsSetExpression:
|
|
66
|
+
# supportsClipboardContext:
|
|
67
|
+
|
|
68
|
+
## Never
|
|
69
|
+
# supportsGotoTargetsRequest:
|
|
70
|
+
# supportsStepInTargetsRequest:
|
|
71
|
+
# supportsReadMemoryRequest:
|
|
72
|
+
# supportsDisassembleRequest:
|
|
73
|
+
# supportsCancelRequest:
|
|
74
|
+
# supportsSteppingGranularity:
|
|
75
|
+
# supportsInstructionBreakpoints:
|
|
76
|
+
)
|
|
77
|
+
send_event 'initialized'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def send **kw
|
|
81
|
+
kw[:seq] = @seq += 1
|
|
82
|
+
str = JSON.dump(kw)
|
|
83
|
+
$stderr.puts "[<] #{str}" if SHOW_PROTOCOL
|
|
84
|
+
# STDERR.puts "[STDERR] [<] #{str}"
|
|
85
|
+
@sock.print header = "Content-Length: #{str.size}\r\n\r\n"
|
|
86
|
+
@sock.write str
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_response req, success: true, **kw
|
|
90
|
+
if kw.empty?
|
|
91
|
+
send type: 'response',
|
|
92
|
+
command: req['command'],
|
|
93
|
+
request_seq: req['seq'],
|
|
94
|
+
success: success,
|
|
95
|
+
message: success ? 'Success' : 'Failed'
|
|
96
|
+
else
|
|
97
|
+
send type: 'response',
|
|
98
|
+
command: req['command'],
|
|
99
|
+
request_seq: req['seq'],
|
|
100
|
+
success: success,
|
|
101
|
+
message: success ? 'Success' : 'Failed',
|
|
102
|
+
body: kw
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def send_event name, **kw
|
|
107
|
+
if kw.empty?
|
|
108
|
+
send type: 'event', event: name
|
|
109
|
+
else
|
|
110
|
+
send type: 'event', event: name, body: kw
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def recv_request
|
|
115
|
+
case header = @sock.gets
|
|
116
|
+
when /Content-Length: (\d+)/
|
|
117
|
+
b = @sock.read(2)
|
|
118
|
+
raise b.inspect unless b == "\r\n"
|
|
119
|
+
|
|
120
|
+
l = @sock.read(s = $1.to_i)
|
|
121
|
+
$stderr.puts "[>] #{l}" if SHOW_PROTOCOL
|
|
122
|
+
JSON.load(l)
|
|
123
|
+
when nil
|
|
124
|
+
nil
|
|
125
|
+
else
|
|
126
|
+
raise "unrecognized line: #{l} (#{l.size} bytes)"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def process
|
|
131
|
+
while req = recv_request
|
|
132
|
+
raise "not a request: #{req.inpsect}" unless req['type'] == 'request'
|
|
133
|
+
args = req.dig('arguments')
|
|
134
|
+
|
|
135
|
+
case req['command']
|
|
136
|
+
|
|
137
|
+
## boot/configuration
|
|
138
|
+
when 'launch'
|
|
139
|
+
send_response req
|
|
140
|
+
when 'setBreakpoints'
|
|
141
|
+
path = args.dig('source', 'path')
|
|
142
|
+
bp_args = args['breakpoints']
|
|
143
|
+
bps = []
|
|
144
|
+
bp_args.each{|bp|
|
|
145
|
+
line = bp['line']
|
|
146
|
+
if cond = bp['condition']
|
|
147
|
+
bps << SESSION.add_line_breakpoint(path, line, cond: cond)
|
|
148
|
+
else
|
|
149
|
+
bps << SESSION.add_line_breakpoint(path, line)
|
|
150
|
+
end
|
|
151
|
+
}
|
|
152
|
+
send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
|
|
153
|
+
when 'setFunctionBreakpoints'
|
|
154
|
+
send_response req
|
|
155
|
+
when 'setExceptionBreakpoints'
|
|
156
|
+
filters = args.dig('filterOptions').map{|bp_info|
|
|
157
|
+
case bp_info.dig('filterId')
|
|
158
|
+
when 'any'
|
|
159
|
+
bp = SESSION.add_catch_breakpoint 'Exception'
|
|
160
|
+
when 'RuntimeError'
|
|
161
|
+
bp = SESSION.add_catch_breakpoint 'RuntimeError'
|
|
162
|
+
else
|
|
163
|
+
bp = nil
|
|
164
|
+
end
|
|
165
|
+
{
|
|
166
|
+
verifiled: bp ? true : false,
|
|
167
|
+
message: bp.inspect,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
send_response req, breakpoints: filters
|
|
171
|
+
when 'configurationDone'
|
|
172
|
+
send_response req
|
|
173
|
+
@q_msg << 'continue'
|
|
174
|
+
when 'attach'
|
|
175
|
+
send_response req
|
|
176
|
+
Process.kill(:SIGINT, Process.pid)
|
|
177
|
+
when 'disconnect'
|
|
178
|
+
send_response req
|
|
179
|
+
@q_msg << 'continue'
|
|
180
|
+
|
|
181
|
+
## control
|
|
182
|
+
when 'continue'
|
|
183
|
+
@q_msg << 'c'
|
|
184
|
+
send_response req, allThreadsContinued: true
|
|
185
|
+
when 'next'
|
|
186
|
+
@q_msg << 'n'
|
|
187
|
+
send_response req
|
|
188
|
+
when 'stepIn'
|
|
189
|
+
@q_msg << 's'
|
|
190
|
+
send_response req
|
|
191
|
+
when 'stepOut'
|
|
192
|
+
@q_msg << 'fin'
|
|
193
|
+
send_response req
|
|
194
|
+
when 'terminate'
|
|
195
|
+
send_response req
|
|
196
|
+
exit
|
|
197
|
+
when 'pause'
|
|
198
|
+
send_response req
|
|
199
|
+
Process.kill(:SIGINT, Process.pid)
|
|
200
|
+
|
|
201
|
+
## query
|
|
202
|
+
when 'threads'
|
|
203
|
+
send_response req, threads: SESSION.managed_thread_clients.map{|tc|
|
|
204
|
+
{ id: tc.id,
|
|
205
|
+
name: tc.name,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
when 'stackTrace',
|
|
210
|
+
'scopes',
|
|
211
|
+
'variables',
|
|
212
|
+
'evaluate',
|
|
213
|
+
'source'
|
|
214
|
+
@q_msg << req
|
|
215
|
+
else
|
|
216
|
+
raise "Unknown request: #{req.inspect}"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
## called by the SESSION thread
|
|
222
|
+
|
|
223
|
+
def readline
|
|
224
|
+
@q_msg.pop || 'kill!'
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def sock skip: false
|
|
228
|
+
yield $stderr
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def respond req, res
|
|
232
|
+
send_response(req, **res)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def puts result
|
|
236
|
+
# STDERR.puts "puts: #{result}"
|
|
237
|
+
# send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def event type, *args
|
|
241
|
+
case type
|
|
242
|
+
when :suspend_bp
|
|
243
|
+
_i, bp = *args
|
|
244
|
+
if bp.kind_of?(CatchBreakpoint)
|
|
245
|
+
reason = 'exception'
|
|
246
|
+
text = bp.description
|
|
247
|
+
else
|
|
248
|
+
reason = 'breakpoint'
|
|
249
|
+
text = bp ? bp.description : 'temporary bp'
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
send_event 'stopped', reason: reason,
|
|
253
|
+
description: text,
|
|
254
|
+
text: text,
|
|
255
|
+
threadId: 1,
|
|
256
|
+
allThreadsStopped: true
|
|
257
|
+
when :suspend_trap
|
|
258
|
+
send_event 'stopped', reason: 'pause',
|
|
259
|
+
threadId: 1,
|
|
260
|
+
allThreadsStopped: true
|
|
261
|
+
when :suspended
|
|
262
|
+
send_event 'stopped', reason: 'step',
|
|
263
|
+
threadId: 1,
|
|
264
|
+
allThreadsStopped: true
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
class Session
|
|
270
|
+
def find_tc id
|
|
271
|
+
@th_clients.each{|th, tc|
|
|
272
|
+
return tc if tc.id == id
|
|
273
|
+
}
|
|
274
|
+
return nil
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def fail_response req, **kw
|
|
278
|
+
@ui.respond req, success: false, **kw
|
|
279
|
+
return :retry
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def process_dap_request req
|
|
283
|
+
case req['command']
|
|
284
|
+
when 'stackTrace'
|
|
285
|
+
tid = req.dig('arguments', 'threadId')
|
|
286
|
+
if tc = find_tc(tid)
|
|
287
|
+
tc << [:dap, :backtrace, req]
|
|
288
|
+
else
|
|
289
|
+
fail_response req
|
|
290
|
+
end
|
|
291
|
+
when 'scopes'
|
|
292
|
+
frame_id = req.dig('arguments', 'frameId')
|
|
293
|
+
if @frame_map[frame_id]
|
|
294
|
+
tid, fid = @frame_map[frame_id]
|
|
295
|
+
if tc = find_tc(tid)
|
|
296
|
+
tc << [:dap, :scopes, req, fid]
|
|
297
|
+
else
|
|
298
|
+
fail_response req
|
|
299
|
+
end
|
|
300
|
+
else
|
|
301
|
+
fail_response req
|
|
302
|
+
end
|
|
303
|
+
when 'variables'
|
|
304
|
+
varid = req.dig('arguments', 'variablesReference')
|
|
305
|
+
if ref = @var_map[varid]
|
|
306
|
+
case ref[0]
|
|
307
|
+
when :globals
|
|
308
|
+
vars = global_variables.map do |name|
|
|
309
|
+
File.write('/tmp/x', "#{name}\n")
|
|
310
|
+
gv = 'Not implemented yet...'
|
|
311
|
+
{
|
|
312
|
+
name: name,
|
|
313
|
+
value: gv.inspect,
|
|
314
|
+
type: (gv.class.name || gv.class.to_s),
|
|
315
|
+
variablesReference: 0,
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
@ui.respond req, {
|
|
320
|
+
variables: vars,
|
|
321
|
+
}
|
|
322
|
+
return :retry
|
|
323
|
+
|
|
324
|
+
when :scope
|
|
325
|
+
frame_id = ref[1]
|
|
326
|
+
tid, fid = @frame_map[frame_id]
|
|
327
|
+
|
|
328
|
+
if tc = find_tc(tid)
|
|
329
|
+
tc << [:dap, :scope, req, fid]
|
|
330
|
+
else
|
|
331
|
+
fail_response req
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
when :variable
|
|
335
|
+
tid, vid = ref[1], ref[2]
|
|
336
|
+
|
|
337
|
+
if tc = find_tc(tid)
|
|
338
|
+
tc << [:dap, :variable, req, vid]
|
|
339
|
+
else
|
|
340
|
+
fail_response req
|
|
341
|
+
end
|
|
342
|
+
else
|
|
343
|
+
raise "Uknown type: #{ref.inspect}"
|
|
344
|
+
end
|
|
345
|
+
else
|
|
346
|
+
fail_response req
|
|
347
|
+
end
|
|
348
|
+
when 'evaluate'
|
|
349
|
+
frame_id = req.dig('arguments', 'frameId')
|
|
350
|
+
if @frame_map[frame_id]
|
|
351
|
+
tid, fid = @frame_map[frame_id]
|
|
352
|
+
expr = req.dig('arguments', 'expression')
|
|
353
|
+
if tc = find_tc(tid)
|
|
354
|
+
tc << [:dap, :evaluate, req, fid, expr]
|
|
355
|
+
else
|
|
356
|
+
fail_response req
|
|
357
|
+
end
|
|
358
|
+
else
|
|
359
|
+
fail_response req, result: "can't evaluate"
|
|
360
|
+
end
|
|
361
|
+
when 'source'
|
|
362
|
+
ref = req.dig('arguments', 'sourceReference')
|
|
363
|
+
if src = @src_map[ref]
|
|
364
|
+
@ui.respond req, content: src.join
|
|
365
|
+
else
|
|
366
|
+
fail_response req, message: 'not found...'
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
return :retry
|
|
370
|
+
else
|
|
371
|
+
raise "Unknown DAP request: #{req.inspect}"
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def dap_event args
|
|
376
|
+
# puts({dap_event: args}.inspect)
|
|
377
|
+
type, req, result = args
|
|
378
|
+
|
|
379
|
+
case type
|
|
380
|
+
when :backtrace
|
|
381
|
+
result[:stackFrames].each.with_index{|fi, i|
|
|
382
|
+
fi[:id] = id = @frame_map.size + 1
|
|
383
|
+
@frame_map[id] = [req.dig('arguments', 'threadId'), i]
|
|
384
|
+
if fi[:source] && src = fi[:source][:sourceReference]
|
|
385
|
+
src_id = @src_map.size + 1
|
|
386
|
+
@src_map[src_id] = src
|
|
387
|
+
fi[:source][:sourceReference] = src_id
|
|
388
|
+
end
|
|
389
|
+
}
|
|
390
|
+
@ui.respond req, result
|
|
391
|
+
when :scopes
|
|
392
|
+
frame_id = req.dig('arguments', 'frameId')
|
|
393
|
+
local_scope = result[:scopes].first
|
|
394
|
+
local_scope[:variablesReference] = id = @var_map.size + 1
|
|
395
|
+
|
|
396
|
+
@var_map[id] = [:scope, frame_id]
|
|
397
|
+
@ui.respond req, result
|
|
398
|
+
when :scope
|
|
399
|
+
tid = result.delete :tid
|
|
400
|
+
register_vars result[:variables], tid
|
|
401
|
+
@ui.respond req, result
|
|
402
|
+
when :variable
|
|
403
|
+
tid = result.delete :tid
|
|
404
|
+
register_vars result[:variables], tid
|
|
405
|
+
@ui.respond req, result
|
|
406
|
+
when :evaluate
|
|
407
|
+
tid = result.delete :tid
|
|
408
|
+
register_var result, tid
|
|
409
|
+
@ui.respond req, result
|
|
410
|
+
else
|
|
411
|
+
raise "unsupported: #{args.inspect}"
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def register_var v, tid
|
|
416
|
+
if (tl_vid = v[:variablesReference]) > 0
|
|
417
|
+
vid = @var_map.size + 1
|
|
418
|
+
@var_map[vid] = [:variable, tid, tl_vid]
|
|
419
|
+
v[:variablesReference] = vid
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def register_vars vars, tid
|
|
424
|
+
raise tid.inspect unless tid.kind_of?(Integer)
|
|
425
|
+
vars.each{|v|
|
|
426
|
+
register_var v, tid
|
|
427
|
+
}
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
class ThreadClient
|
|
432
|
+
def process_dap args
|
|
433
|
+
# pp tc: self, args: args
|
|
434
|
+
type = args.shift
|
|
435
|
+
req = args.shift
|
|
436
|
+
|
|
437
|
+
case type
|
|
438
|
+
when :backtrace
|
|
439
|
+
event! :dap_result, :backtrace, req, {
|
|
440
|
+
stackFrames: @target_frames.map.with_index{|frame, i|
|
|
441
|
+
path = frame.realpath
|
|
442
|
+
ref = frame.file_lines unless File.exist?(path)
|
|
443
|
+
|
|
444
|
+
{
|
|
445
|
+
# id: ??? # filled by SESSION
|
|
446
|
+
name: frame.name,
|
|
447
|
+
line: frame.location.lineno,
|
|
448
|
+
column: 1,
|
|
449
|
+
source: {
|
|
450
|
+
name: File.basename(frame.path),
|
|
451
|
+
path: path,
|
|
452
|
+
sourceReference: ref,
|
|
453
|
+
},
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
when :scopes
|
|
458
|
+
fid = args.shift
|
|
459
|
+
frame = @target_frames[fid]
|
|
460
|
+
lnum = frame.binding ? frame.binding.local_variables.size : 0
|
|
461
|
+
|
|
462
|
+
event! :dap_result, :scopes, req, scopes: [{
|
|
463
|
+
name: 'Local variables',
|
|
464
|
+
presentationHint: 'locals',
|
|
465
|
+
# variablesReference: N, # filled by SESSION
|
|
466
|
+
namedVariables: lnum,
|
|
467
|
+
indexedVariables: 0,
|
|
468
|
+
expensive: false,
|
|
469
|
+
}, {
|
|
470
|
+
name: 'Global variables',
|
|
471
|
+
presentationHint: 'globals',
|
|
472
|
+
variablesReference: 1, # GLOBAL
|
|
473
|
+
namedVariables: global_variables.size,
|
|
474
|
+
indexedVariables: 0,
|
|
475
|
+
expensive: false,
|
|
476
|
+
}]
|
|
477
|
+
when :scope
|
|
478
|
+
fid = args.shift
|
|
479
|
+
frame = @target_frames[fid]
|
|
480
|
+
if b = frame.binding
|
|
481
|
+
vars = b.local_variables.map{|name|
|
|
482
|
+
v = b.local_variable_get(name)
|
|
483
|
+
variable(name, v)
|
|
484
|
+
}
|
|
485
|
+
vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
|
|
486
|
+
vars.unshift variable('%return', frame.return_value) if frame.has_return_value
|
|
487
|
+
vars.unshift variable('%self', b.receiver)
|
|
488
|
+
else
|
|
489
|
+
vars = [variable('%self', frame.self)]
|
|
490
|
+
vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
|
|
491
|
+
vars.push variable('%return', frame.return_value) if frame.has_return_value
|
|
492
|
+
end
|
|
493
|
+
event! :dap_result, :scope, req, variables: vars, tid: self.id
|
|
494
|
+
|
|
495
|
+
when :variable
|
|
496
|
+
vid = args.shift
|
|
497
|
+
obj = @var_map[vid]
|
|
498
|
+
if obj
|
|
499
|
+
case req.dig('arguments', 'filter')
|
|
500
|
+
when 'indexed'
|
|
501
|
+
start = req.dig('arguments', 'start') || 0
|
|
502
|
+
count = req.dig('arguments', 'count') || obj.size
|
|
503
|
+
vars = (start ... (start + count)).map{|i|
|
|
504
|
+
variable(i.to_s, obj[i])
|
|
505
|
+
}
|
|
506
|
+
else
|
|
507
|
+
vars = []
|
|
508
|
+
|
|
509
|
+
case obj
|
|
510
|
+
when Hash
|
|
511
|
+
vars = obj.map{|k, v|
|
|
512
|
+
variable(DEBUGGER__.short_inspect(k), v)
|
|
513
|
+
}
|
|
514
|
+
when Struct
|
|
515
|
+
vars = obj.members.map{|m|
|
|
516
|
+
variable(m, obj[m])
|
|
517
|
+
}
|
|
518
|
+
when String
|
|
519
|
+
vars = [
|
|
520
|
+
variable('#length', obj.length),
|
|
521
|
+
variable('#encoding', obj.encoding)
|
|
522
|
+
]
|
|
523
|
+
when Class, Module
|
|
524
|
+
vars = obj.instance_variables.map{|iv|
|
|
525
|
+
variable(iv, obj.instance_variable_get(iv))
|
|
526
|
+
}
|
|
527
|
+
vars.unshift variable('%ancestors', obj.ancestors[1..])
|
|
528
|
+
when Range
|
|
529
|
+
vars = [
|
|
530
|
+
variable('#begin', obj.begin),
|
|
531
|
+
variable('#end', obj.end),
|
|
532
|
+
]
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
vars += obj.instance_variables.map{|iv|
|
|
536
|
+
variable(iv, obj.instance_variable_get(iv))
|
|
537
|
+
}
|
|
538
|
+
vars.unshift variable('#class', obj.class)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
|
|
542
|
+
|
|
543
|
+
when :evaluate
|
|
544
|
+
fid, expr = args
|
|
545
|
+
frame = @target_frames[fid]
|
|
546
|
+
|
|
547
|
+
if frame && (b = frame.binding)
|
|
548
|
+
begin
|
|
549
|
+
result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
|
|
550
|
+
rescue Exception => e
|
|
551
|
+
result = e
|
|
552
|
+
end
|
|
553
|
+
else
|
|
554
|
+
result = 'can not evaluate on this frame...'
|
|
555
|
+
end
|
|
556
|
+
event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
|
|
557
|
+
else
|
|
558
|
+
raise "Unkown req: #{args.inspect}"
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def evaluate_result r
|
|
563
|
+
v = variable nil, r
|
|
564
|
+
v.delete(:name)
|
|
565
|
+
v[:result] = DEBUGGER__.short_inspect(r)
|
|
566
|
+
v
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
|
|
570
|
+
if indexedVariables > 0 || namedVariables > 0
|
|
571
|
+
vid = @var_map.size + 1
|
|
572
|
+
@var_map[vid] = obj
|
|
573
|
+
else
|
|
574
|
+
vid = 0
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
ivnum = obj.instance_variables.size
|
|
578
|
+
|
|
579
|
+
{ name: name,
|
|
580
|
+
value: DEBUGGER__.short_inspect(obj, use_short),
|
|
581
|
+
type: obj.class.name || obj.class.to_s,
|
|
582
|
+
variablesReference: vid,
|
|
583
|
+
indexedVariables: indexedVariables,
|
|
584
|
+
namedVariables: namedVariables + ivnum,
|
|
585
|
+
}
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def variable name, obj
|
|
589
|
+
case obj
|
|
590
|
+
when Array
|
|
591
|
+
variable_ name, obj, indexedVariables: obj.size
|
|
592
|
+
when Hash
|
|
593
|
+
variable_ name, obj, namedVariables: obj.size
|
|
594
|
+
when String
|
|
595
|
+
variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
|
|
596
|
+
when Struct
|
|
597
|
+
variable_ name, obj, namedVariables: obj.size
|
|
598
|
+
when Class, Module
|
|
599
|
+
variable_ name, obj, namedVariables: 1 # %ancestors (#ancestors without self)
|
|
600
|
+
when Range
|
|
601
|
+
variable_ name, obj, namedVariables: 2 # #begin, #end
|
|
602
|
+
else
|
|
603
|
+
variable_ name, obj, namedVariables: 1 # #class
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|