ruby-lsp-ree 0.1.0 → 0.1.2
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/CHANGELOG.md +27 -0
- data/Gemfile.lock +20 -0
- data/lib/ruby_lsp/ruby_lsp_ree/addon.rb +2 -5
- data/lib/ruby_lsp/ruby_lsp_ree/completion.rb +58 -115
- data/lib/ruby_lsp/ruby_lsp_ree/completion_utils.rb +267 -0
- data/lib/ruby_lsp/ruby_lsp_ree/definition.rb +88 -5
- data/lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_document.rb +150 -0
- data/lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_document_builder.rb +82 -0
- data/lib/ruby_lsp/ruby_lsp_ree/parsing/parsed_link_node.rb +94 -0
- data/lib/ruby_lsp/ruby_lsp_ree/ree_formatter.rb +21 -30
- data/lib/ruby_lsp/ruby_lsp_ree/ree_indexing_enhancement.rb +25 -25
- data/lib/ruby_lsp/ruby_lsp_ree/ree_lsp_utils.rb +118 -72
- data/lib/ruby_lsp/ruby_lsp_ree/ree_object_finder.rb +43 -0
- data/lib/ruby_lsp_ree/version.rb +1 -1
- metadata +8 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require_relative 'parsed_link_node'
|
|
2
|
+
|
|
3
|
+
class RubyLsp::Ree::ParsedDocument
|
|
4
|
+
include RubyLsp::Ree::ReeLspUtils
|
|
5
|
+
|
|
6
|
+
LINK_DSL_MODULE = 'Ree::LinkDSL'
|
|
7
|
+
|
|
8
|
+
attr_reader :ast, :package_name, :class_node, :fn_node, :fn_block_node, :class_includes,
|
|
9
|
+
:link_nodes, :values, :action_node, :action_block_node, :dao_node, :dao_block_node, :filters,
|
|
10
|
+
:bean_node, :bean_block_node, :bean_methods
|
|
11
|
+
|
|
12
|
+
def initialize(ast)
|
|
13
|
+
@ast = ast
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def links_container_node
|
|
17
|
+
@fn_node || @action_node || @dao_node || @bean_node
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def links_container_block_node
|
|
21
|
+
@fn_block_node || @action_block_node || @dao_block_node || @bean_block_node
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def includes_link_dsl?
|
|
25
|
+
@class_includes.any?{ _1.name == LINK_DSL_MODULE }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def includes_linked_constant?(const_name)
|
|
29
|
+
@link_nodes.map(&:imports).flatten.include?(const_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def includes_linked_object?(obj_name)
|
|
33
|
+
@link_nodes.map(&:name).include?(obj_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def has_blank_links_container?
|
|
37
|
+
links_container_node && !links_container_block_node
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def set_package_name(package_name)
|
|
41
|
+
@package_name = package_name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_class_node
|
|
45
|
+
@class_node ||= ast.statements.body.detect{ |node| node.is_a?(Prism::ClassNode) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_fn_node
|
|
49
|
+
return unless class_node
|
|
50
|
+
|
|
51
|
+
@fn_node ||= class_node.body.body.detect{ |node| node.name == :fn }
|
|
52
|
+
@fn_block_node = @fn_node&.block
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parse_action_node
|
|
56
|
+
return unless class_node
|
|
57
|
+
|
|
58
|
+
@action_node ||= class_node.body.body.detect{ |node| node.name == :action }
|
|
59
|
+
@action_block_node = @action_node&.block
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_dao_node
|
|
63
|
+
return unless class_node
|
|
64
|
+
|
|
65
|
+
@dao_node ||= class_node.body.body.detect{ |node| node.name == :dao }
|
|
66
|
+
@dao_block_node = @dao_node&.block
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_bean_node
|
|
70
|
+
return unless class_node
|
|
71
|
+
|
|
72
|
+
@bean_node ||= class_node.body.body.detect{ |node| node.name == :bean }
|
|
73
|
+
@bean_block_node = @bean_node&.block
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_class_includes
|
|
77
|
+
return unless class_node
|
|
78
|
+
|
|
79
|
+
@class_includes ||= class_node.body.body.select{ _1.name == :include }.map do |class_include|
|
|
80
|
+
parent_name = class_include.arguments.arguments.first.parent.name.to_s
|
|
81
|
+
module_name = class_include.arguments.arguments.first.name
|
|
82
|
+
|
|
83
|
+
OpenStruct.new(
|
|
84
|
+
name: [parent_name, module_name].compact.join('::')
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_links
|
|
90
|
+
return unless class_node
|
|
91
|
+
|
|
92
|
+
nodes = if links_container_node && links_container_block_node.body
|
|
93
|
+
links_container_block_node.body.body.select{ |node| node.name == :link }
|
|
94
|
+
elsif class_includes.any?{ _1.name == LINK_DSL_MODULE }
|
|
95
|
+
class_node.body.body.select{ |node| node.name == :link }
|
|
96
|
+
else
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@link_nodes = nodes.map do |link_node|
|
|
101
|
+
link_node = RubyLsp::Ree::ParsedLinkNode.new(link_node, package_name)
|
|
102
|
+
link_node.parse_imports
|
|
103
|
+
link_node
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_values
|
|
108
|
+
return unless class_node
|
|
109
|
+
|
|
110
|
+
@values ||= class_node.body.body
|
|
111
|
+
.select{ _1.name == :val }
|
|
112
|
+
.map{ OpenStruct.new(name: _1.arguments.arguments.first.unescaped) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_filters
|
|
116
|
+
return unless class_node
|
|
117
|
+
|
|
118
|
+
@filters ||= class_node.body.body
|
|
119
|
+
.select{ _1.name == :filter }
|
|
120
|
+
.map{ OpenStruct.new(name: _1.arguments.arguments.first.unescaped, signatures: parse_filter_signature(_1)) }
|
|
121
|
+
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_bean_methods
|
|
125
|
+
return unless class_node
|
|
126
|
+
|
|
127
|
+
@bean_methods ||= class_node.body.body
|
|
128
|
+
.select{ _1.is_a?(Prism::DefNode) }
|
|
129
|
+
.map{ OpenStruct.new(name: _1.name.to_s, signatures: parse_signatures_from_params(_1.parameters)) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_filter_signature(filter_node)
|
|
133
|
+
return [] unless filter_node
|
|
134
|
+
|
|
135
|
+
lambda_node = filter_node.arguments&.arguments[1]
|
|
136
|
+
return [] unless lambda_node
|
|
137
|
+
|
|
138
|
+
parse_signatures_from_params(lambda_node.parameters.parameters)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def parse_signatures_from_params(parameters)
|
|
142
|
+
signature_params = signature_params_from_node(parameters)
|
|
143
|
+
[RubyIndexer::Entry::Signature.new(signature_params)]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_class_name
|
|
147
|
+
name_parts = [class_node.constant_path&.parent&.name, class_node.constant_path.name]
|
|
148
|
+
name_parts.compact.map(&:to_s).join('::')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require 'prism'
|
|
2
|
+
require_relative 'parsed_document'
|
|
3
|
+
|
|
4
|
+
class RubyLsp::Ree::ParsedDocumentBuilder
|
|
5
|
+
extend RubyLsp::Ree::ReeLspUtils
|
|
6
|
+
|
|
7
|
+
def self.build_from_uri(uri, type = nil)
|
|
8
|
+
ast = Prism.parse_file(uri.path).value
|
|
9
|
+
document = build_document(ast, type)
|
|
10
|
+
|
|
11
|
+
document.set_package_name(package_name_from_uri(uri))
|
|
12
|
+
|
|
13
|
+
document
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.build_from_ast(ast, uri, type = nil)
|
|
17
|
+
document = build_document(ast, type)
|
|
18
|
+
|
|
19
|
+
document.set_package_name(package_name_from_uri(uri))
|
|
20
|
+
|
|
21
|
+
document
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.build_from_source(source, type = nil)
|
|
25
|
+
ast = Prism.parse(source).value
|
|
26
|
+
build_document(ast, type)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.build_document(ast, type)
|
|
30
|
+
case type
|
|
31
|
+
when :enum
|
|
32
|
+
build_enum_document(ast)
|
|
33
|
+
when :dao
|
|
34
|
+
build_dao_document(ast)
|
|
35
|
+
when :bean
|
|
36
|
+
build_bean_document(ast)
|
|
37
|
+
else
|
|
38
|
+
build_regular_document(ast)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.build_regular_document(ast)
|
|
43
|
+
document = RubyLsp::Ree::ParsedDocument.new(ast)
|
|
44
|
+
|
|
45
|
+
document.parse_class_node
|
|
46
|
+
document.parse_fn_node
|
|
47
|
+
document.parse_action_node
|
|
48
|
+
document.parse_bean_node
|
|
49
|
+
document.parse_dao_node
|
|
50
|
+
document.parse_class_includes
|
|
51
|
+
document.parse_links
|
|
52
|
+
|
|
53
|
+
document
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.build_enum_document(ast)
|
|
57
|
+
document = RubyLsp::Ree::ParsedDocument.new(ast)
|
|
58
|
+
|
|
59
|
+
document.parse_class_node
|
|
60
|
+
document.parse_values
|
|
61
|
+
|
|
62
|
+
document
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.build_dao_document(ast)
|
|
66
|
+
document = RubyLsp::Ree::ParsedDocument.new(ast)
|
|
67
|
+
|
|
68
|
+
document.parse_class_node
|
|
69
|
+
document.parse_filters
|
|
70
|
+
|
|
71
|
+
document
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.build_bean_document(ast)
|
|
75
|
+
document = RubyLsp::Ree::ParsedDocument.new(ast)
|
|
76
|
+
|
|
77
|
+
document.parse_class_node
|
|
78
|
+
document.parse_bean_methods
|
|
79
|
+
|
|
80
|
+
document
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require 'prism'
|
|
2
|
+
|
|
3
|
+
class RubyLsp::Ree::ParsedLinkNode
|
|
4
|
+
attr_reader :node, :document_package, :name, :imports
|
|
5
|
+
|
|
6
|
+
FROM_ARG_KEY = 'from'
|
|
7
|
+
IMPORT_ARG_KEY = 'import'
|
|
8
|
+
|
|
9
|
+
def initialize(node, document_package = nil)
|
|
10
|
+
@node = node
|
|
11
|
+
@document_package = document_package
|
|
12
|
+
@name = parse_name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def link_package_name
|
|
16
|
+
from_arg_value || document_package
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def location
|
|
20
|
+
@node.location
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def from_arg_value
|
|
24
|
+
@kw_args ||= @node.arguments.arguments.detect{ |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
25
|
+
return unless @kw_args
|
|
26
|
+
|
|
27
|
+
@from_param ||= @kw_args.elements.detect{ _1.key.unescaped == FROM_ARG_KEY }
|
|
28
|
+
return unless @from_param
|
|
29
|
+
|
|
30
|
+
@from_param.value.unescaped
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def name_arg_node
|
|
34
|
+
@node.arguments.arguments.first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def link_type
|
|
38
|
+
return @link_type if @link_type
|
|
39
|
+
|
|
40
|
+
@link_type = case name_arg_node
|
|
41
|
+
when Prism::SymbolNode
|
|
42
|
+
:object_name
|
|
43
|
+
when Prism::StringNode
|
|
44
|
+
:file_path
|
|
45
|
+
else
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def file_path_type?
|
|
51
|
+
link_type == :file_path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def object_name_type?
|
|
55
|
+
link_type == :object_name
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_name
|
|
59
|
+
case name_arg_node
|
|
60
|
+
when Prism::SymbolNode
|
|
61
|
+
name_arg_node.value
|
|
62
|
+
when Prism::StringNode
|
|
63
|
+
name_arg_node.unescaped
|
|
64
|
+
else
|
|
65
|
+
""
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_imports
|
|
70
|
+
@imports ||= get_imports
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def get_imports
|
|
76
|
+
return [] if @node.arguments.arguments.size == 1
|
|
77
|
+
|
|
78
|
+
last_arg = @node.arguments.arguments.last
|
|
79
|
+
|
|
80
|
+
if last_arg.is_a?(Prism::KeywordHashNode)
|
|
81
|
+
import_arg = last_arg.elements.detect{ _1.key.unescaped == IMPORT_ARG_KEY }
|
|
82
|
+
return [] unless import_arg
|
|
83
|
+
|
|
84
|
+
[import_arg.value.body.body.first.name.to_s]
|
|
85
|
+
elsif last_arg.is_a?(Prism::LambdaNode)
|
|
86
|
+
[last_arg.body.body.first.name.to_s]
|
|
87
|
+
else
|
|
88
|
+
return []
|
|
89
|
+
end
|
|
90
|
+
rescue => e
|
|
91
|
+
$stderr.puts("can't parse imports: #{e.message}")
|
|
92
|
+
return []
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -3,40 +3,31 @@ module RubyLsp
|
|
|
3
3
|
class ReeFormatter
|
|
4
4
|
include RubyLsp::Requests::Support::Formatter
|
|
5
5
|
include RubyLsp::Ree::ReeLspUtils
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
def initialize
|
|
8
8
|
end
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
def run_formatting(uri, document)
|
|
11
|
-
$stderr.puts("run_formating")
|
|
12
|
-
|
|
13
11
|
source = document.source
|
|
14
12
|
sort_links(source)
|
|
15
13
|
end
|
|
16
|
-
|
|
14
|
+
|
|
17
15
|
private
|
|
18
|
-
|
|
16
|
+
|
|
19
17
|
def sort_links(source)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if doc_info.link_nodes.size < doc_info.block_node.body.body.size
|
|
26
|
-
$stderr.puts("block contains not only link, don't sort")
|
|
27
|
-
return source
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
if doc_info.link_nodes.any?{ _1.location.start_line != _1.location.end_line }
|
|
18
|
+
parsed_doc = RubyLsp::Ree::ParsedDocumentBuilder.build_from_source(source)
|
|
19
|
+
return source if !parsed_doc.link_nodes&.any?
|
|
20
|
+
|
|
21
|
+
if parsed_doc.link_nodes.any?{ _1.location.start_line != _1.location.end_line }
|
|
31
22
|
$stderr.puts("multiline link definitions, don't sort")
|
|
32
23
|
return source
|
|
33
24
|
end
|
|
34
|
-
|
|
25
|
+
|
|
35
26
|
# sort link nodes
|
|
36
|
-
sorted_link_nodes =
|
|
37
|
-
a_name = a.arguments.arguments.first
|
|
38
|
-
b_name = b.arguments.arguments.first
|
|
39
|
-
|
|
27
|
+
sorted_link_nodes = parsed_doc.link_nodes.sort{ |a, b|
|
|
28
|
+
a_name = a.node.arguments.arguments.first
|
|
29
|
+
b_name = b.node.arguments.arguments.first
|
|
30
|
+
|
|
40
31
|
if a_name.is_a?(Prism::SymbolNode) && !b_name.is_a?(Prism::SymbolNode)
|
|
41
32
|
-1
|
|
42
33
|
elsif b_name.is_a?(Prism::SymbolNode) && !a_name.is_a?(Prism::SymbolNode)
|
|
@@ -45,25 +36,25 @@ module RubyLsp
|
|
|
45
36
|
a_name.unescaped <=> b_name.unescaped
|
|
46
37
|
end
|
|
47
38
|
}
|
|
48
|
-
|
|
39
|
+
|
|
49
40
|
# check if no re-order
|
|
50
|
-
if
|
|
41
|
+
if parsed_doc.link_nodes.map{ _1.node.arguments.arguments.first.unescaped } == sorted_link_nodes.map{ _1.node.arguments.arguments.first.unescaped }
|
|
51
42
|
return source
|
|
52
43
|
end
|
|
53
|
-
|
|
44
|
+
|
|
54
45
|
# insert nodes to source
|
|
55
|
-
link_lines =
|
|
56
|
-
|
|
46
|
+
link_lines = parsed_doc.link_nodes.map{ _1.location.start_line }
|
|
47
|
+
|
|
57
48
|
source_lines = source.lines
|
|
58
|
-
|
|
49
|
+
|
|
59
50
|
sorted_lines = sorted_link_nodes.map do |sorted_link|
|
|
60
51
|
source_lines[sorted_link.location.start_line - 1]
|
|
61
52
|
end
|
|
62
|
-
|
|
53
|
+
|
|
63
54
|
link_lines.each_with_index do |link_line, index|
|
|
64
55
|
source_lines[link_line - 1] = sorted_lines[index]
|
|
65
56
|
end
|
|
66
|
-
|
|
57
|
+
|
|
67
58
|
source_lines.join()
|
|
68
59
|
end
|
|
69
60
|
end
|
|
@@ -1,42 +1,37 @@
|
|
|
1
|
+
require 'prism'
|
|
2
|
+
require_relative "ree_lsp_utils"
|
|
3
|
+
|
|
1
4
|
module RubyLsp
|
|
2
5
|
module Ree
|
|
3
6
|
class ReeIndexingEnhancement < RubyIndexer::Enhancement
|
|
7
|
+
include RubyLsp::Ree::ReeLspUtils
|
|
8
|
+
|
|
9
|
+
REE_INDEXED_OBJECTS = [:fn, :enum, :action, :dao, :bean]
|
|
10
|
+
|
|
4
11
|
def on_call_node_enter(node)
|
|
5
12
|
return unless @listener.current_owner
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
return unless node.name == :fn
|
|
14
|
+
return unless REE_INDEXED_OBJECTS.include?(node.name)
|
|
9
15
|
return unless node.arguments
|
|
16
|
+
return unless node.arguments.child_nodes.first.is_a?(Prism::SymbolNode)
|
|
17
|
+
|
|
18
|
+
obj_name = node.arguments.child_nodes.first.unescaped
|
|
19
|
+
return unless current_filename == obj_name
|
|
10
20
|
|
|
11
|
-
# index = @listener.instance_variable_get(:@index)
|
|
12
|
-
|
|
13
21
|
location = node.location
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
comments = "ree_object\nsome_documentation"
|
|
17
|
-
|
|
18
|
-
# visibility = RubyIndexer::Entry::Visibility::PUBLIC
|
|
19
|
-
# owner = index['Object'].first
|
|
20
|
-
|
|
21
|
-
# index.add(RubyIndexer::Entry::Method.new(
|
|
22
|
-
# fn_name,
|
|
23
|
-
# @listener.instance_variable_get(:@uri),
|
|
24
|
-
# location,
|
|
25
|
-
# location,
|
|
26
|
-
# comments,
|
|
27
|
-
# signatures,
|
|
28
|
-
# visibility,
|
|
29
|
-
# owner,
|
|
30
|
-
# ))
|
|
22
|
+
signatures = parse_signatures(obj_name)
|
|
23
|
+
comments = "ree_object\ntype: :#{node.name}"
|
|
31
24
|
|
|
32
25
|
@listener.add_method(
|
|
33
|
-
|
|
26
|
+
obj_name,
|
|
34
27
|
location,
|
|
35
28
|
signatures,
|
|
36
29
|
comments: comments
|
|
37
30
|
)
|
|
38
31
|
end
|
|
39
32
|
|
|
33
|
+
private
|
|
34
|
+
|
|
40
35
|
def parse_signatures(fn_name)
|
|
41
36
|
uri = @listener.instance_variable_get(:@uri)
|
|
42
37
|
ast = Prism.parse_file(uri.path).value
|
|
@@ -44,13 +39,18 @@ module RubyLsp
|
|
|
44
39
|
class_node = ast.statements.body.detect{ |node| node.is_a?(Prism::ClassNode) }
|
|
45
40
|
return [] unless class_node
|
|
46
41
|
|
|
47
|
-
call_node = class_node.body.body.detect{ |node| node.name == :call }
|
|
42
|
+
call_node = class_node.body.body.detect{ |node| node.respond_to?(:name) && node.name == :call }
|
|
48
43
|
return [] unless call_node
|
|
49
|
-
|
|
50
|
-
signature_params =
|
|
44
|
+
|
|
45
|
+
signature_params = signature_params_from_node(call_node.parameters)
|
|
51
46
|
|
|
52
47
|
[RubyIndexer::Entry::Signature.new(signature_params)]
|
|
53
48
|
end
|
|
49
|
+
|
|
50
|
+
def current_filename
|
|
51
|
+
uri = @listener.instance_variable_get(:@uri)
|
|
52
|
+
File.basename(uri.path, '.rb')
|
|
53
|
+
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|