steep 0.1.0.pre2 → 0.1.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 +5 -5
- data/.travis.yml +1 -1
- data/README.md +146 -33
- data/bin/smoke_runner.rb +43 -10
- data/lib/steep/ast/annotation/collection.rb +93 -0
- data/lib/steep/ast/annotation.rb +131 -0
- data/lib/steep/ast/buffer.rb +47 -0
- data/lib/steep/ast/location.rb +82 -0
- data/lib/steep/ast/method_type.rb +116 -0
- data/lib/steep/ast/signature/class.rb +33 -0
- data/lib/steep/ast/signature/const.rb +17 -0
- data/lib/steep/ast/signature/env.rb +123 -0
- data/lib/steep/ast/signature/extension.rb +21 -0
- data/lib/steep/ast/signature/gvar.rb +17 -0
- data/lib/steep/ast/signature/interface.rb +31 -0
- data/lib/steep/ast/signature/members.rb +71 -0
- data/lib/steep/ast/signature/module.rb +21 -0
- data/lib/steep/ast/type_params.rb +13 -0
- data/lib/steep/ast/types/any.rb +39 -0
- data/lib/steep/ast/types/bot.rb +39 -0
- data/lib/steep/ast/types/class.rb +35 -0
- data/lib/steep/ast/types/helper.rb +21 -0
- data/lib/steep/ast/types/instance.rb +39 -0
- data/lib/steep/ast/types/intersection.rb +74 -0
- data/lib/steep/ast/types/name.rb +124 -0
- data/lib/steep/ast/types/self.rb +39 -0
- data/lib/steep/ast/types/top.rb +39 -0
- data/lib/steep/ast/types/union.rb +74 -0
- data/lib/steep/ast/types/var.rb +57 -0
- data/lib/steep/ast/types/void.rb +35 -0
- data/lib/steep/cli.rb +28 -1
- data/lib/steep/drivers/annotations.rb +32 -0
- data/lib/steep/drivers/check.rb +53 -77
- data/lib/steep/drivers/scaffold.rb +303 -0
- data/lib/steep/drivers/utils/each_signature.rb +66 -0
- data/lib/steep/drivers/utils/validator.rb +115 -0
- data/lib/steep/drivers/validate.rb +39 -0
- data/lib/steep/errors.rb +291 -19
- data/lib/steep/interface/abstract.rb +44 -0
- data/lib/steep/interface/builder.rb +470 -0
- data/lib/steep/interface/instantiated.rb +126 -0
- data/lib/steep/interface/ivar_chain.rb +26 -0
- data/lib/steep/interface/method.rb +60 -0
- data/lib/steep/{interface.rb → interface/method_type.rb} +111 -100
- data/lib/steep/interface/substitution.rb +65 -0
- data/lib/steep/module_name.rb +116 -0
- data/lib/steep/parser.rb +1314 -814
- data/lib/steep/parser.y +536 -175
- data/lib/steep/source.rb +220 -25
- data/lib/steep/subtyping/check.rb +673 -0
- data/lib/steep/subtyping/constraints.rb +275 -0
- data/lib/steep/subtyping/relation.rb +41 -0
- data/lib/steep/subtyping/result.rb +126 -0
- data/lib/steep/subtyping/trace.rb +48 -0
- data/lib/steep/subtyping/variable_occurrence.rb +49 -0
- data/lib/steep/subtyping/variable_variance.rb +69 -0
- data/lib/steep/type_construction.rb +1630 -524
- data/lib/steep/type_inference/block_params.rb +100 -0
- data/lib/steep/type_inference/constant_env.rb +55 -0
- data/lib/steep/type_inference/send_args.rb +222 -0
- data/lib/steep/type_inference/type_env.rb +226 -0
- data/lib/steep/type_name.rb +27 -7
- data/lib/steep/typing.rb +4 -0
- data/lib/steep/version.rb +1 -1
- data/lib/steep.rb +71 -16
- data/smoke/and/a.rb +4 -2
- data/smoke/array/a.rb +4 -5
- data/smoke/array/b.rb +4 -4
- data/smoke/block/a.rb +2 -2
- data/smoke/block/a.rbi +2 -0
- data/smoke/block/b.rb +15 -0
- data/smoke/case/a.rb +3 -3
- data/smoke/class/a.rb +3 -3
- data/smoke/class/b.rb +0 -2
- data/smoke/class/d.rb +2 -2
- data/smoke/class/e.rb +1 -1
- data/smoke/class/f.rb +2 -2
- data/smoke/class/g.rb +8 -0
- data/smoke/const/a.rb +3 -3
- data/smoke/dstr/a.rb +1 -1
- data/smoke/ensure/a.rb +22 -0
- data/smoke/enumerator/a.rb +6 -6
- data/smoke/enumerator/b.rb +22 -0
- data/smoke/extension/a.rb +2 -2
- data/smoke/extension/b.rb +3 -3
- data/smoke/extension/c.rb +1 -1
- data/smoke/hello/hello.rb +2 -2
- data/smoke/if/a.rb +4 -2
- data/smoke/kwbegin/a.rb +8 -0
- data/smoke/literal/a.rb +5 -5
- data/smoke/method/a.rb +5 -5
- data/smoke/method/a.rbi +4 -0
- data/smoke/method/b.rb +29 -0
- data/smoke/module/a.rb +3 -3
- data/smoke/module/a.rbi +9 -0
- data/smoke/module/b.rb +2 -2
- data/smoke/module/c.rb +1 -1
- data/smoke/module/d.rb +5 -0
- data/smoke/module/e.rb +13 -0
- data/smoke/module/f.rb +13 -0
- data/smoke/rescue/a.rb +62 -0
- data/smoke/super/a.rb +2 -2
- data/smoke/type_case/a.rb +35 -0
- data/smoke/yield/a.rb +2 -2
- data/stdlib/builtin.rbi +463 -24
- data/steep.gemspec +3 -2
- metadata +91 -29
- data/lib/steep/annotation.rb +0 -223
- data/lib/steep/signature/class.rb +0 -450
- data/lib/steep/signature/extension.rb +0 -51
- data/lib/steep/signature/interface.rb +0 -49
- data/lib/steep/types/any.rb +0 -31
- data/lib/steep/types/class.rb +0 -27
- data/lib/steep/types/instance.rb +0 -27
- data/lib/steep/types/merge.rb +0 -32
- data/lib/steep/types/name.rb +0 -57
- data/lib/steep/types/union.rb +0 -42
- data/lib/steep/types/var.rb +0 -38
- data/lib/steep/types.rb +0 -4
@@ -1,5 +1,5 @@
|
|
1
1
|
module Steep
|
2
|
-
|
2
|
+
module Interface
|
3
3
|
class Params
|
4
4
|
attr_reader :required
|
5
5
|
attr_reader :optional
|
@@ -17,17 +17,15 @@ module Steep
|
|
17
17
|
@rest_keywords = rest_keywords
|
18
18
|
end
|
19
19
|
|
20
|
-
def with(required: nil, optional: nil, rest: nil, required_keywords: nil, optional_keywords: nil, rest_keywords: nil)
|
21
|
-
self.class.new(required: required || self.required,
|
22
|
-
optional: optional || self.optional,
|
23
|
-
rest: rest || self.rest,
|
24
|
-
required_keywords: required_keywords || self.required_keywords,
|
25
|
-
optional_keywords: optional_keywords || self.optional_keywords,
|
26
|
-
rest_keywords: rest_keywords || self.rest_keywords)
|
27
|
-
end
|
28
|
-
|
29
20
|
def self.empty
|
30
|
-
new(
|
21
|
+
self.new(
|
22
|
+
required: [],
|
23
|
+
optional: [],
|
24
|
+
rest: nil,
|
25
|
+
required_keywords: {},
|
26
|
+
optional_keywords: {},
|
27
|
+
rest_keywords: nil
|
28
|
+
)
|
31
29
|
end
|
32
30
|
|
33
31
|
def ==(other)
|
@@ -138,150 +136,163 @@ module Steep
|
|
138
136
|
end
|
139
137
|
end
|
140
138
|
|
139
|
+
def free_variables
|
140
|
+
Set.new.tap do |fvs|
|
141
|
+
each_type do |type|
|
142
|
+
fvs.merge type.free_variables
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
141
147
|
def closed?
|
142
148
|
required.all?(&:closed?) && optional.all?(&:closed?) && (!rest || rest.closed?) && required_keywords.values.all?(&:closed?) && optional_keywords.values.all?(&:closed?) && (!rest_keywords || rest_keywords.closed?)
|
143
149
|
end
|
144
150
|
|
145
|
-
def
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
151
|
+
def has_keyword?
|
152
|
+
required_keywords.any? || optional_keywords.any? || rest_keywords
|
153
|
+
end
|
154
|
+
|
155
|
+
def subst(s)
|
156
|
+
self.class.new(
|
157
|
+
required: required.map {|t| t.subst(s) },
|
158
|
+
optional: optional.map {|t| t.subst(s) },
|
159
|
+
rest: rest&.subst(s),
|
160
|
+
required_keywords: required_keywords.transform_values {|t| t.subst(s) },
|
161
|
+
optional_keywords: optional_keywords.transform_values {|t| t.subst(s) },
|
162
|
+
rest_keywords: rest_keywords&.subst(s)
|
163
|
+
)
|
152
164
|
end
|
153
165
|
|
154
166
|
def size
|
155
167
|
required.size + optional.size + (rest ? 1 : 0) + required_keywords.size + optional_keywords.size + (rest_keywords ? 1 : 0)
|
156
168
|
end
|
169
|
+
|
170
|
+
def to_s
|
171
|
+
required = self.required.map {|ty| ty.to_s }
|
172
|
+
optional = self.optional.map {|ty| "?#{ty}" }
|
173
|
+
rest = self.rest ? ["*#{self.rest}"] : []
|
174
|
+
required_keywords = self.required_keywords.map {|name, type| "#{name}: #{type}" }
|
175
|
+
optional_keywords = self.optional_keywords.map {|name, type| "?#{name}: #{type}"}
|
176
|
+
rest_keywords = self.rest_keywords ? ["**#{self.rest_keywords}"] : []
|
177
|
+
"(#{(required + optional + rest + required_keywords + optional_keywords + rest_keywords).join(", ")})"
|
178
|
+
end
|
157
179
|
end
|
158
180
|
|
159
|
-
class
|
160
|
-
attr_reader :type_params
|
181
|
+
class Block
|
161
182
|
attr_reader :params
|
162
|
-
attr_reader :block
|
163
183
|
attr_reader :return_type
|
164
184
|
|
165
|
-
|
166
|
-
|
167
|
-
def initialize(type_params:, params:, block:, return_type:)
|
168
|
-
@type_params = type_params
|
185
|
+
def initialize(params:, return_type:)
|
169
186
|
@params = params
|
170
|
-
@block = block
|
171
187
|
@return_type = return_type
|
172
188
|
end
|
173
189
|
|
174
|
-
def updated(type_params: NONE, params: NONE, block: NONE, return_type: NONE)
|
175
|
-
self.class.new(type_params: type_params.equal?(NONE) ? self.type_params : type_params,
|
176
|
-
params: params.equal?(NONE) ? self.params : params,
|
177
|
-
block: block.equal?(NONE) ? self.block : block,
|
178
|
-
return_type: return_type.equal?(NONE) ? self.return_type : return_type)
|
179
|
-
end
|
180
|
-
|
181
190
|
def ==(other)
|
182
|
-
other.is_a?(self.class) &&
|
183
|
-
other.params == params &&
|
184
|
-
other.block == block &&
|
185
|
-
other.return_type == return_type
|
191
|
+
other.is_a?(self.class) && other.params == params && other.return_type == return_type
|
186
192
|
end
|
187
193
|
|
188
194
|
def closed?
|
189
|
-
params.closed? &&
|
195
|
+
params.closed? && return_type.closed?
|
190
196
|
end
|
191
197
|
|
192
|
-
def
|
193
|
-
self.class.new(
|
194
|
-
|
195
|
-
|
196
|
-
|
198
|
+
def subst(s)
|
199
|
+
self.class.new(
|
200
|
+
params: params.subst(s),
|
201
|
+
return_type: return_type.subst(s)
|
202
|
+
)
|
197
203
|
end
|
198
204
|
|
199
|
-
def
|
200
|
-
|
205
|
+
def free_variables
|
206
|
+
params.free_variables + return_type.free_variables
|
207
|
+
end
|
201
208
|
|
202
|
-
|
203
|
-
|
204
|
-
block: block&.substitute(klass: nil, instance: nil, params: subst),
|
205
|
-
return_type: return_type.substitute(klass: nil, instance: nil, params: subst))
|
209
|
+
def to_s
|
210
|
+
"{ #{params} -> #{return_type} }"
|
206
211
|
end
|
207
212
|
end
|
208
213
|
|
209
|
-
class
|
214
|
+
class MethodType
|
215
|
+
attr_reader :type_params
|
210
216
|
attr_reader :params
|
217
|
+
attr_reader :block
|
211
218
|
attr_reader :return_type
|
219
|
+
attr_reader :location
|
212
220
|
|
213
|
-
|
221
|
+
NONE = Object.new
|
222
|
+
|
223
|
+
def initialize(type_params:, params:, block:, return_type:, location:)
|
224
|
+
@type_params = type_params
|
214
225
|
@params = params
|
226
|
+
@block = block
|
215
227
|
@return_type = return_type
|
228
|
+
@location = location
|
216
229
|
end
|
217
230
|
|
218
231
|
def ==(other)
|
219
|
-
other.is_a?(self.class) &&
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
232
|
+
other.is_a?(self.class) &&
|
233
|
+
other.type_params == type_params &&
|
234
|
+
other.params == params &&
|
235
|
+
other.block == block &&
|
236
|
+
other.return_type == return_type &&
|
237
|
+
(!other.location || !location || other.location == location)
|
224
238
|
end
|
225
239
|
|
226
|
-
def
|
227
|
-
|
228
|
-
return_type: return_type.substitute(klass: klass, instance: instance, params: params))
|
240
|
+
def free_variables
|
241
|
+
(params.free_variables + (block&.free_variables || Set.new) + return_type.free_variables) - Set.new(type_params)
|
229
242
|
end
|
230
|
-
end
|
231
|
-
|
232
|
-
class Method
|
233
|
-
attr_reader :super_method
|
234
|
-
attr_reader :types
|
235
|
-
attr_reader :attributes
|
236
243
|
|
237
|
-
def
|
238
|
-
|
239
|
-
@super_method = super_method
|
240
|
-
@attributes = attributes
|
241
|
-
end
|
244
|
+
def subst(s)
|
245
|
+
s_ = s.except(type_params)
|
242
246
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
247
|
+
self.class.new(
|
248
|
+
type_params: type_params,
|
249
|
+
params: params.subst(s_),
|
250
|
+
block: block&.subst(s_),
|
251
|
+
return_type: return_type.subst(s_),
|
252
|
+
location: location
|
253
|
+
)
|
248
254
|
end
|
249
255
|
|
250
|
-
def
|
251
|
-
|
256
|
+
def each_type(&block)
|
257
|
+
if block_given?
|
258
|
+
params.each_type(&block)
|
259
|
+
self.block&.tap do
|
260
|
+
self.block.params.each_type(&block)
|
261
|
+
yield(self.block.return_type)
|
262
|
+
end
|
263
|
+
yield(return_type)
|
264
|
+
else
|
265
|
+
enum_for :each_type
|
266
|
+
end
|
252
267
|
end
|
253
268
|
|
254
|
-
def
|
269
|
+
def instantiate(s)
|
255
270
|
self.class.new(
|
256
|
-
|
257
|
-
|
258
|
-
|
271
|
+
type_params: [],
|
272
|
+
params: params.subst(s),
|
273
|
+
block: block&.subst(s),
|
274
|
+
return_type: return_type.subst(s),
|
275
|
+
location: location,
|
276
|
+
)
|
259
277
|
end
|
260
278
|
|
261
|
-
def
|
279
|
+
def with(type_params: NONE, params: NONE, block: NONE, return_type: NONE, location: NONE)
|
262
280
|
self.class.new(
|
263
|
-
|
264
|
-
|
281
|
+
type_params: type_params.equal?(NONE) ? self.type_params : type_params,
|
282
|
+
params: params.equal?(NONE) ? self.params : params,
|
283
|
+
block: block.equal?(NONE) ? self.block : block,
|
284
|
+
return_type: return_type.equal?(NONE) ? self.return_type : return_type,
|
285
|
+
location: location.equal?(NONE) ? self.location : location
|
265
286
|
)
|
266
287
|
end
|
267
|
-
end
|
268
|
-
|
269
|
-
attr_reader :name
|
270
|
-
attr_reader :params
|
271
|
-
attr_reader :methods
|
272
288
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
end
|
278
|
-
|
279
|
-
def closed?
|
280
|
-
methods.values.all?(&:closed?)
|
281
|
-
end
|
289
|
+
def to_s
|
290
|
+
type_params = !self.type_params.empty? ? "<#{self.type_params.map{|x| "'#{x}" }.join(", ")}> " : ""
|
291
|
+
params = self.params.to_s
|
292
|
+
block = self.block ? " #{self.block}" : ""
|
282
293
|
|
283
|
-
|
284
|
-
|
294
|
+
"#{type_params}#{params}#{block} -> #{return_type}"
|
295
|
+
end
|
285
296
|
end
|
286
297
|
end
|
287
298
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Steep
|
2
|
+
module Interface
|
3
|
+
class Substitution
|
4
|
+
attr_reader :dictionary
|
5
|
+
attr_reader :instance_type
|
6
|
+
attr_reader :module_type
|
7
|
+
attr_reader :self_type
|
8
|
+
|
9
|
+
def initialize(dictionary:, instance_type:, module_type:, self_type:)
|
10
|
+
@dictionary = dictionary
|
11
|
+
@instance_type = instance_type
|
12
|
+
@module_type = module_type
|
13
|
+
@self_type = self_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.empty
|
17
|
+
new(dictionary: {}, instance_type: AST::Types::Instance.new, module_type: AST::Types::Class.new, self_type: AST::Types::Self.new)
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](key)
|
21
|
+
dictionary[key] or raise "Unknown variable: #{key}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def key?(var)
|
25
|
+
dictionary.key?(var)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.build(vars, types = nil, instance_type: AST::Types::Instance.new, module_type: AST::Types::Class.new, self_type: AST::Types::Self.new)
|
29
|
+
types ||= vars.map {|var| AST::Types::Var.fresh(var) }
|
30
|
+
|
31
|
+
raise "Invalid substitution: vars.size=#{vars.size}, types.size=#{types.size}" unless vars.size == types.size
|
32
|
+
|
33
|
+
dic = vars.zip(types).each.with_object({}) do |(var, type), d|
|
34
|
+
d[var] = type
|
35
|
+
end
|
36
|
+
|
37
|
+
new(dictionary: dic, instance_type: instance_type, module_type: module_type, self_type: self_type)
|
38
|
+
end
|
39
|
+
|
40
|
+
def except(vars)
|
41
|
+
self.class.new(
|
42
|
+
dictionary: dictionary.reject {|k, _| vars.include?(k) },
|
43
|
+
instance_type: instance_type,
|
44
|
+
module_type: module_type,
|
45
|
+
self_type: self_type
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def merge!(s)
|
50
|
+
dictionary.transform_values! {|ty| ty.subst(s) }
|
51
|
+
dictionary.merge!(s.dictionary) do |key, a, b|
|
52
|
+
if a == b
|
53
|
+
a
|
54
|
+
else
|
55
|
+
raise "Duplicated key on merge!: #{key}, #{a}, #{b}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def add!(v, ty)
|
61
|
+
merge!(Substitution.new(dictionary: { v => ty }, instance_type: nil, module_type: nil, self_type: nil))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Steep
|
2
|
+
class ModuleName
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name:, absolute:)
|
6
|
+
@name = name
|
7
|
+
@absolute = absolute
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parse(name)
|
11
|
+
name = name.to_s
|
12
|
+
new(name: name.gsub(/\A::/, ""), absolute: name.start_with?("::"))
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_node(node)
|
16
|
+
case node.type
|
17
|
+
when :const
|
18
|
+
relative_node = new(name: node.children.last.to_s, absolute: false)
|
19
|
+
parent_node = node.children.first
|
20
|
+
|
21
|
+
case parent_node&.type
|
22
|
+
when :cbase
|
23
|
+
relative_node.absolute!
|
24
|
+
when nil
|
25
|
+
relative_node
|
26
|
+
else
|
27
|
+
from_node(parent_node)&.yield_self do |parent|
|
28
|
+
parent + relative_node
|
29
|
+
end
|
30
|
+
end
|
31
|
+
when :casgn
|
32
|
+
relative_node = new(name: node.children[1].to_s, absolute: false)
|
33
|
+
parent_node = node.children.first
|
34
|
+
|
35
|
+
case parent_node&.type
|
36
|
+
when :cbase
|
37
|
+
relative_node.absolute!
|
38
|
+
when nil
|
39
|
+
relative_node
|
40
|
+
else
|
41
|
+
from_node(parent_node)&.yield_self do |parent|
|
42
|
+
parent + relative_node
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
other.is_a?(self.class) && other.name == name && other.absolute? == absolute?
|
52
|
+
end
|
53
|
+
|
54
|
+
def hash
|
55
|
+
self.class.hash ^ name.hash ^ @absolute.hash
|
56
|
+
end
|
57
|
+
|
58
|
+
alias eql? ==
|
59
|
+
|
60
|
+
def absolute!
|
61
|
+
self.class.new(name: name, absolute: true)
|
62
|
+
end
|
63
|
+
|
64
|
+
def absolute?
|
65
|
+
!!@absolute
|
66
|
+
end
|
67
|
+
|
68
|
+
def relative?
|
69
|
+
!absolute?
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
if absolute?
|
74
|
+
"::#{name}"
|
75
|
+
else
|
76
|
+
name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def +(other)
|
81
|
+
case other
|
82
|
+
when self.class
|
83
|
+
if other.absolute?
|
84
|
+
other
|
85
|
+
else
|
86
|
+
self.class.new(name: "#{name}::#{other.name}", absolute: absolute?)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
self + self.class.parse(other)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def components
|
94
|
+
name.split(/::/).map.with_index {|s, index|
|
95
|
+
if index == 0 && absolute?
|
96
|
+
self.class.parse(s).absolute!
|
97
|
+
else
|
98
|
+
self.class.parse(s)
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def parent
|
104
|
+
components = components()
|
105
|
+
components.pop
|
106
|
+
|
107
|
+
unless components.empty?
|
108
|
+
self.class.parse(components.join("::"))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def simple?
|
113
|
+
components.size == 1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|