parlour 3.0.0 → 5.0.0.beta.3
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/ISSUE_TEMPLATE/bug-report.md +0 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- data/.gitignore +1 -1
- data/.parlour +5 -0
- data/.rspec +0 -0
- data/.travis.yml +0 -0
- data/CHANGELOG.md +57 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +233 -19
- data/Rakefile +0 -0
- data/exe/parlour +109 -4
- data/lib/parlour.rb +27 -1
- data/lib/parlour/conflict_resolver.rb +31 -9
- data/lib/parlour/conversion/converter.rb +34 -0
- data/lib/parlour/conversion/rbi_to_rbs.rb +223 -0
- data/lib/parlour/debugging.rb +0 -0
- data/lib/parlour/detached_rbi_generator.rb +1 -6
- data/lib/parlour/detached_rbs_generator.rb +25 -0
- data/lib/parlour/generator.rb +34 -0
- data/lib/parlour/kernel_hack.rb +0 -0
- data/lib/parlour/options.rb +71 -0
- data/lib/parlour/parse_error.rb +0 -0
- data/lib/parlour/plugin.rb +1 -1
- data/lib/parlour/rbi_generator.rb +24 -37
- data/lib/parlour/rbi_generator/arbitrary.rb +5 -2
- data/lib/parlour/rbi_generator/attribute.rb +14 -5
- data/lib/parlour/rbi_generator/class_namespace.rb +8 -3
- data/lib/parlour/rbi_generator/constant.rb +28 -8
- data/lib/parlour/rbi_generator/enum_class_namespace.rb +8 -3
- data/lib/parlour/rbi_generator/extend.rb +5 -2
- data/lib/parlour/rbi_generator/include.rb +5 -2
- data/lib/parlour/rbi_generator/method.rb +15 -10
- data/lib/parlour/rbi_generator/module_namespace.rb +7 -2
- data/lib/parlour/rbi_generator/namespace.rb +68 -18
- data/lib/parlour/rbi_generator/parameter.rb +13 -7
- data/lib/parlour/rbi_generator/rbi_object.rb +19 -78
- data/lib/parlour/rbi_generator/struct_class_namespace.rb +9 -2
- data/lib/parlour/rbi_generator/struct_prop.rb +12 -9
- data/lib/parlour/rbi_generator/type_alias.rb +101 -0
- data/lib/parlour/rbs_generator.rb +24 -0
- data/lib/parlour/rbs_generator/arbitrary.rb +92 -0
- data/lib/parlour/rbs_generator/attribute.rb +82 -0
- data/lib/parlour/rbs_generator/block.rb +49 -0
- data/lib/parlour/rbs_generator/class_namespace.rb +106 -0
- data/lib/parlour/rbs_generator/constant.rb +95 -0
- data/lib/parlour/rbs_generator/extend.rb +92 -0
- data/lib/parlour/rbs_generator/include.rb +92 -0
- data/lib/parlour/rbs_generator/interface_namespace.rb +34 -0
- data/lib/parlour/rbs_generator/method.rb +146 -0
- data/lib/parlour/rbs_generator/method_signature.rb +104 -0
- data/lib/parlour/rbs_generator/module_namespace.rb +35 -0
- data/lib/parlour/rbs_generator/namespace.rb +627 -0
- data/lib/parlour/rbs_generator/parameter.rb +146 -0
- data/lib/parlour/rbs_generator/rbs_object.rb +78 -0
- data/lib/parlour/rbs_generator/type_alias.rb +96 -0
- data/lib/parlour/type_loader.rb +25 -12
- data/lib/parlour/type_parser.rb +174 -17
- data/lib/parlour/typed_object.rb +87 -0
- data/lib/parlour/types.rb +539 -0
- data/lib/parlour/version.rb +1 -1
- data/parlour.gemspec +1 -1
- data/plugin_examples/foobar_plugin.rb +0 -0
- data/rbi/parlour.rbi +1856 -0
- metadata +35 -10
- data/lib/parlour/rbi_generator/options.rb +0 -74
@@ -0,0 +1,34 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
class Generator
|
4
|
+
extend T::Sig
|
5
|
+
|
6
|
+
sig { params(break_params: Integer, tab_size: Integer, sort_namespaces: T::Boolean).void }
|
7
|
+
# Creates a new generator.
|
8
|
+
#
|
9
|
+
# @param break_params [Integer] If there are at least this many parameters in a
|
10
|
+
# signature, then it is broken onto separate lines.
|
11
|
+
# @param tab_size [Integer] The number of spaces to use per indent.
|
12
|
+
# @param sort_namespaces [Boolean] Whether to sort all items within a
|
13
|
+
# namespace alphabetically.
|
14
|
+
# @return [void]
|
15
|
+
def initialize(break_params: 4, tab_size: 2, sort_namespaces: false)
|
16
|
+
@options = Options.new(
|
17
|
+
break_params: break_params,
|
18
|
+
tab_size: tab_size,
|
19
|
+
sort_namespaces: sort_namespaces
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { overridable.returns(Options) }
|
24
|
+
# The formatting options for this generator. Currently ignored.
|
25
|
+
# @return [Options]
|
26
|
+
attr_reader :options
|
27
|
+
|
28
|
+
sig { overridable.returns(T.nilable(Plugin)) }
|
29
|
+
# The plugin which is currently generating new definitions.
|
30
|
+
# {Plugin#run_plugins} controls this value.
|
31
|
+
# @return [Plugin, nil]
|
32
|
+
attr_accessor :current_plugin
|
33
|
+
end
|
34
|
+
end
|
data/lib/parlour/kernel_hack.rb
CHANGED
File without changes
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Parlour
|
3
|
+
# A set of immutable formatting options.
|
4
|
+
class Options
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(break_params: Integer, tab_size: Integer, sort_namespaces: T::Boolean).void }
|
8
|
+
# Creates a new set of formatting options.
|
9
|
+
#
|
10
|
+
# @example Create Options with +break_params+ of +4+ and +tab_size+ of +2+.
|
11
|
+
# Parlour::Options.new(break_params: 4, tab_size: 2)
|
12
|
+
#
|
13
|
+
# @param break_params [Integer] If there are at least this many parameters in a
|
14
|
+
# signature, then it is broken onto separate lines.
|
15
|
+
# @param tab_size [Integer] The number of spaces to use per indent.
|
16
|
+
# @param sort_namespaces [Boolean] Whether to sort all items within a
|
17
|
+
# namespace alphabetically.
|
18
|
+
# @return [void]
|
19
|
+
def initialize(break_params:, tab_size:, sort_namespaces:)
|
20
|
+
@break_params = break_params
|
21
|
+
@tab_size = tab_size
|
22
|
+
@sort_namespaces = sort_namespaces
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(Integer) }
|
26
|
+
# If there are at least this many parameters in a signature, then it
|
27
|
+
# is broken onto separate lines.
|
28
|
+
#
|
29
|
+
# # With break_params: 5
|
30
|
+
# sig { params(name: String, age: Integer, hobbies: T::Array(String), country: Symbol).void }
|
31
|
+
#
|
32
|
+
# # With break_params: 4
|
33
|
+
# sig do
|
34
|
+
# params(
|
35
|
+
# name: String,
|
36
|
+
# age: Integer,
|
37
|
+
# hobbies: T::Array(String),
|
38
|
+
# country: Symbol
|
39
|
+
# ).void
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @return [Integer]
|
43
|
+
attr_reader :break_params
|
44
|
+
|
45
|
+
sig { returns(Integer) }
|
46
|
+
# The number of spaces to use per indent.
|
47
|
+
# @return [Integer]
|
48
|
+
attr_reader :tab_size
|
49
|
+
|
50
|
+
sig { returns(T::Boolean) }
|
51
|
+
# Whether to sort all items within a namespace alphabetically.
|
52
|
+
# Items which are typically grouped together, such as "include" or
|
53
|
+
# "extend" calls, will remain grouped together when sorted.
|
54
|
+
# If true, items are sorted by their name when the RBI is generated.
|
55
|
+
# If false, items are generated in the order they are added to the
|
56
|
+
# namespace.
|
57
|
+
# @return [Boolean]
|
58
|
+
attr_reader :sort_namespaces
|
59
|
+
|
60
|
+
sig { params(level: Integer, str: String).returns(String) }
|
61
|
+
# Returns a string indented to the given indent level, according to the
|
62
|
+
# set {tab_size}.
|
63
|
+
#
|
64
|
+
# @param level [Integer] The indent level, as an integer. 0 is totally unindented.
|
65
|
+
# @param str [String] The string to indent.
|
66
|
+
# @return [String] The indented string.
|
67
|
+
def indented(level, str)
|
68
|
+
" " * (level * tab_size) + str
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/parlour/parse_error.rb
CHANGED
File without changes
|
data/lib/parlour/plugin.rb
CHANGED
@@ -1,55 +1,42 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
3
|
# The RBI generator.
|
4
|
-
class RbiGenerator
|
5
|
-
|
4
|
+
class RbiGenerator < Generator
|
5
|
+
# For backwards compatibility.
|
6
|
+
# Before Parlour 5.0, Parlour::Options was Parlour::RbiGenerator::Options.
|
7
|
+
Options = Parlour::Options
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# @example Create a default generator.
|
11
|
-
# generator = Parlour::RbiGenerator.new
|
12
|
-
#
|
13
|
-
# @example Create a generator with a custom +tab_size+ of 3.
|
14
|
-
# generator = Parlour::RbiGenerator.new(tab_size: 3)
|
15
|
-
#
|
16
|
-
# @param break_params [Integer] If there are at least this many parameters in a
|
17
|
-
# Sorbet +sig+, then it is broken onto separate lines.
|
18
|
-
# @param tab_size [Integer] The number of spaces to use per indent.
|
19
|
-
# @param sort_namespaces [Boolean] Whether to sort all items within a
|
20
|
-
# namespace alphabetically.
|
21
|
-
# @return [void]
|
22
|
-
def initialize(break_params: 4, tab_size: 2, sort_namespaces: false)
|
23
|
-
@options = Options.new(
|
24
|
-
break_params: break_params,
|
25
|
-
tab_size: tab_size,
|
26
|
-
sort_namespaces: sort_namespaces
|
27
|
-
)
|
28
|
-
@root = Namespace.new(self)
|
9
|
+
def initialize(**hash)
|
10
|
+
super
|
11
|
+
@root = RbiGenerator::Namespace.new(self)
|
29
12
|
end
|
30
13
|
|
31
|
-
sig { overridable.returns(
|
32
|
-
# The formatting options for this generator.
|
33
|
-
# @return [Options]
|
34
|
-
attr_reader :options
|
35
|
-
|
36
|
-
sig { overridable.returns(Namespace) }
|
14
|
+
sig { overridable.returns(RbiGenerator::Namespace) }
|
37
15
|
# The root {Namespace} of this generator.
|
38
16
|
# @return [Namespace]
|
39
17
|
attr_reader :root
|
40
18
|
|
41
|
-
sig { overridable.returns(T.nilable(Plugin)) }
|
42
|
-
# The plugin which is currently generating new definitions.
|
43
|
-
# {Plugin#run_plugins} controls this value.
|
44
|
-
# @return [Plugin, nil]
|
45
|
-
attr_accessor :current_plugin
|
46
|
-
|
47
19
|
sig { overridable.params(strictness: String).returns(String) }
|
48
20
|
# Returns the complete contents of the generated RBI file as a string.
|
49
21
|
#
|
50
22
|
# @return [String] The generated RBI file
|
51
23
|
def rbi(strictness = 'strong')
|
52
|
-
|
24
|
+
# TODO: Early test option - convert to RBS if requested
|
25
|
+
# Absolutely remove this later on
|
26
|
+
if ENV['PARLOUR_CONVERT_TO_RBS']
|
27
|
+
# Perform conversion
|
28
|
+
root.generalize_from_rbi!
|
29
|
+
rbs_gen = Parlour::RbsGenerator.new
|
30
|
+
converter = Parlour::Conversion::RbiToRbs.new(rbs_gen)
|
31
|
+
root.children.each do |child|
|
32
|
+
converter.convert_object(child, rbs_gen.root)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Write the final RBS
|
36
|
+
rbs_gen.rbs
|
37
|
+
else
|
38
|
+
"# typed: #{strictness}\n" + root.generate_rbi(0, options).join("\n") + "\n"
|
39
|
+
end
|
53
40
|
end
|
54
41
|
end
|
55
42
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
|
-
class RbiGenerator
|
3
|
+
class RbiGenerator < Generator
|
4
4
|
# Represents miscellaneous Ruby code.
|
5
5
|
class Arbitrary < RbiObject
|
6
6
|
sig do
|
7
7
|
params(
|
8
|
-
generator:
|
8
|
+
generator: Generator,
|
9
9
|
code: String,
|
10
10
|
block: T.nilable(T.proc.params(x: Arbitrary).void)
|
11
11
|
).void
|
@@ -87,6 +87,9 @@ module Parlour
|
|
87
87
|
def describe
|
88
88
|
"Arbitrary code (#{code})"
|
89
89
|
end
|
90
|
+
|
91
|
+
sig { override.void }
|
92
|
+
def generalize_from_rbi!; end # Nothing to do
|
90
93
|
end
|
91
94
|
end
|
92
95
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
|
-
class RbiGenerator
|
3
|
+
class RbiGenerator < Generator
|
4
4
|
# Represents an attribute reader, writer or accessor.
|
5
5
|
class Attribute < Method
|
6
6
|
sig do
|
7
7
|
params(
|
8
|
-
generator:
|
8
|
+
generator: Generator,
|
9
9
|
name: String,
|
10
10
|
kind: Symbol,
|
11
|
-
type:
|
11
|
+
type: Types::TypeLike,
|
12
12
|
class_attribute: T::Boolean,
|
13
13
|
block: T.nilable(T.proc.params(x: Attribute).void)
|
14
14
|
).void
|
@@ -20,8 +20,7 @@ module Parlour
|
|
20
20
|
# @param name [String] The name of this attribute.
|
21
21
|
# @param kind [Symbol] The kind of attribute this is; one of :writer, :reader or
|
22
22
|
# :accessor.
|
23
|
-
# @param type [String]
|
24
|
-
# +"String"+ or +"T.untyped"+.
|
23
|
+
# @param type [String, Types::Type] This attribute's type.
|
25
24
|
# @param class_attribute [Boolean] Whether this attribute belongs to the
|
26
25
|
# singleton class.
|
27
26
|
# @param block A block which the new instance yields itself to.
|
@@ -32,6 +31,7 @@ module Parlour
|
|
32
31
|
# attr_accessor and attr_reader should have: sig { returns(X) }
|
33
32
|
# attr_writer :foo should have: sig { params(foo: X).returns(X) }
|
34
33
|
|
34
|
+
@type = type
|
35
35
|
@kind = kind
|
36
36
|
@class_attribute = class_attribute
|
37
37
|
case kind
|
@@ -55,6 +55,10 @@ module Parlour
|
|
55
55
|
# Whether this attribute belongs to the singleton class.
|
56
56
|
attr_reader :class_attribute
|
57
57
|
|
58
|
+
sig { returns(Types::TypeLike) }
|
59
|
+
# The type of this attribute.
|
60
|
+
attr_reader :type
|
61
|
+
|
58
62
|
sig { override.params(other: Object).returns(T::Boolean) }
|
59
63
|
# Returns true if this instance is equal to another attribute.
|
60
64
|
#
|
@@ -69,6 +73,11 @@ module Parlour
|
|
69
73
|
)
|
70
74
|
end
|
71
75
|
|
76
|
+
sig { override.void }
|
77
|
+
def generalize_from_rbi!
|
78
|
+
@type = TypeParser.parse_single_type(@type) if String === @type
|
79
|
+
end
|
80
|
+
|
72
81
|
private
|
73
82
|
|
74
83
|
sig do
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
|
-
class RbiGenerator
|
3
|
+
class RbiGenerator < Generator
|
4
4
|
# Represents a class definition.
|
5
5
|
class ClassNamespace < Namespace
|
6
6
|
extend T::Sig
|
7
7
|
|
8
8
|
sig do
|
9
9
|
params(
|
10
|
-
generator:
|
10
|
+
generator: Generator,
|
11
11
|
name: String,
|
12
12
|
final: T::Boolean,
|
13
13
|
superclass: T.nilable(String),
|
@@ -47,7 +47,7 @@ module Parlour
|
|
47
47
|
class_definition = superclass.nil? \
|
48
48
|
? "class #{name}"
|
49
49
|
: "class #{name} < #{superclass}"
|
50
|
-
|
50
|
+
|
51
51
|
lines = generate_comments(indent_level, options)
|
52
52
|
lines << options.indented(indent_level, class_definition)
|
53
53
|
lines += [options.indented(indent_level + 1, "abstract!"), ""] if abstract
|
@@ -116,6 +116,11 @@ module Parlour
|
|
116
116
|
"#{"abstract, " if abstract}#{children.length} children, " +
|
117
117
|
"#{includes.length} includes, #{extends.length} extends"
|
118
118
|
end
|
119
|
+
|
120
|
+
sig { override.void }
|
121
|
+
def generalize_from_rbi!
|
122
|
+
super
|
123
|
+
end
|
119
124
|
end
|
120
125
|
end
|
121
126
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
|
-
class RbiGenerator
|
3
|
+
class RbiGenerator < Generator
|
4
4
|
# Represents a constant definition.
|
5
5
|
class Constant < RbiObject
|
6
6
|
sig do
|
7
7
|
params(
|
8
|
-
generator:
|
8
|
+
generator: Generator,
|
9
9
|
name: String,
|
10
|
-
value:
|
10
|
+
value: Types::TypeLike,
|
11
|
+
eigen_constant: T::Boolean,
|
11
12
|
block: T.nilable(T.proc.params(x: Constant).void)
|
12
13
|
).void
|
13
14
|
end
|
@@ -15,16 +16,23 @@ module Parlour
|
|
15
16
|
#
|
16
17
|
# @param name [String] The name of the constant.
|
17
18
|
# @param value [String] The value of the constant, as a Ruby code string.
|
18
|
-
|
19
|
+
# @param eigen_constant [Boolean] Whether this constant is defined on the
|
20
|
+
# eigenclass of the current namespace.
|
21
|
+
def initialize(generator, name: '', value: '', eigen_constant: false, &block)
|
19
22
|
super(generator, name)
|
20
23
|
@value = value
|
24
|
+
@eigen_constant = eigen_constant
|
21
25
|
yield_self(&block) if block
|
22
26
|
end
|
23
27
|
|
24
|
-
# @return [String] The value of the constant
|
25
|
-
sig { returns(
|
28
|
+
# @return [String] The value or type of the constant.
|
29
|
+
sig { returns(Types::TypeLike) }
|
26
30
|
attr_reader :value
|
27
31
|
|
32
|
+
# @return [Boolean] Whether this constant is defined on the eigenclass
|
33
|
+
# of the current namespace.
|
34
|
+
attr_reader :eigen_constant
|
35
|
+
|
28
36
|
sig { params(other: Object).returns(T::Boolean) }
|
29
37
|
# Returns true if this instance is equal to another extend.
|
30
38
|
#
|
@@ -32,7 +40,8 @@ module Parlour
|
|
32
40
|
# subclass of it), this will always return false.
|
33
41
|
# @return [Boolean]
|
34
42
|
def ==(other)
|
35
|
-
Constant === other && name == other.name && value == other.value
|
43
|
+
Constant === other && name == other.name && value == other.value \
|
44
|
+
&& eigen_constant == other.eigen_constant
|
36
45
|
end
|
37
46
|
|
38
47
|
sig do
|
@@ -47,7 +56,11 @@ module Parlour
|
|
47
56
|
# @param options [Options] The formatting options to use.
|
48
57
|
# @return [Array<String>] The RBI lines, formatted as specified.
|
49
58
|
def generate_rbi(indent_level, options)
|
50
|
-
|
59
|
+
if String === @value
|
60
|
+
[options.indented(indent_level, "#{name} = #{@value}")]
|
61
|
+
else
|
62
|
+
[options.indented(indent_level, "#{name} = T.let(nil, #{@value.generate_rbi})")]
|
63
|
+
end
|
51
64
|
end
|
52
65
|
|
53
66
|
sig do
|
@@ -89,6 +102,13 @@ module Parlour
|
|
89
102
|
def describe
|
90
103
|
"Constant (#{name} = #{value})"
|
91
104
|
end
|
105
|
+
|
106
|
+
sig { override.void }
|
107
|
+
def generalize_from_rbi!
|
108
|
+
# There's a good change this is an untyped constant, so rescue
|
109
|
+
# ParseError and use untyped
|
110
|
+
@value = (TypeParser.parse_single_type(@value) if String === @value) rescue Types::Untyped.new
|
111
|
+
end
|
92
112
|
end
|
93
113
|
end
|
94
114
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
|
-
class RbiGenerator
|
3
|
+
class RbiGenerator < Generator
|
4
4
|
# Represents an enum definition; that is, a class with an +enum+ call.
|
5
5
|
class EnumClassNamespace < ClassNamespace
|
6
6
|
extend T::Sig
|
7
7
|
|
8
8
|
sig do
|
9
9
|
params(
|
10
|
-
generator:
|
10
|
+
generator: Generator,
|
11
11
|
name: String,
|
12
12
|
final: T::Boolean,
|
13
13
|
enums: T::Array[T.any([String, String], String)],
|
@@ -38,7 +38,7 @@ module Parlour
|
|
38
38
|
sig do
|
39
39
|
override.params(
|
40
40
|
indent_level: Integer,
|
41
|
-
options: Options
|
41
|
+
options: Options,
|
42
42
|
).returns(T::Array[String])
|
43
43
|
end
|
44
44
|
# Generates the RBI lines for the body of this enum. This consists of
|
@@ -107,6 +107,11 @@ module Parlour
|
|
107
107
|
@enums = other.enums if enums.empty?
|
108
108
|
end
|
109
109
|
end
|
110
|
+
|
111
|
+
sig { override.void }
|
112
|
+
def generalize_from_rbi!
|
113
|
+
super
|
114
|
+
end
|
110
115
|
end
|
111
116
|
end
|
112
117
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# typed: true
|
2
2
|
module Parlour
|
3
|
-
class RbiGenerator
|
3
|
+
class RbiGenerator < Generator
|
4
4
|
# Represents an +extend+ call.
|
5
5
|
class Extend < RbiObject
|
6
6
|
sig do
|
7
7
|
params(
|
8
|
-
generator:
|
8
|
+
generator: Generator,
|
9
9
|
name: String,
|
10
10
|
block: T.nilable(T.proc.params(x: Extend).void)
|
11
11
|
).void
|
@@ -82,6 +82,9 @@ module Parlour
|
|
82
82
|
def describe
|
83
83
|
"Extend (#{name})"
|
84
84
|
end
|
85
|
+
|
86
|
+
sig { override.void }
|
87
|
+
def generalize_from_rbi!; end # Nothing to do
|
85
88
|
end
|
86
89
|
end
|
87
90
|
end
|