steep 1.2.0 → 1.3.0.pre.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -2
  3. data/Gemfile.lock +14 -4
  4. data/Gemfile.steep +2 -2
  5. data/Gemfile.steep.lock +17 -5
  6. data/Steepfile +7 -9
  7. data/lib/steep/annotation_parser.rb +34 -28
  8. data/lib/steep/ast/annotation.rb +16 -5
  9. data/lib/steep/ast/node/type_application.rb +74 -0
  10. data/lib/steep/ast/node/type_assertion.rb +56 -0
  11. data/lib/steep/ast/types/factory.rb +5 -1
  12. data/lib/steep/diagnostic/helper.rb +2 -1
  13. data/lib/steep/diagnostic/lsp_formatter.rb +3 -1
  14. data/lib/steep/diagnostic/ruby.rb +70 -5
  15. data/lib/steep/diagnostic/signature.rb +21 -8
  16. data/lib/steep/drivers/check.rb +1 -1
  17. data/lib/steep/drivers/checkfile.rb +1 -1
  18. data/lib/steep/drivers/langserver.rb +2 -2
  19. data/lib/steep/drivers/stats.rb +1 -1
  20. data/lib/steep/drivers/watch.rb +1 -1
  21. data/lib/steep/drivers/worker.rb +0 -1
  22. data/lib/steep/server/lsp_formatter.rb +13 -3
  23. data/lib/steep/server/master.rb +4 -1
  24. data/lib/steep/server/worker_process.rb +86 -14
  25. data/lib/steep/services/hover_provider/rbs.rb +7 -7
  26. data/lib/steep/services/hover_provider/ruby.rb +19 -4
  27. data/lib/steep/services/signature_service.rb +7 -4
  28. data/lib/steep/signature/validator.rb +36 -13
  29. data/lib/steep/source.rb +189 -71
  30. data/lib/steep/type_construction.rb +232 -126
  31. data/lib/steep/type_inference/logic_type_interpreter.rb +31 -5
  32. data/lib/steep/version.rb +1 -1
  33. data/lib/steep.rb +2 -0
  34. data/rbs_collection.steep.lock.yaml +52 -11
  35. data/rbs_collection.steep.yaml +1 -1
  36. data/sig/shims/exception.rbs +4 -0
  37. data/sig/shims/parser/comment.rbs +33 -0
  38. data/sig/shims/parser.rbs +30 -2
  39. data/sig/steep/annotation_parser.rbs +59 -0
  40. data/sig/steep/ast/annotation.rbs +21 -26
  41. data/sig/steep/ast/node/type_application.rbs +31 -0
  42. data/sig/steep/ast/node/type_assertion.rbs +26 -0
  43. data/sig/steep/ast/types/factory.rbs +0 -2
  44. data/sig/steep/diagnostic/helper.rbs +9 -3
  45. data/sig/steep/diagnostic/lsp_formatter.rbs +12 -8
  46. data/sig/steep/diagnostic/ruby.rbs +62 -8
  47. data/sig/steep/diagnostic/signature.rbs +118 -85
  48. data/sig/steep/drivers/worker.rbs +11 -13
  49. data/sig/steep/range_extension.rbs +7 -0
  50. data/sig/steep/server/lsp_formatter.rbs +14 -7
  51. data/sig/steep/server/worker_process.rbs +74 -12
  52. data/sig/steep/services/hover_provider/rbs.rbs +27 -7
  53. data/sig/steep/services/hover_provider/ruby.rbs +18 -4
  54. data/sig/steep/services/hover_provider/singleton_methods.rbs +1 -1
  55. data/sig/steep/signature/validator.rbs +76 -0
  56. data/sig/steep/source.rbs +54 -30
  57. data/sig/steep/type_construction.rbs +85 -27
  58. data/sig/steep/type_inference/method_call.rbs +1 -1
  59. data/smoke/diagnostics-rbs/inherit-module.rbs +2 -0
  60. data/smoke/diagnostics-rbs/test_expectations.yml +12 -0
  61. data/steep.gemspec +6 -1
  62. metadata +86 -6
@@ -20,7 +20,7 @@ module Steep
20
20
  end
21
21
 
22
22
  def each_error(&block)
23
- if block_given?
23
+ if block
24
24
  @errors.each(&block)
25
25
  else
26
26
  enum_for :each_error
@@ -64,6 +64,8 @@ module Steep
64
64
  )
65
65
 
66
66
  type_params.zip(type_args).each do |param, arg|
67
+ arg or raise
68
+
67
69
  if param.upper_bound
68
70
  upper_bound_type = factory.type(param.upper_bound).subst(subst)
69
71
  arg_type = factory.type(arg)
@@ -121,7 +123,7 @@ module Steep
121
123
 
122
124
  if name && type_params && type_args
123
125
  if !type_params.empty? && !type_args.empty?
124
- validate_type_application_constraints(type.name, type_params, type_args, location: type.location)
126
+ validate_type_application_constraints(name, type_params, type_args, location: type.location)
125
127
  end
126
128
  end
127
129
 
@@ -156,19 +158,21 @@ module Steep
156
158
  end
157
159
 
158
160
  def mixin_constraints(definition, mixin_ancestors, immediate_self_types:)
161
+ # @type var relations: Array[[Subtyping::Relation[AST::Types::t], RBS::Definition::Ancestor::Instance]]
159
162
  relations = []
160
163
 
161
164
  self_type = checker.factory.type(definition.self_type)
162
165
  if immediate_self_types && !immediate_self_types.empty?
163
- self_type = AST::Types::Intersection.build(
164
- types: immediate_self_types.map {|st| ancestor_to_type(st) }.push(self_type),
165
- location: nil
166
- )
166
+ # @type var sts: Array[AST::Types::t]
167
+ sts = immediate_self_types.map {|st| ancestor_to_type(st) }
168
+ self_type = AST::Types::Intersection.build(types: sts.push(self_type), location: nil)
167
169
  end
168
170
 
169
171
  mixin_ancestors.each do |ancestor|
170
172
  args = ancestor.args.map {|type| checker.factory.type(type) }
171
173
  ancestor_ancestors = builder.ancestor_builder.one_instance_ancestors(ancestor.name)
174
+ ancestor_ancestors.self_types or raise
175
+ ancestor_ancestors.params or raise
172
176
  self_constraints = ancestor_ancestors.self_types.map do |self_ancestor|
173
177
  s = Interface::Substitution.build(ancestor_ancestors.params, args)
174
178
  ancestor_to_type(self_ancestor).subst(s)
@@ -263,7 +267,7 @@ module Steep
263
267
  end
264
268
 
265
269
  ancestors = builder.ancestor_builder.one_instance_ancestors(name)
266
- mixin_constraints(definition, ancestors.included_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
270
+ mixin_constraints(definition, ancestors.included_modules || raise, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
267
271
  checker.check(
268
272
  relation,
269
273
  self_type: AST::Types::Self.new,
@@ -271,6 +275,8 @@ module Steep
271
275
  class_type: AST::Types::Class.new,
272
276
  constraints: Subtyping::Constraints.empty
273
277
  ).else do
278
+ raise if ancestor.source.is_a?(Symbol)
279
+
274
280
  @errors << Diagnostic::Signature::ModuleSelfTypeError.new(
275
281
  name: name,
276
282
  location: ancestor.source&.location || raise,
@@ -292,6 +298,14 @@ module Steep
292
298
  end
293
299
 
294
300
  builder.build_singleton(name).tap do |definition|
301
+ entry =
302
+ case definition.entry
303
+ when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
304
+ definition.entry
305
+ else
306
+ raise
307
+ end
308
+
295
309
  definition.instance_variables.each do |name, var|
296
310
  if parent = var.parent_variable
297
311
  var_type = checker.factory.type(var.type)
@@ -327,7 +341,7 @@ module Steep
327
341
  definition.class_variables.each do |name, var|
328
342
  if var.declared_in == definition.type_name
329
343
  if (parent = var.parent_variable) && var.declared_in != parent.declared_in
330
- class_var = definition.entry.decls.flat_map {|decl| decl.decl.members }.find do |member|
344
+ class_var = entry.decls.flat_map {|decl| decl.decl.members }.find do |member|
331
345
  member.is_a?(RBS::AST::Members::ClassVariable) && member.name == name
332
346
  end
333
347
 
@@ -336,7 +350,7 @@ module Steep
336
350
  class_name: definition.type_name,
337
351
  other_class_name: parent.declared_in,
338
352
  variable_name: name,
339
- location: class_var.location[:name]
353
+ location: class_var.location&.[](:name)
340
354
  )
341
355
  end
342
356
  end
@@ -344,6 +358,7 @@ module Steep
344
358
  end
345
359
 
346
360
  ancestors = builder.ancestor_builder.one_singleton_ancestors(name)
361
+ ancestors.extended_modules or raise
347
362
  mixin_constraints(definition, ancestors.extended_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor|
348
363
  checker.check(
349
364
  relation,
@@ -352,6 +367,8 @@ module Steep
352
367
  class_type: AST::Types::Class.new,
353
368
  constraints: Subtyping::Constraints.empty
354
369
  ).else do
370
+ raise if ancestor.source.is_a?(Symbol)
371
+
355
372
  @errors << Diagnostic::Signature::ModuleSelfTypeError.new(
356
373
  name: name,
357
374
  location: ancestor.source&.location || raise,
@@ -381,19 +398,23 @@ module Steep
381
398
  builder.build_instance(ancestor.name)
382
399
  when ancestor.name.interface?
383
400
  builder.build_interface(ancestor.name)
401
+ else
402
+ raise
384
403
  end
385
404
 
386
405
  location =
387
- if ancestor.source == :super
406
+ case ancestor.source
407
+ when :super
388
408
  primary_decl = env.class_decls[name].primary.decl
409
+ primary_decl.is_a?(RBS::AST::Declarations::Class) or raise
389
410
  if super_class = primary_decl.super_class
390
411
  super_class.location
391
412
  else
392
413
  # Implicit super class (Object): this can be skipped in fact...
393
- primary_decl.location[:name]
414
+ primary_decl.location&.aref(:name)
394
415
  end
395
416
  else
396
- ancestor.source.location
417
+ ancestor.source&.location
397
418
  end
398
419
 
399
420
  validate_type_application_constraints(
@@ -423,12 +444,14 @@ module Steep
423
444
  case ancestor
424
445
  when RBS::Definition::Ancestor::Instance
425
446
  # Interface ancestor cannot be other than Interface
447
+ ancestor.source.is_a?(Symbol) and raise
448
+
426
449
  defn = builder.build_interface(ancestor.name)
427
450
  validate_type_application_constraints(
428
451
  ancestor.name,
429
452
  defn.type_params_decl,
430
453
  ancestor.args,
431
- location: ancestor.source.location || raise
454
+ location: ancestor.source&.location || raise
432
455
  )
433
456
  end
434
457
  end
data/lib/steep/source.rb CHANGED
@@ -1,27 +1,11 @@
1
1
  module Steep
2
2
  class Source
3
- class LocatedAnnotation
4
- attr_reader :line
5
- attr_reader :annotation
6
- attr_reader :source
7
-
8
- def initialize(line:, source:, annotation:)
9
- @line = line
10
- @source = source
11
- @annotation = annotation
12
- end
13
-
14
- def ==(other)
15
- other.is_a?(LocatedAnnotation) &&
16
- other.line == line &&
17
- other.annotation == annotation
18
- end
19
- end
20
-
21
3
  attr_reader :path
22
4
  attr_reader :node
23
5
  attr_reader :mapping
24
6
 
7
+ extend NodeHelper
8
+
25
9
  def initialize(path:, node:, mapping:)
26
10
  @path = path
27
11
  @node = node
@@ -47,46 +31,61 @@ module Steep
47
31
 
48
32
  def self.parse(source_code, path:, factory:)
49
33
  buffer = ::Parser::Source::Buffer.new(path.to_s, 1, source: source_code)
50
- node = new_parser().parse(buffer)
34
+ node, comments = new_parser().parse_with_comments(buffer)
51
35
 
36
+ # @type var annotations: Array[AST::Annotation::t]
52
37
  annotations = []
53
-
54
- _, comments, _ = yield_self do
55
- buffer = ::Parser::Source::Buffer.new(path.to_s, 1, source: source_code)
56
- new_parser().tokenize(buffer)
57
- end
38
+ # @type var type_comments: Hash[Integer, type_comment]
39
+ type_comments = {}
58
40
 
59
41
  buffer = RBS::Buffer.new(name: path, content: source_code)
42
+ annotation_parser = AnnotationParser.new(factory: factory)
60
43
 
61
44
  comments.each do |comment|
62
- src = comment.text.gsub(/\A#\s*/, '')
63
- location = RBS::Location.new(buffer: buffer,
64
- start_pos: comment.location.expression.begin_pos + 1,
65
- end_pos: comment.location.expression.end_pos)
66
- annotation = AnnotationParser.new(factory: factory).parse(src, location: location)
67
- if annotation
68
- annotations << LocatedAnnotation.new(line: comment.location.line, source: src, annotation: annotation)
45
+ if comment.inline?
46
+ content = comment.text.delete_prefix('#')
47
+ content.lstrip!
48
+ prefix = comment.text.size - content.size
49
+ content.rstrip!
50
+ suffix = comment.text.size - content.size - prefix
51
+
52
+ location = RBS::Location.new(
53
+ buffer: buffer,
54
+ start_pos: comment.location.expression.begin_pos + prefix,
55
+ end_pos: comment.location.expression.end_pos - suffix
56
+ )
57
+
58
+ case
59
+ when annotation = annotation_parser.parse(content, location: location)
60
+ annotations << annotation
61
+ when assertion = AST::Node::TypeAssertion.parse(location)
62
+ type_comments[assertion.line] = assertion
63
+ when tapp = AST::Node::TypeApplication.parse(location)
64
+ type_comments[tapp.line] = tapp
65
+ end
69
66
  end
70
67
  end
71
68
 
72
- mapping = {}.compare_by_identity
69
+ map = {}
70
+ map.compare_by_identity
73
71
 
74
72
  if node
75
- construct_mapping(node: node, annotations: annotations, mapping: mapping)
73
+ node = insert_type_node(node, type_comments)
74
+ construct_mapping(node: node, annotations: annotations, mapping: map)
76
75
  end
77
76
 
78
77
  annotations.each do |annot|
79
- mapping[node] ||= []
80
- mapping[node] << annot
78
+ map[node] ||= []
79
+ map[node] << annot
81
80
  end
82
81
 
83
- new(path: path, node: node, mapping: mapping)
82
+ new(path: path, node: node, mapping: map)
84
83
  end
85
84
 
86
85
  def self.construct_mapping(node:, annotations:, mapping:, line_range: nil)
87
86
  case node.type
88
87
  when :if
89
- if node.loc.is_a?(::Parser::Source::Map::Ternary)
88
+ if node.loc.respond_to?(:question)
90
89
  # Skip ternary operator
91
90
  each_child_node node do |child|
92
91
  construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil)
@@ -200,7 +199,7 @@ module Steep
200
199
  last_cond = node.children[-2]
201
200
  body = node.children.last
202
201
 
203
- node.children.take(node.children.size-1) do |child|
202
+ node.children.take(node.children.size-1).each do |child|
204
203
  construct_mapping(node: child, annotations: annotations, mapping: mapping, line_range: nil)
205
204
  end
206
205
 
@@ -235,7 +234,7 @@ module Steep
235
234
  end
236
235
  end
237
236
 
238
- associated_annotations = annotations.select do |annot|
237
+ associated_annotations, other_annotations = annotations.partition do |annot|
239
238
  case node.type
240
239
  when :def, :module, :class, :block, :ensure, :defs
241
240
  loc = node.loc
@@ -257,33 +256,34 @@ module Steep
257
256
  associated_annotations.each do |annot|
258
257
  mapping[node] ||= []
259
258
  mapping[node] << annot
260
- annotations.delete annot
261
259
  end
262
- end
263
260
 
264
- def self.each_child_node(node)
265
- node.children.each do |child|
266
- if child.is_a?(::AST::Node)
267
- yield child
268
- end
269
- end
261
+ annotations.replace(other_annotations)
270
262
  end
271
263
 
272
- def self.map_child_nodes(node)
264
+ def self.map_child_node(node, type = nil, skip: nil)
273
265
  children = node.children.map do |child|
274
- if child.is_a?(::AST::Node)
275
- yield child
266
+ if child.is_a?(Parser::AST::Node)
267
+ if skip
268
+ if skip.member?(child)
269
+ child
270
+ else
271
+ yield child
272
+ end
273
+ else
274
+ yield child
275
+ end
276
276
  else
277
277
  child
278
278
  end
279
279
  end
280
280
 
281
- node.updated(nil, children)
281
+ node.updated(type, children)
282
282
  end
283
283
 
284
284
  def annotations(block:, factory:, context:)
285
285
  AST::Annotation::Collection.new(
286
- annotations: (mapping[block] || []).map(&:annotation),
286
+ annotations: (mapping[block] || []),
287
287
  factory: factory,
288
288
  context: context
289
289
  )
@@ -292,7 +292,7 @@ module Steep
292
292
  def each_annotation(&block)
293
293
  if block_given?
294
294
  mapping.each do |node, annots|
295
- yield node, annots.map(&:annotation)
295
+ yield [node, annots]
296
296
  end
297
297
  else
298
298
  enum_for :each_annotation
@@ -301,9 +301,11 @@ module Steep
301
301
 
302
302
  def each_heredoc_node(node = self.node, parents = [], &block)
303
303
  if block
304
+ return unless node
305
+
304
306
  case node.type
305
307
  when :dstr, :str
306
- if node.location.is_a?(Parser::Source::Map::Heredoc)
308
+ if node.location.respond_to?(:heredoc_body)
307
309
  yield [node, *parents]
308
310
  end
309
311
  end
@@ -344,7 +346,9 @@ module Steep
344
346
  parents.unshift node
345
347
 
346
348
  Source.each_child_node(node) do |child|
347
- ns = find_nodes_loc(child, position, parents) and return ns
349
+ if ns = find_nodes_loc(child, position, parents)
350
+ return ns
351
+ end
348
352
  end
349
353
 
350
354
  parents
@@ -359,12 +363,14 @@ module Steep
359
363
  node.location.expression.source_buffer.source_line(i+1).size + 1
360
364
  end + column
361
365
 
362
- if nodes = find_heredoc_nodes(line, column, position)
363
- Source.each_child_node(nodes[0]) do |child|
364
- find_nodes_loc(child, position, nodes) and break
366
+ if heredoc_nodes = find_heredoc_nodes(line, column, position)
367
+ Source.each_child_node(heredoc_nodes[0]) do |child|
368
+ if nodes = find_nodes_loc(child, position, heredoc_nodes)
369
+ return nodes
370
+ end
365
371
  end
366
372
 
367
- nodes
373
+ return heredoc_nodes
368
374
  else
369
375
  find_nodes_loc(node, position, [])
370
376
  end
@@ -385,7 +391,7 @@ module Steep
385
391
  delete_defs(node.children[0], allow_list)
386
392
  end
387
393
  else
388
- map_child_nodes(node) do |child|
394
+ map_child_node(node) do |child|
389
395
  delete_defs(child, allow_list)
390
396
  end
391
397
  end
@@ -398,7 +404,9 @@ module Steep
398
404
 
399
405
  node_ = Source.delete_defs(node, defs)
400
406
 
401
- mapping = {}.compare_by_identity
407
+ # @type var mapping: Hash[Parser::AST::Node, Array[AST::Annotation::t]]
408
+ mapping = {}
409
+ mapping.compare_by_identity
402
410
 
403
411
  annotations = self.mapping.values.flatten
404
412
  Source.construct_mapping(node: node_, annotations: annotations, mapping: mapping)
@@ -414,19 +422,129 @@ module Steep
414
422
  end
415
423
  end
416
424
 
417
- def compact_siblings(node)
418
- case node
419
- when :def
420
- node.updated(:nil, [])
421
- when :defs
422
- node.children[0]
423
- when :class
424
- node.updated(:class, [node.children[0], node.children[1], nil])
425
- when :module
426
- node.updated(:module, [node.children[0], nil])
425
+ def self.insert_type_node(node, comments)
426
+ if node.location.expression
427
+ first_line = node.location.expression.first_line
428
+ last_line = node.location.expression.last_line
429
+ last_comment = comments[last_line]
430
+
431
+ if (first_line..last_line).none? {|l| comments.key?(l) }
432
+ return node
433
+ end
434
+
435
+ case
436
+ when last_comment.is_a?(AST::Node::TypeAssertion)
437
+ case node.type
438
+ when :lvasgn, :ivasgn, :gvasgn, :cvasgn, :casgn
439
+ # Skip
440
+ when :masgn
441
+ lhs, rhs = node.children
442
+ node = node.updated(nil, [lhs, insert_type_node(rhs, comments)])
443
+ return adjust_location(node)
444
+ when :return, :break, :next
445
+ # Skip
446
+ when :begin
447
+ if node.loc.begin
448
+ # paren
449
+ child_assertions = comments.except(last_line)
450
+ node = map_child_node(node) {|child| insert_type_node(child, child_assertions) }
451
+ node = adjust_location(node)
452
+ return assertion_node(node, last_comment)
453
+ end
454
+ else
455
+ child_assertions = comments.except(last_line)
456
+ node = map_child_node(node) {|child| insert_type_node(child, child_assertions) }
457
+ node = adjust_location(node)
458
+ return assertion_node(node, last_comment)
459
+ end
460
+ when selector_line = sendish_node?(node)
461
+ if (comment = comments[selector_line]).is_a?(AST::Node::TypeApplication)
462
+ child_assertions = comments.except(selector_line)
463
+ case node.type
464
+ when :block
465
+ send, *children = node.children
466
+ node = node.updated(
467
+ nil,
468
+ [
469
+ map_child_node(send) {|child| insert_type_node(child, child_assertions) },
470
+ *children.map {|child| insert_type_node(child, child_assertions) }
471
+ ]
472
+ )
473
+ when :numblock
474
+ send, size, body = node.children
475
+ node = node.updated(
476
+ nil,
477
+ [
478
+ map_child_node(send) {|child| insert_type_node(child, child_assertions) },
479
+ size,
480
+ insert_type_node(body, child_assertions)
481
+ ]
482
+ )
483
+ else
484
+ node = map_child_node(node) {|child| insert_type_node(child, child_assertions) }
485
+ end
486
+ node = adjust_location(node)
487
+ return type_application_node(node, comment)
488
+ end
489
+ end
490
+ end
491
+
492
+ adjust_location(
493
+ map_child_node(node, nil) {|child| insert_type_node(child, comments) }
494
+ )
495
+ end
496
+
497
+ def self.sendish_node?(node)
498
+ send_node =
499
+ case node.type
500
+ when :send, :csend
501
+ node
502
+ when :block, :numblock
503
+ send = node.children[0]
504
+ case send.type
505
+ when :send, :csend
506
+ send
507
+ end
508
+ end
509
+
510
+ if send_node
511
+ if send_node.location.dot
512
+ send_node.location.selector.line
513
+ end
514
+ end
515
+ end
516
+
517
+ def self.adjust_location(node)
518
+ if end_pos = node.location.expression&.end_pos
519
+ if last_pos = each_child_node(node).map {|node| node.location.expression&.end_pos }.compact.max
520
+ if last_pos > end_pos
521
+ props = { location: node.location.with_expression(node.location.expression.with(end_pos: last_pos)) }
522
+ end
523
+ end
524
+ end
525
+
526
+ if props
527
+ node.updated(nil, nil, props)
427
528
  else
428
529
  node
429
530
  end
430
531
  end
532
+
533
+ def self.assertion_node(node, type)
534
+ map = Parser::Source::Map.new(node.location.expression.with(end_pos: type.location.end_pos))
535
+ Parser::AST::Node.new(:assertion, [node, type], { location: map })
536
+ end
537
+
538
+ def self.type_application_node(node, tapp)
539
+ if node.location.expression.end_pos > tapp.location.end_pos
540
+ map = Parser::Source::Map.new(node.location.expression)
541
+ else
542
+ map = Parser::Source::Map.new(node.location.expression.with(end_pos: tapp.location.end_pos))
543
+ end
544
+
545
+ node = Parser::AST::Node.new(:tapp, [node, tapp], { location: map })
546
+ tapp.set_node(node)
547
+ node
548
+ end
431
549
  end
432
550
  end