syntax_tree 5.3.0 → 6.0.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 +4 -4
- data/.rubocop.yml +12 -1
- data/CHANGELOG.md +64 -1
- data/Gemfile.lock +2 -2
- data/README.md +28 -9
- data/Rakefile +12 -8
- data/bin/console +1 -0
- data/bin/whitequark +79 -0
- data/doc/changing_structure.md +16 -0
- data/lib/syntax_tree/basic_visitor.rb +44 -5
- data/lib/syntax_tree/cli.rb +2 -2
- data/lib/syntax_tree/dsl.rb +23 -11
- data/lib/syntax_tree/{visitor/field_visitor.rb → field_visitor.rb} +54 -55
- data/lib/syntax_tree/formatter.rb +1 -1
- data/lib/syntax_tree/index.rb +56 -54
- data/lib/syntax_tree/json_visitor.rb +55 -0
- data/lib/syntax_tree/language_server.rb +157 -2
- data/lib/syntax_tree/match_visitor.rb +120 -0
- data/lib/syntax_tree/mermaid.rb +177 -0
- data/lib/syntax_tree/mermaid_visitor.rb +69 -0
- data/lib/syntax_tree/{visitor/mutation_visitor.rb → mutation_visitor.rb} +27 -27
- data/lib/syntax_tree/node.rb +198 -107
- data/lib/syntax_tree/parser.rb +322 -118
- data/lib/syntax_tree/pretty_print_visitor.rb +83 -0
- data/lib/syntax_tree/reflection.rb +241 -0
- data/lib/syntax_tree/translation/parser.rb +3019 -0
- data/lib/syntax_tree/translation/rubocop_ast.rb +21 -0
- data/lib/syntax_tree/translation.rb +28 -0
- data/lib/syntax_tree/version.rb +1 -1
- data/lib/syntax_tree/with_scope.rb +244 -0
- data/lib/syntax_tree/yarv/basic_block.rb +53 -0
- data/lib/syntax_tree/yarv/calldata.rb +91 -0
- data/lib/syntax_tree/yarv/compiler.rb +110 -100
- data/lib/syntax_tree/yarv/control_flow_graph.rb +257 -0
- data/lib/syntax_tree/yarv/data_flow_graph.rb +338 -0
- data/lib/syntax_tree/yarv/decompiler.rb +1 -1
- data/lib/syntax_tree/yarv/disassembler.rb +104 -80
- data/lib/syntax_tree/yarv/instruction_sequence.rb +43 -18
- data/lib/syntax_tree/yarv/instructions.rb +203 -649
- data/lib/syntax_tree/yarv/legacy.rb +12 -24
- data/lib/syntax_tree/yarv/sea_of_nodes.rb +534 -0
- data/lib/syntax_tree/yarv.rb +18 -0
- data/lib/syntax_tree.rb +88 -56
- data/tasks/sorbet.rake +277 -0
- data/tasks/whitequark.rake +87 -0
- metadata +23 -11
- data/.gitmodules +0 -9
- data/lib/syntax_tree/language_server/inlay_hints.rb +0 -159
- data/lib/syntax_tree/visitor/environment.rb +0 -84
- data/lib/syntax_tree/visitor/json_visitor.rb +0 -55
- data/lib/syntax_tree/visitor/match_visitor.rb +0 -122
- data/lib/syntax_tree/visitor/pretty_print_visitor.rb +0 -85
- data/lib/syntax_tree/visitor/with_environment.rb +0 -140
@@ -1,55 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SyntaxTree
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
class FieldVisitor < BasicVisitor
|
4
|
+
# This is the parent class of a lot of built-in visitors for Syntax Tree. It
|
5
|
+
# reflects visiting each of the fields on every node in turn. It itself does
|
6
|
+
# not do anything with these fields, it leaves that behavior up to the
|
7
|
+
# subclass to implement.
|
8
|
+
#
|
9
|
+
# In order to properly use this class, you will need to subclass it and
|
10
|
+
# implement #comments, #field, #list, #node, #pairs, and #text. Those are
|
11
|
+
# documented here.
|
12
|
+
#
|
13
|
+
# == comments(node)
|
14
|
+
#
|
15
|
+
# This accepts the node that is being visited and does something depending on
|
16
|
+
# the comments attached to the node.
|
17
|
+
#
|
18
|
+
# == field(name, value)
|
19
|
+
#
|
20
|
+
# This accepts the name of the field being visited as a string (like "value")
|
21
|
+
# and the actual value of that field. The value can be a subclass of Node or
|
22
|
+
# any other type that can be held within the tree.
|
23
|
+
#
|
24
|
+
# == list(name, values)
|
25
|
+
#
|
26
|
+
# This accepts the name of the field being visited as well as a list of
|
27
|
+
# values. This is used, for example, when visiting something like the body of
|
28
|
+
# a Statements node.
|
29
|
+
#
|
30
|
+
# == node(name, node)
|
31
|
+
#
|
32
|
+
# This is the parent serialization method for each node. It is called with the
|
33
|
+
# node itself, as well as the type of the node as a string. The type is an
|
34
|
+
# internally used value that usually resembles the name of the ripper event
|
35
|
+
# that generated the node. The method should yield to the given block which
|
36
|
+
# then calls through to visit each of the fields on the node.
|
37
|
+
#
|
38
|
+
# == text(name, value)
|
39
|
+
#
|
40
|
+
# This accepts the name of the field being visited as well as a string value
|
41
|
+
# representing the value of the field.
|
42
|
+
#
|
43
|
+
# == pairs(name, values)
|
44
|
+
#
|
45
|
+
# This accepts the name of the field being visited as well as a list of pairs
|
46
|
+
# that represent the value of the field. It is used only in a couple of
|
47
|
+
# circumstances, like when visiting the list of optional parameters defined on
|
48
|
+
# a method.
|
49
|
+
#
|
50
|
+
class FieldVisitor < BasicVisitor
|
51
|
+
visit_methods do
|
53
52
|
def visit_aref(node)
|
54
53
|
node(node, "aref") do
|
55
54
|
field("collection", node.collection)
|
@@ -1017,14 +1016,14 @@ module SyntaxTree
|
|
1017
1016
|
def visit___end__(node)
|
1018
1017
|
visit_token(node, "__end__")
|
1019
1018
|
end
|
1019
|
+
end
|
1020
1020
|
|
1021
|
-
|
1021
|
+
private
|
1022
1022
|
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
end
|
1023
|
+
def visit_token(node, type)
|
1024
|
+
node(node, type) do
|
1025
|
+
field("value", node.value)
|
1026
|
+
comments(node)
|
1028
1027
|
end
|
1029
1028
|
end
|
1030
1029
|
end
|
@@ -138,7 +138,7 @@ module SyntaxTree
|
|
138
138
|
# going to just print out the node as it was seen in the source.
|
139
139
|
doc =
|
140
140
|
if last_leading&.ignore?
|
141
|
-
range = source[node.
|
141
|
+
range = source[node.start_char...node.end_char]
|
142
142
|
first = true
|
143
143
|
|
144
144
|
range.each_line(chomp: true) do |line|
|
data/lib/syntax_tree/index.rb
CHANGED
@@ -257,74 +257,76 @@ module SyntaxTree
|
|
257
257
|
@statements = nil
|
258
258
|
end
|
259
259
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
results << ClassDefinition.new(
|
266
|
-
nesting.dup,
|
267
|
-
name,
|
268
|
-
location,
|
269
|
-
comments_for(node)
|
270
|
-
)
|
271
|
-
|
272
|
-
nesting << name
|
273
|
-
super
|
274
|
-
nesting.pop
|
275
|
-
end
|
276
|
-
|
277
|
-
def visit_const_ref(node)
|
278
|
-
node.constant.value
|
279
|
-
end
|
260
|
+
visit_methods do
|
261
|
+
def visit_class(node)
|
262
|
+
name = visit(node.constant).to_sym
|
263
|
+
location =
|
264
|
+
Location.new(node.location.start_line, node.location.start_column)
|
280
265
|
|
281
|
-
|
282
|
-
name = node.name.value.to_sym
|
283
|
-
location =
|
284
|
-
Location.new(node.location.start_line, node.location.start_column)
|
285
|
-
|
286
|
-
results << if node.target.nil?
|
287
|
-
MethodDefinition.new(
|
266
|
+
results << ClassDefinition.new(
|
288
267
|
nesting.dup,
|
289
268
|
name,
|
290
269
|
location,
|
291
270
|
comments_for(node)
|
292
271
|
)
|
293
|
-
|
294
|
-
|
272
|
+
|
273
|
+
nesting << name
|
274
|
+
super
|
275
|
+
nesting.pop
|
276
|
+
end
|
277
|
+
|
278
|
+
def visit_const_ref(node)
|
279
|
+
node.constant.value
|
280
|
+
end
|
281
|
+
|
282
|
+
def visit_def(node)
|
283
|
+
name = node.name.value.to_sym
|
284
|
+
location =
|
285
|
+
Location.new(node.location.start_line, node.location.start_column)
|
286
|
+
|
287
|
+
results << if node.target.nil?
|
288
|
+
MethodDefinition.new(
|
289
|
+
nesting.dup,
|
290
|
+
name,
|
291
|
+
location,
|
292
|
+
comments_for(node)
|
293
|
+
)
|
294
|
+
else
|
295
|
+
SingletonMethodDefinition.new(
|
296
|
+
nesting.dup,
|
297
|
+
name,
|
298
|
+
location,
|
299
|
+
comments_for(node)
|
300
|
+
)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def visit_module(node)
|
305
|
+
name = visit(node.constant).to_sym
|
306
|
+
location =
|
307
|
+
Location.new(node.location.start_line, node.location.start_column)
|
308
|
+
|
309
|
+
results << ModuleDefinition.new(
|
295
310
|
nesting.dup,
|
296
311
|
name,
|
297
312
|
location,
|
298
313
|
comments_for(node)
|
299
314
|
)
|
300
|
-
end
|
301
|
-
end
|
302
|
-
|
303
|
-
def visit_module(node)
|
304
|
-
name = visit(node.constant).to_sym
|
305
|
-
location =
|
306
|
-
Location.new(node.location.start_line, node.location.start_column)
|
307
315
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
comments_for(node)
|
313
|
-
)
|
314
|
-
|
315
|
-
nesting << name
|
316
|
-
super
|
317
|
-
nesting.pop
|
318
|
-
end
|
316
|
+
nesting << name
|
317
|
+
super
|
318
|
+
nesting.pop
|
319
|
+
end
|
319
320
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
321
|
+
def visit_program(node)
|
322
|
+
super
|
323
|
+
results
|
324
|
+
end
|
324
325
|
|
325
|
-
|
326
|
-
|
327
|
-
|
326
|
+
def visit_statements(node)
|
327
|
+
@statements = node
|
328
|
+
super
|
329
|
+
end
|
328
330
|
end
|
329
331
|
|
330
332
|
private
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module SyntaxTree
|
6
|
+
# This visitor transforms the AST into a hash that contains only primitives
|
7
|
+
# that can be easily serialized into JSON.
|
8
|
+
class JSONVisitor < FieldVisitor
|
9
|
+
attr_reader :target
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@target = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def comments(node)
|
18
|
+
target[:comments] = visit_all(node.comments)
|
19
|
+
end
|
20
|
+
|
21
|
+
def field(name, value)
|
22
|
+
target[name] = value.is_a?(Node) ? visit(value) : value
|
23
|
+
end
|
24
|
+
|
25
|
+
def list(name, values)
|
26
|
+
target[name] = visit_all(values)
|
27
|
+
end
|
28
|
+
|
29
|
+
def node(node, type)
|
30
|
+
previous = @target
|
31
|
+
@target = { type: type, location: visit_location(node.location) }
|
32
|
+
yield
|
33
|
+
@target
|
34
|
+
ensure
|
35
|
+
@target = previous
|
36
|
+
end
|
37
|
+
|
38
|
+
def pairs(name, values)
|
39
|
+
target[name] = values.map { |(key, value)| [visit(key), visit(value)] }
|
40
|
+
end
|
41
|
+
|
42
|
+
def text(name, value)
|
43
|
+
target[name] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_location(location)
|
47
|
+
[
|
48
|
+
location.start_line,
|
49
|
+
location.start_char,
|
50
|
+
location.end_line,
|
51
|
+
location.end_char
|
52
|
+
]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -2,10 +2,9 @@
|
|
2
2
|
|
3
3
|
require "cgi"
|
4
4
|
require "json"
|
5
|
+
require "pp"
|
5
6
|
require "uri"
|
6
7
|
|
7
|
-
require_relative "language_server/inlay_hints"
|
8
|
-
|
9
8
|
module SyntaxTree
|
10
9
|
# Syntax Tree additionally ships with a language server conforming to the
|
11
10
|
# language server protocol. It can be invoked through the CLI by running:
|
@@ -13,6 +12,162 @@ module SyntaxTree
|
|
13
12
|
# stree lsp
|
14
13
|
#
|
15
14
|
class LanguageServer
|
15
|
+
# This class provides inlay hints for the language server. For more
|
16
|
+
# information, see the spec here:
|
17
|
+
# https://github.com/microsoft/language-server-protocol/issues/956.
|
18
|
+
class InlayHints < Visitor
|
19
|
+
# This represents a hint that is going to be displayed in the editor.
|
20
|
+
class Hint
|
21
|
+
attr_reader :line, :character, :label
|
22
|
+
|
23
|
+
def initialize(line:, character:, label:)
|
24
|
+
@line = line
|
25
|
+
@character = character
|
26
|
+
@label = label
|
27
|
+
end
|
28
|
+
|
29
|
+
# This is the shape that the LSP expects.
|
30
|
+
def to_json(*opts)
|
31
|
+
{
|
32
|
+
position: {
|
33
|
+
line: line,
|
34
|
+
character: character
|
35
|
+
},
|
36
|
+
label: label
|
37
|
+
}.to_json(*opts)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :stack, :hints
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
@stack = []
|
45
|
+
@hints = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def visit(node)
|
49
|
+
stack << node
|
50
|
+
result = super
|
51
|
+
stack.pop
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
visit_methods do
|
56
|
+
# Adds parentheses around assignments contained within the default
|
57
|
+
# values of parameters. For example,
|
58
|
+
#
|
59
|
+
# def foo(a = b = c)
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# becomes
|
63
|
+
#
|
64
|
+
# def foo(a = ₍b = c₎)
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
def visit_assign(node)
|
68
|
+
parentheses(node.location) if stack[-2].is_a?(Params)
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds parentheses around binary expressions to make it clear which
|
73
|
+
# subexpression will be evaluated first. For example,
|
74
|
+
#
|
75
|
+
# a + b * c
|
76
|
+
#
|
77
|
+
# becomes
|
78
|
+
#
|
79
|
+
# a + ₍b * c₎
|
80
|
+
#
|
81
|
+
def visit_binary(node)
|
82
|
+
case stack[-2]
|
83
|
+
when Assign, OpAssign
|
84
|
+
parentheses(node.location)
|
85
|
+
when Binary
|
86
|
+
parentheses(node.location) if stack[-2].operator != node.operator
|
87
|
+
end
|
88
|
+
|
89
|
+
super
|
90
|
+
end
|
91
|
+
|
92
|
+
# Adds parentheses around ternary operators contained within certain
|
93
|
+
# expressions where it could be confusing which subexpression will get
|
94
|
+
# evaluated first. For example,
|
95
|
+
#
|
96
|
+
# a ? b : c ? d : e
|
97
|
+
#
|
98
|
+
# becomes
|
99
|
+
#
|
100
|
+
# a ? b : ₍c ? d : e₎
|
101
|
+
#
|
102
|
+
def visit_if_op(node)
|
103
|
+
case stack[-2]
|
104
|
+
when Assign, Binary, IfOp, OpAssign
|
105
|
+
parentheses(node.location)
|
106
|
+
end
|
107
|
+
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
# Adds the implicitly rescued StandardError into a bare rescue clause.
|
112
|
+
# For example,
|
113
|
+
#
|
114
|
+
# begin
|
115
|
+
# rescue
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# becomes
|
119
|
+
#
|
120
|
+
# begin
|
121
|
+
# rescue StandardError
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
def visit_rescue(node)
|
125
|
+
if node.exception.nil?
|
126
|
+
hints << Hint.new(
|
127
|
+
line: node.location.start_line - 1,
|
128
|
+
character: node.location.start_column + "rescue".length,
|
129
|
+
label: " StandardError"
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
136
|
+
# Adds parentheses around unary statements using the - operator that are
|
137
|
+
# contained within Binary nodes. For example,
|
138
|
+
#
|
139
|
+
# -a + b
|
140
|
+
#
|
141
|
+
# becomes
|
142
|
+
#
|
143
|
+
# ₍-a₎ + b
|
144
|
+
#
|
145
|
+
def visit_unary(node)
|
146
|
+
if stack[-2].is_a?(Binary) && (node.operator == "-")
|
147
|
+
parentheses(node.location)
|
148
|
+
end
|
149
|
+
|
150
|
+
super
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def parentheses(location)
|
157
|
+
hints << Hint.new(
|
158
|
+
line: location.start_line - 1,
|
159
|
+
character: location.start_column,
|
160
|
+
label: "₍"
|
161
|
+
)
|
162
|
+
|
163
|
+
hints << Hint.new(
|
164
|
+
line: location.end_line - 1,
|
165
|
+
character: location.end_column,
|
166
|
+
label: "₎"
|
167
|
+
)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
16
171
|
# This is a small module that effectively mirrors pattern matching. We're
|
17
172
|
# using it so that we can support truffleruby without having to ignore the
|
18
173
|
# language server.
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
# This visitor transforms the AST into a Ruby pattern matching expression that
|
5
|
+
# would match correctly against the AST.
|
6
|
+
class MatchVisitor < FieldVisitor
|
7
|
+
attr_reader :q
|
8
|
+
|
9
|
+
def initialize(q)
|
10
|
+
@q = q
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit(node)
|
14
|
+
case node
|
15
|
+
when Node
|
16
|
+
super
|
17
|
+
when String
|
18
|
+
# pp will split up a string on newlines and concat them together using a
|
19
|
+
# "+" operator. This breaks the pattern matching expression. So instead
|
20
|
+
# we're going to check here for strings and manually put the entire
|
21
|
+
# value into the output buffer.
|
22
|
+
q.text(node.inspect)
|
23
|
+
else
|
24
|
+
node.pretty_print(q)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def comments(node)
|
31
|
+
return if node.comments.empty?
|
32
|
+
|
33
|
+
q.nest(0) do
|
34
|
+
q.text("comments: [")
|
35
|
+
q.indent do
|
36
|
+
q.breakable("")
|
37
|
+
q.seplist(node.comments) { |comment| visit(comment) }
|
38
|
+
end
|
39
|
+
q.breakable("")
|
40
|
+
q.text("]")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def field(name, value)
|
45
|
+
q.nest(0) do
|
46
|
+
q.text(name)
|
47
|
+
q.text(": ")
|
48
|
+
visit(value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def list(name, values)
|
53
|
+
q.group do
|
54
|
+
q.text(name)
|
55
|
+
q.text(": [")
|
56
|
+
q.indent do
|
57
|
+
q.breakable("")
|
58
|
+
q.seplist(values) { |value| visit(value) }
|
59
|
+
end
|
60
|
+
q.breakable("")
|
61
|
+
q.text("]")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def node(node, _type)
|
66
|
+
items = []
|
67
|
+
q.with_target(items) { yield }
|
68
|
+
|
69
|
+
if items.empty?
|
70
|
+
q.text(node.class.name)
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
q.group do
|
75
|
+
q.text(node.class.name)
|
76
|
+
q.text("[")
|
77
|
+
q.indent do
|
78
|
+
q.breakable("")
|
79
|
+
q.seplist(items) { |item| q.target << item }
|
80
|
+
end
|
81
|
+
q.breakable("")
|
82
|
+
q.text("]")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def pairs(name, values)
|
87
|
+
q.group do
|
88
|
+
q.text(name)
|
89
|
+
q.text(": [")
|
90
|
+
q.indent do
|
91
|
+
q.breakable("")
|
92
|
+
q.seplist(values) do |(key, value)|
|
93
|
+
q.group do
|
94
|
+
q.text("[")
|
95
|
+
q.indent do
|
96
|
+
q.breakable("")
|
97
|
+
visit(key)
|
98
|
+
q.text(",")
|
99
|
+
q.breakable
|
100
|
+
visit(value || nil)
|
101
|
+
end
|
102
|
+
q.breakable("")
|
103
|
+
q.text("]")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
q.breakable("")
|
108
|
+
q.text("]")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def text(name, value)
|
113
|
+
q.nest(0) do
|
114
|
+
q.text(name)
|
115
|
+
q.text(": ")
|
116
|
+
value.pretty_print(q)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|