ruby-lsp 0.11.2 → 0.12.1
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/VERSION +1 -1
- data/exe/ruby-lsp +11 -2
- data/exe/ruby-lsp-check +2 -1
- data/exe/ruby-lsp-doctor +15 -0
- data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +125 -0
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +10 -2
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +205 -0
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +23 -106
- data/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +6 -6
- data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +101 -49
- data/lib/ruby_indexer/ruby_indexer.rb +4 -3
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +49 -16
- data/lib/ruby_indexer/test/constant_test.rb +99 -36
- data/lib/ruby_indexer/test/index_test.rb +1 -1
- data/lib/ruby_indexer/test/method_test.rb +73 -0
- data/lib/ruby_indexer/test/test_case.rb +5 -1
- data/lib/ruby_lsp/addon.rb +8 -8
- data/lib/ruby_lsp/document.rb +14 -14
- data/lib/ruby_lsp/executor.rb +89 -53
- data/lib/ruby_lsp/internal.rb +7 -2
- data/lib/ruby_lsp/listener.rb +6 -6
- data/lib/ruby_lsp/requests/base_request.rb +1 -9
- data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
- data/lib/ruby_lsp/requests/code_lens.rb +47 -31
- data/lib/ruby_lsp/requests/completion.rb +83 -32
- data/lib/ruby_lsp/requests/definition.rb +21 -15
- data/lib/ruby_lsp/requests/diagnostics.rb +1 -1
- data/lib/ruby_lsp/requests/document_highlight.rb +508 -31
- data/lib/ruby_lsp/requests/document_link.rb +24 -17
- data/lib/ruby_lsp/requests/document_symbol.rb +42 -42
- data/lib/ruby_lsp/requests/folding_ranges.rb +83 -77
- data/lib/ruby_lsp/requests/hover.rb +22 -17
- data/lib/ruby_lsp/requests/inlay_hints.rb +6 -6
- data/lib/ruby_lsp/requests/selection_ranges.rb +13 -105
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +92 -92
- data/lib/ruby_lsp/requests/support/annotation.rb +3 -3
- data/lib/ruby_lsp/requests/support/common.rb +5 -5
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +21 -7
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +19 -0
- data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +10 -7
- data/lib/ruby_lsp/requests/support/sorbet.rb +28 -28
- data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -4
- data/lib/ruby_lsp/requests.rb +0 -1
- data/lib/ruby_lsp/setup_bundler.rb +26 -17
- metadata +20 -17
- data/lib/ruby_lsp/event_emitter.rb +0 -351
- data/lib/ruby_lsp/requests/support/highlight_target.rb +0 -118
@@ -2,10 +2,10 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module RubyIndexer
|
5
|
-
class IndexVisitor <
|
5
|
+
class IndexVisitor < Prism::Visitor
|
6
6
|
extend T::Sig
|
7
7
|
|
8
|
-
sig { params(index: Index, parse_result:
|
8
|
+
sig { params(index: Index, parse_result: Prism::ParseResult, file_path: String).void }
|
9
9
|
def initialize(index, parse_result, file_path)
|
10
10
|
@index = index
|
11
11
|
@file_path = file_path
|
@@ -14,95 +14,127 @@ module RubyIndexer
|
|
14
14
|
parse_result.comments.to_h do |c|
|
15
15
|
[c.location.start_line, c]
|
16
16
|
end,
|
17
|
-
T::Hash[Integer,
|
17
|
+
T::Hash[Integer, Prism::Comment],
|
18
18
|
)
|
19
19
|
|
20
20
|
super()
|
21
21
|
end
|
22
22
|
|
23
|
-
sig { override.params(node:
|
23
|
+
sig { override.params(node: Prism::ClassNode).void }
|
24
24
|
def visit_class_node(node)
|
25
|
-
|
25
|
+
add_class_entry(node)
|
26
26
|
end
|
27
27
|
|
28
|
-
sig { override.params(node:
|
28
|
+
sig { override.params(node: Prism::ModuleNode).void }
|
29
29
|
def visit_module_node(node)
|
30
|
-
|
30
|
+
add_module_entry(node)
|
31
31
|
end
|
32
32
|
|
33
|
-
sig { override.params(node:
|
33
|
+
sig { override.params(node: Prism::MultiWriteNode).void }
|
34
|
+
def visit_multi_write_node(node)
|
35
|
+
value = node.value
|
36
|
+
values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : []
|
37
|
+
|
38
|
+
node.targets.each_with_index do |target, i|
|
39
|
+
current_value = values[i]
|
40
|
+
# The moment we find a splat on the right hand side of the assignment, we can no longer figure out which value
|
41
|
+
# gets assigned to what
|
42
|
+
values.clear if current_value.is_a?(Prism::SplatNode)
|
43
|
+
|
44
|
+
case target
|
45
|
+
when Prism::ConstantTargetNode
|
46
|
+
add_constant(target, fully_qualify_name(target.name.to_s), current_value)
|
47
|
+
when Prism::ConstantPathTargetNode
|
48
|
+
add_constant(target, fully_qualify_name(target.slice), current_value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { override.params(node: Prism::ConstantPathWriteNode).void }
|
34
54
|
def visit_constant_path_write_node(node)
|
35
55
|
# ignore variable constants like `var::FOO` or `self.class::FOO`
|
36
56
|
target = node.target
|
37
|
-
return unless target.parent.nil? || target.parent.is_a?(
|
57
|
+
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
|
38
58
|
|
39
59
|
name = fully_qualify_name(target.location.slice)
|
40
60
|
add_constant(node, name)
|
41
61
|
end
|
42
62
|
|
43
|
-
sig { override.params(node:
|
63
|
+
sig { override.params(node: Prism::ConstantPathOrWriteNode).void }
|
44
64
|
def visit_constant_path_or_write_node(node)
|
45
65
|
# ignore variable constants like `var::FOO` or `self.class::FOO`
|
46
66
|
target = node.target
|
47
|
-
return unless target.parent.nil? || target.parent.is_a?(
|
67
|
+
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
|
48
68
|
|
49
69
|
name = fully_qualify_name(target.location.slice)
|
50
70
|
add_constant(node, name)
|
51
71
|
end
|
52
72
|
|
53
|
-
sig { override.params(node:
|
73
|
+
sig { override.params(node: Prism::ConstantPathOperatorWriteNode).void }
|
54
74
|
def visit_constant_path_operator_write_node(node)
|
55
75
|
# ignore variable constants like `var::FOO` or `self.class::FOO`
|
56
76
|
target = node.target
|
57
|
-
return unless target.parent.nil? || target.parent.is_a?(
|
77
|
+
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
|
58
78
|
|
59
79
|
name = fully_qualify_name(target.location.slice)
|
60
80
|
add_constant(node, name)
|
61
81
|
end
|
62
82
|
|
63
|
-
sig { override.params(node:
|
83
|
+
sig { override.params(node: Prism::ConstantPathAndWriteNode).void }
|
64
84
|
def visit_constant_path_and_write_node(node)
|
65
85
|
# ignore variable constants like `var::FOO` or `self.class::FOO`
|
66
86
|
target = node.target
|
67
|
-
return unless target.parent.nil? || target.parent.is_a?(
|
87
|
+
return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
|
68
88
|
|
69
89
|
name = fully_qualify_name(target.location.slice)
|
70
90
|
add_constant(node, name)
|
71
91
|
end
|
72
92
|
|
73
|
-
sig { override.params(node:
|
93
|
+
sig { override.params(node: Prism::ConstantWriteNode).void }
|
74
94
|
def visit_constant_write_node(node)
|
75
95
|
name = fully_qualify_name(node.name.to_s)
|
76
96
|
add_constant(node, name)
|
77
97
|
end
|
78
98
|
|
79
|
-
sig { override.params(node:
|
99
|
+
sig { override.params(node: Prism::ConstantOrWriteNode).void }
|
80
100
|
def visit_constant_or_write_node(node)
|
81
101
|
name = fully_qualify_name(node.name.to_s)
|
82
102
|
add_constant(node, name)
|
83
103
|
end
|
84
104
|
|
85
|
-
sig { override.params(node:
|
105
|
+
sig { override.params(node: Prism::ConstantAndWriteNode).void }
|
86
106
|
def visit_constant_and_write_node(node)
|
87
107
|
name = fully_qualify_name(node.name.to_s)
|
88
108
|
add_constant(node, name)
|
89
109
|
end
|
90
110
|
|
91
|
-
sig { override.params(node:
|
111
|
+
sig { override.params(node: Prism::ConstantOperatorWriteNode).void }
|
92
112
|
def visit_constant_operator_write_node(node)
|
93
113
|
name = fully_qualify_name(node.name.to_s)
|
94
114
|
add_constant(node, name)
|
95
115
|
end
|
96
116
|
|
97
|
-
sig { override.params(node:
|
117
|
+
sig { override.params(node: Prism::CallNode).void }
|
98
118
|
def visit_call_node(node)
|
99
119
|
message = node.message
|
100
120
|
handle_private_constant(node) if message == "private_constant"
|
101
121
|
end
|
102
122
|
|
123
|
+
sig { override.params(node: Prism::DefNode).void }
|
124
|
+
def visit_def_node(node)
|
125
|
+
method_name = node.name.to_s
|
126
|
+
comments = collect_comments(node)
|
127
|
+
case node.receiver
|
128
|
+
when nil
|
129
|
+
@index << Entry::InstanceMethod.new(method_name, @file_path, node.location, comments, node.parameters)
|
130
|
+
when Prism::SelfNode
|
131
|
+
@index << Entry::SingletonMethod.new(method_name, @file_path, node.location, comments, node.parameters)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
103
135
|
private
|
104
136
|
|
105
|
-
sig { params(node:
|
137
|
+
sig { params(node: Prism::CallNode).void }
|
106
138
|
def handle_private_constant(node)
|
107
139
|
arguments = node.arguments&.arguments
|
108
140
|
return unless arguments
|
@@ -110,9 +142,9 @@ module RubyIndexer
|
|
110
142
|
first_argument = arguments.first
|
111
143
|
|
112
144
|
name = case first_argument
|
113
|
-
when
|
145
|
+
when Prism::StringNode
|
114
146
|
first_argument.content
|
115
|
-
when
|
147
|
+
when Prism::SymbolNode
|
116
148
|
first_argument.value
|
117
149
|
end
|
118
150
|
|
@@ -130,58 +162,78 @@ module RubyIndexer
|
|
130
162
|
sig do
|
131
163
|
params(
|
132
164
|
node: T.any(
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
165
|
+
Prism::ConstantWriteNode,
|
166
|
+
Prism::ConstantOrWriteNode,
|
167
|
+
Prism::ConstantAndWriteNode,
|
168
|
+
Prism::ConstantOperatorWriteNode,
|
169
|
+
Prism::ConstantPathWriteNode,
|
170
|
+
Prism::ConstantPathOrWriteNode,
|
171
|
+
Prism::ConstantPathOperatorWriteNode,
|
172
|
+
Prism::ConstantPathAndWriteNode,
|
173
|
+
Prism::ConstantTargetNode,
|
174
|
+
Prism::ConstantPathTargetNode,
|
141
175
|
),
|
142
176
|
name: String,
|
177
|
+
value: T.nilable(Prism::Node),
|
143
178
|
).void
|
144
179
|
end
|
145
|
-
def add_constant(node, name)
|
146
|
-
value = node.value
|
180
|
+
def add_constant(node, name, value = nil)
|
181
|
+
value = node.value unless node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode)
|
147
182
|
comments = collect_comments(node)
|
148
183
|
|
149
184
|
@index << case value
|
150
|
-
when
|
151
|
-
|
152
|
-
when
|
153
|
-
|
185
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
186
|
+
Entry::UnresolvedAlias.new(value.slice, @stack.dup, name, @file_path, node.location, comments)
|
187
|
+
when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode,
|
188
|
+
Prism::ConstantOperatorWriteNode
|
154
189
|
|
155
190
|
# If the right hand side is another constant assignment, we need to visit it because that constant has to be
|
156
191
|
# indexed too
|
157
192
|
visit(value)
|
158
|
-
|
159
|
-
when
|
160
|
-
|
193
|
+
Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments)
|
194
|
+
when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
|
195
|
+
Prism::ConstantPathAndWriteNode
|
161
196
|
|
162
197
|
visit(value)
|
163
|
-
|
198
|
+
Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments)
|
164
199
|
else
|
165
|
-
|
200
|
+
Entry::Constant.new(name, @file_path, node.location, comments)
|
166
201
|
end
|
167
202
|
end
|
168
203
|
|
169
|
-
sig { params(node:
|
170
|
-
def
|
204
|
+
sig { params(node: Prism::ModuleNode).void }
|
205
|
+
def add_module_entry(node)
|
171
206
|
name = node.constant_path.location.slice
|
207
|
+
return visit_child_nodes(node) unless /^[A-Z:]/.match?(name)
|
172
208
|
|
173
|
-
|
174
|
-
|
175
|
-
|
209
|
+
comments = collect_comments(node)
|
210
|
+
|
211
|
+
@index << Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments)
|
212
|
+
@stack << name
|
213
|
+
visit_child_nodes(node)
|
214
|
+
@stack.pop
|
215
|
+
end
|
216
|
+
|
217
|
+
sig { params(node: Prism::ClassNode).void }
|
218
|
+
def add_class_entry(node)
|
219
|
+
name = node.constant_path.location.slice
|
220
|
+
return visit_child_nodes(node) unless /^[A-Z:]/.match?(name)
|
176
221
|
|
177
222
|
comments = collect_comments(node)
|
178
|
-
|
223
|
+
|
224
|
+
superclass = node.superclass
|
225
|
+
parent_class = case superclass
|
226
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
227
|
+
superclass.slice
|
228
|
+
end
|
229
|
+
|
230
|
+
@index << Entry::Class.new(fully_qualify_name(name), @file_path, node.location, comments, parent_class)
|
179
231
|
@stack << name
|
180
232
|
visit(node.body)
|
181
233
|
@stack.pop
|
182
234
|
end
|
183
235
|
|
184
|
-
sig { params(node:
|
236
|
+
sig { params(node: Prism::Node).returns(T::Array[String]) }
|
185
237
|
def collect_comments(node)
|
186
238
|
comments = []
|
187
239
|
|
@@ -7,16 +7,17 @@ require "did_you_mean"
|
|
7
7
|
require "ruby_indexer/lib/ruby_indexer/indexable_path"
|
8
8
|
require "ruby_indexer/lib/ruby_indexer/visitor"
|
9
9
|
require "ruby_indexer/lib/ruby_indexer/index"
|
10
|
+
require "ruby_indexer/lib/ruby_indexer/entry"
|
10
11
|
require "ruby_indexer/lib/ruby_indexer/configuration"
|
11
12
|
require "ruby_indexer/lib/ruby_indexer/prefix_tree"
|
12
13
|
|
13
14
|
module RubyIndexer
|
15
|
+
@configuration = T.let(Configuration.new, Configuration)
|
16
|
+
|
14
17
|
class << self
|
15
18
|
extend T::Sig
|
16
19
|
|
17
20
|
sig { returns(Configuration) }
|
18
|
-
|
19
|
-
@configuration ||= T.let(Configuration.new, T.nilable(Configuration))
|
20
|
-
end
|
21
|
+
attr_reader :configuration
|
21
22
|
end
|
22
23
|
end
|
@@ -11,7 +11,7 @@ module RubyIndexer
|
|
11
11
|
end
|
12
12
|
RUBY
|
13
13
|
|
14
|
-
assert_entry("Foo",
|
14
|
+
assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_class_with_statements
|
@@ -21,7 +21,7 @@ module RubyIndexer
|
|
21
21
|
end
|
22
22
|
RUBY
|
23
23
|
|
24
|
-
assert_entry("Foo",
|
24
|
+
assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:2-3")
|
25
25
|
end
|
26
26
|
|
27
27
|
def test_colon_colon_class
|
@@ -30,7 +30,7 @@ module RubyIndexer
|
|
30
30
|
end
|
31
31
|
RUBY
|
32
32
|
|
33
|
-
assert_entry("Foo",
|
33
|
+
assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
34
34
|
end
|
35
35
|
|
36
36
|
def test_colon_colon_class_inside_class
|
@@ -41,8 +41,8 @@ module RubyIndexer
|
|
41
41
|
end
|
42
42
|
RUBY
|
43
43
|
|
44
|
-
assert_entry("Bar",
|
45
|
-
assert_entry("Foo",
|
44
|
+
assert_entry("Bar", Entry::Class, "/fake/path/foo.rb:0-0:3-3")
|
45
|
+
assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
|
46
46
|
end
|
47
47
|
|
48
48
|
def test_namespaced_class
|
@@ -51,7 +51,7 @@ module RubyIndexer
|
|
51
51
|
end
|
52
52
|
RUBY
|
53
53
|
|
54
|
-
assert_entry("Foo::Bar",
|
54
|
+
assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
55
55
|
end
|
56
56
|
|
57
57
|
def test_dynamically_namespaced_class
|
@@ -69,7 +69,7 @@ module RubyIndexer
|
|
69
69
|
end
|
70
70
|
RUBY
|
71
71
|
|
72
|
-
assert_entry("Foo",
|
72
|
+
assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
73
73
|
end
|
74
74
|
|
75
75
|
def test_module_with_statements
|
@@ -79,7 +79,7 @@ module RubyIndexer
|
|
79
79
|
end
|
80
80
|
RUBY
|
81
81
|
|
82
|
-
assert_entry("Foo",
|
82
|
+
assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:2-3")
|
83
83
|
end
|
84
84
|
|
85
85
|
def test_colon_colon_module
|
@@ -88,7 +88,7 @@ module RubyIndexer
|
|
88
88
|
end
|
89
89
|
RUBY
|
90
90
|
|
91
|
-
assert_entry("Foo",
|
91
|
+
assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
92
92
|
end
|
93
93
|
|
94
94
|
def test_namespaced_module
|
@@ -97,7 +97,7 @@ module RubyIndexer
|
|
97
97
|
end
|
98
98
|
RUBY
|
99
99
|
|
100
|
-
assert_entry("Foo::Bar",
|
100
|
+
assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
101
101
|
end
|
102
102
|
|
103
103
|
def test_dynamically_namespaced_module
|
@@ -124,11 +124,11 @@ module RubyIndexer
|
|
124
124
|
end
|
125
125
|
RUBY
|
126
126
|
|
127
|
-
assert_entry("Foo",
|
128
|
-
assert_entry("Foo::Bar",
|
129
|
-
assert_entry("Foo::Baz",
|
130
|
-
assert_entry("Foo::Baz::Qux",
|
131
|
-
assert_entry("Foo::Baz::Qux::Something",
|
127
|
+
assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:10-3")
|
128
|
+
assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
|
129
|
+
assert_entry("Foo::Baz", Entry::Module, "/fake/path/foo.rb:4-2:9-5")
|
130
|
+
assert_entry("Foo::Baz::Qux", Entry::Class, "/fake/path/foo.rb:5-4:8-7")
|
131
|
+
assert_entry("Foo::Baz::Qux::Something", Entry::Class, "/fake/path/foo.rb:6-6:7-9")
|
132
132
|
end
|
133
133
|
|
134
134
|
def test_deleting_from_index_based_on_file_path
|
@@ -137,7 +137,7 @@ module RubyIndexer
|
|
137
137
|
end
|
138
138
|
RUBY
|
139
139
|
|
140
|
-
assert_entry("Foo",
|
140
|
+
assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
141
141
|
|
142
142
|
@index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
|
143
143
|
refute_entry("Foo")
|
@@ -239,5 +239,38 @@ module RubyIndexer
|
|
239
239
|
d_const = @index["A::D"].first
|
240
240
|
assert_equal(:public, d_const.visibility)
|
241
241
|
end
|
242
|
+
|
243
|
+
def test_keeping_track_of_super_classes
|
244
|
+
index(<<~RUBY)
|
245
|
+
class Foo < Bar
|
246
|
+
end
|
247
|
+
|
248
|
+
class Baz
|
249
|
+
end
|
250
|
+
|
251
|
+
module Something
|
252
|
+
class Baz
|
253
|
+
end
|
254
|
+
|
255
|
+
class Qux < ::Baz
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class FinalThing < Something::Baz
|
260
|
+
end
|
261
|
+
RUBY
|
262
|
+
|
263
|
+
foo = T.must(@index["Foo"].first)
|
264
|
+
assert_equal("Bar", foo.parent_class)
|
265
|
+
|
266
|
+
baz = T.must(@index["Baz"].first)
|
267
|
+
assert_nil(baz.parent_class)
|
268
|
+
|
269
|
+
qux = T.must(@index["Something::Qux"].first)
|
270
|
+
assert_equal("::Baz", qux.parent_class)
|
271
|
+
|
272
|
+
final_thing = T.must(@index["FinalThing"].first)
|
273
|
+
assert_equal("Something::Baz", final_thing.parent_class)
|
274
|
+
end
|
242
275
|
end
|
243
276
|
end
|