ruby-lsp 0.17.3 → 0.17.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +251 -100
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +173 -114
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +337 -77
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +43 -14
  8. data/lib/ruby_indexer/test/classes_and_modules_test.rb +79 -3
  9. data/lib/ruby_indexer/test/index_test.rb +563 -29
  10. data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
  11. data/lib/ruby_indexer/test/method_test.rb +75 -25
  12. data/lib/ruby_indexer/test/rbs_indexer_test.rb +38 -2
  13. data/lib/ruby_indexer/test/test_case.rb +1 -5
  14. data/lib/ruby_lsp/addon.rb +13 -1
  15. data/lib/ruby_lsp/document.rb +50 -23
  16. data/lib/ruby_lsp/erb_document.rb +125 -0
  17. data/lib/ruby_lsp/global_state.rb +11 -4
  18. data/lib/ruby_lsp/internal.rb +3 -0
  19. data/lib/ruby_lsp/listeners/completion.rb +69 -34
  20. data/lib/ruby_lsp/listeners/definition.rb +34 -23
  21. data/lib/ruby_lsp/listeners/hover.rb +14 -7
  22. data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
  23. data/lib/ruby_lsp/node_context.rb +6 -1
  24. data/lib/ruby_lsp/requests/code_action_resolve.rb +2 -2
  25. data/lib/ruby_lsp/requests/completion.rb +6 -5
  26. data/lib/ruby_lsp/requests/completion_resolve.rb +7 -4
  27. data/lib/ruby_lsp/requests/definition.rb +4 -3
  28. data/lib/ruby_lsp/requests/formatting.rb +2 -0
  29. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
  30. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  31. data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -2
  32. data/lib/ruby_lsp/requests/support/common.rb +19 -1
  33. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
  34. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +87 -0
  35. data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
  36. data/lib/ruby_lsp/requests.rb +2 -0
  37. data/lib/ruby_lsp/ruby_document.rb +10 -0
  38. data/lib/ruby_lsp/server.rb +95 -26
  39. data/lib/ruby_lsp/store.rb +23 -8
  40. data/lib/ruby_lsp/test_helper.rb +3 -1
  41. data/lib/ruby_lsp/type_inferrer.rb +86 -0
  42. metadata +10 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c02fa9ec776002a86f4eeee4037132d8b0f9403f101ccff5c7a31c46385a2818
4
- data.tar.gz: 3b788794763cee273e2ce677955c3750ebac90962c8d328946f91998a900eca5
3
+ metadata.gz: 30ab87e9f42251fafcf8c085d14272b6e0cc0fb6e6968db3fc4c304e92000c63
4
+ data.tar.gz: 305592ad82c3e41b72dff4c0a48ff60e6158ffba6d06b802844418c3873a6ef9
5
5
  SHA512:
6
- metadata.gz: e3e152e24d1dca4aca721537f64df9062ae48aeea65b361ffd80dc6710cd263b0e8d1181f970b6d06e6147882c1cad40ba91d25ef6f684e2f5374a9738e1fae9
7
- data.tar.gz: 79c3eb785ac55de19c4e9031ed6c8ca0a9a6df27156244ba7ee3ca2109659f8f2b9ce6efe7259cfcd65922a0b8cefc384fca2215dbdffac41735d9149db651a0
6
+ metadata.gz: 3a38745ee99e4e62843f3bc2a698ec8c447d9bbb39091e158b2a64e36e46a1612962d1062635a007e13aabd3709733b9726a302ef680bd2881673e6abd48e038
7
+ data.tar.gz: 57b5327b0f17af4530b21ba3c51eb2bff13912e76df77f78284cd9644cebef252e1cbdd38b960de19cc64a68d9daf3788c6e78ef3b95b4bb5d6dc7f97e56c58f
data/README.md CHANGED
@@ -109,6 +109,10 @@ features. This is the mechanism that powers addons like
109
109
  - [Ruby LSP RSpec](https://github.com/st0012/ruby-lsp-rspec)
110
110
  - [Ruby LSP rubyfmt](https://github.com/jscharf/ruby-lsp-rubyfmt)
111
111
 
112
+ Additionally, some tools may include a Ruby LSP addon directly, like
113
+
114
+ - [Standard Ruby (from v1.39.1)](https://github.com/standardrb/standard/wiki/IDE:-vscode#using-ruby-lsp)
115
+
112
116
  Other community driven addons can be found in [rubygems](https://rubygems.org/search?query=name%3A+ruby-lsp) by
113
117
  searching for the `ruby-lsp` prefix.
114
118
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.3
1
+ 0.17.5
@@ -5,6 +5,9 @@ module RubyIndexer
5
5
  class DeclarationListener
6
6
  extend T::Sig
7
7
 
8
+ OBJECT_NESTING = T.let(["Object"].freeze, T::Array[String])
9
+ BASIC_OBJECT_NESTING = T.let(["BasicObject"].freeze, T::Array[String])
10
+
8
11
  sig do
9
12
  params(index: Index, dispatcher: Prism::Dispatcher, parse_result: Prism::ParseResult, file_path: String).void
10
13
  end
@@ -33,6 +36,8 @@ module RubyIndexer
33
36
  :on_class_node_leave,
34
37
  :on_module_node_enter,
35
38
  :on_module_node_leave,
39
+ :on_singleton_class_node_enter,
40
+ :on_singleton_class_node_leave,
36
41
  :on_def_node_enter,
37
42
  :on_def_node_leave,
38
43
  :on_call_node_enter,
@@ -59,30 +64,43 @@ module RubyIndexer
59
64
  sig { params(node: Prism::ClassNode).void }
60
65
  def on_class_node_enter(node)
61
66
  @visibility_stack.push(Entry::Visibility::PUBLIC)
62
- name = node.constant_path.location.slice
67
+ constant_path = node.constant_path
68
+ name = constant_path.slice
63
69
 
64
70
  comments = collect_comments(node)
65
71
 
66
72
  superclass = node.superclass
73
+
74
+ nesting = name.start_with?("::") ? [name.delete_prefix("::")] : @stack + [name.delete_prefix("::")]
75
+
67
76
  parent_class = case superclass
68
77
  when Prism::ConstantReadNode, Prism::ConstantPathNode
69
78
  superclass.slice
70
79
  else
71
- "::Object"
80
+ case nesting
81
+ when OBJECT_NESTING
82
+ # When Object is reopened, its parent class should still be the top-level BasicObject
83
+ "::BasicObject"
84
+ when BASIC_OBJECT_NESTING
85
+ # When BasicObject is reopened, its parent class should still be nil
86
+ nil
87
+ else
88
+ # Otherwise, the parent class should be the top-level Object
89
+ "::Object"
90
+ end
72
91
  end
73
92
 
74
- nesting = name.start_with?("::") ? [name.delete_prefix("::")] : @stack + [name.delete_prefix("::")]
75
-
76
93
  entry = Entry::Class.new(
77
94
  nesting,
78
95
  @file_path,
79
96
  node.location,
97
+ constant_path.location,
80
98
  comments,
81
99
  parent_class,
82
100
  )
83
101
 
84
102
  @owner_stack << entry
85
- @index << entry
103
+ @index.add(entry)
86
104
  @stack << name
87
105
  end
88
106
 
@@ -96,15 +114,16 @@ module RubyIndexer
96
114
  sig { params(node: Prism::ModuleNode).void }
97
115
  def on_module_node_enter(node)
98
116
  @visibility_stack.push(Entry::Visibility::PUBLIC)
99
- name = node.constant_path.location.slice
117
+ constant_path = node.constant_path
118
+ name = constant_path.slice
100
119
 
101
120
  comments = collect_comments(node)
102
121
 
103
122
  nesting = name.start_with?("::") ? [name.delete_prefix("::")] : @stack + [name.delete_prefix("::")]
104
- entry = Entry::Module.new(nesting, @file_path, node.location, comments)
123
+ entry = Entry::Module.new(nesting, @file_path, node.location, constant_path.location, comments)
105
124
 
106
125
  @owner_stack << entry
107
- @index << entry
126
+ @index.add(entry)
108
127
  @stack << name
109
128
  end
110
129
 
@@ -115,6 +134,44 @@ module RubyIndexer
115
134
  @visibility_stack.pop
116
135
  end
117
136
 
137
+ sig { params(node: Prism::SingletonClassNode).void }
138
+ def on_singleton_class_node_enter(node)
139
+ @visibility_stack.push(Entry::Visibility::PUBLIC)
140
+
141
+ current_owner = @owner_stack.last
142
+
143
+ if current_owner
144
+ expression = node.expression
145
+ @stack << (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
146
+
147
+ existing_entries = T.cast(@index[@stack.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
148
+
149
+ if existing_entries
150
+ entry = T.must(existing_entries.first)
151
+ entry.update_singleton_information(node.location, expression.location, collect_comments(node))
152
+ else
153
+ entry = Entry::SingletonClass.new(
154
+ @stack,
155
+ @file_path,
156
+ node.location,
157
+ expression.location,
158
+ collect_comments(node),
159
+ nil,
160
+ )
161
+ @index.add(entry, skip_prefix_tree: true)
162
+ end
163
+
164
+ @owner_stack << entry
165
+ end
166
+ end
167
+
168
+ sig { params(node: Prism::SingletonClassNode).void }
169
+ def on_singleton_class_node_leave(node)
170
+ @stack.pop
171
+ @owner_stack.pop
172
+ @visibility_stack.pop
173
+ end
174
+
118
175
  sig { params(node: Prism::MultiWriteNode).void }
119
176
  def on_multi_write_node_enter(node)
120
177
  value = node.value
@@ -246,119 +303,119 @@ module RubyIndexer
246
303
 
247
304
  case node.receiver
248
305
  when nil
249
- @index << Entry::InstanceMethod.new(
306
+ @index.add(Entry::Method.new(
250
307
  method_name,
251
308
  @file_path,
252
309
  node.location,
310
+ node.name_loc,
253
311
  comments,
254
- node.parameters,
312
+ [Entry::Signature.new(list_params(node.parameters))],
255
313
  current_visibility,
256
314
  @owner_stack.last,
257
- )
315
+ ))
258
316
  when Prism::SelfNode
259
- @index << Entry::SingletonMethod.new(
260
- method_name,
261
- @file_path,
262
- node.location,
263
- comments,
264
- node.parameters,
265
- current_visibility,
266
- @owner_stack.last,
267
- )
317
+ owner = @owner_stack.last
318
+
319
+ if owner
320
+ singleton = @index.existing_or_new_singleton_class(owner.name)
321
+
322
+ @index.add(Entry::Method.new(
323
+ method_name,
324
+ @file_path,
325
+ node.location,
326
+ node.name_loc,
327
+ comments,
328
+ [Entry::Signature.new(list_params(node.parameters))],
329
+ current_visibility,
330
+ singleton,
331
+ ))
332
+
333
+ @owner_stack << singleton
334
+ @stack << "<Class:#{@stack.last}>"
335
+ end
268
336
  end
269
337
  end
270
338
 
271
339
  sig { params(node: Prism::DefNode).void }
272
340
  def on_def_node_leave(node)
273
341
  @inside_def = false
342
+
343
+ if node.receiver.is_a?(Prism::SelfNode)
344
+ @owner_stack.pop
345
+ @stack.pop
346
+ end
274
347
  end
275
348
 
276
349
  sig { params(node: Prism::InstanceVariableWriteNode).void }
277
350
  def on_instance_variable_write_node_enter(node)
278
- name = node.name.to_s
279
- return if name == "@"
280
-
281
- @index << Entry::InstanceVariable.new(
282
- name,
283
- @file_path,
284
- node.name_loc,
285
- collect_comments(node),
286
- @owner_stack.last,
287
- )
351
+ handle_instance_variable(node, node.name_loc)
288
352
  end
289
353
 
290
354
  sig { params(node: Prism::InstanceVariableAndWriteNode).void }
291
355
  def on_instance_variable_and_write_node_enter(node)
292
- name = node.name.to_s
293
- return if name == "@"
294
-
295
- @index << Entry::InstanceVariable.new(
296
- name,
297
- @file_path,
298
- node.name_loc,
299
- collect_comments(node),
300
- @owner_stack.last,
301
- )
356
+ handle_instance_variable(node, node.name_loc)
302
357
  end
303
358
 
304
359
  sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
305
360
  def on_instance_variable_operator_write_node_enter(node)
306
- name = node.name.to_s
307
- return if name == "@"
308
-
309
- @index << Entry::InstanceVariable.new(
310
- name,
311
- @file_path,
312
- node.name_loc,
313
- collect_comments(node),
314
- @owner_stack.last,
315
- )
361
+ handle_instance_variable(node, node.name_loc)
316
362
  end
317
363
 
318
364
  sig { params(node: Prism::InstanceVariableOrWriteNode).void }
319
365
  def on_instance_variable_or_write_node_enter(node)
320
- name = node.name.to_s
321
- return if name == "@"
322
-
323
- @index << Entry::InstanceVariable.new(
324
- name,
325
- @file_path,
326
- node.name_loc,
327
- collect_comments(node),
328
- @owner_stack.last,
329
- )
366
+ handle_instance_variable(node, node.name_loc)
330
367
  end
331
368
 
332
369
  sig { params(node: Prism::InstanceVariableTargetNode).void }
333
370
  def on_instance_variable_target_node_enter(node)
334
- name = node.name.to_s
335
- return if name == "@"
336
-
337
- @index << Entry::InstanceVariable.new(
338
- name,
339
- @file_path,
340
- node.location,
341
- collect_comments(node),
342
- @owner_stack.last,
343
- )
371
+ handle_instance_variable(node, node.location)
344
372
  end
345
373
 
346
374
  sig { params(node: Prism::AliasMethodNode).void }
347
375
  def on_alias_method_node_enter(node)
348
376
  method_name = node.new_name.slice
349
377
  comments = collect_comments(node)
350
- @index << Entry::UnresolvedMethodAlias.new(
351
- method_name,
352
- node.old_name.slice,
353
- @owner_stack.last,
354
- @file_path,
355
- node.new_name.location,
356
- comments,
378
+ @index.add(
379
+ Entry::UnresolvedMethodAlias.new(
380
+ method_name,
381
+ node.old_name.slice,
382
+ @owner_stack.last,
383
+ @file_path,
384
+ node.new_name.location,
385
+ comments,
386
+ ),
357
387
  )
358
388
  end
359
389
 
360
390
  private
361
391
 
392
+ sig do
393
+ params(
394
+ node: T.any(
395
+ Prism::InstanceVariableAndWriteNode,
396
+ Prism::InstanceVariableOperatorWriteNode,
397
+ Prism::InstanceVariableOrWriteNode,
398
+ Prism::InstanceVariableTargetNode,
399
+ Prism::InstanceVariableWriteNode,
400
+ ),
401
+ loc: Prism::Location,
402
+ ).void
403
+ end
404
+ def handle_instance_variable(node, loc)
405
+ name = node.name.to_s
406
+ return if name == "@"
407
+
408
+ # When instance variables are declared inside the class body, they turn into class instance variables rather than
409
+ # regular instance variables
410
+ owner = @owner_stack.last
411
+
412
+ if owner && !@inside_def
413
+ owner = @index.existing_or_new_singleton_class(owner.name)
414
+ end
415
+
416
+ @index.add(Entry::InstanceVariable.new(name, @file_path, loc, collect_comments(node), owner))
417
+ end
418
+
362
419
  sig { params(node: Prism::CallNode).void }
363
420
  def handle_private_constant(node)
364
421
  arguments = node.arguments&.arguments
@@ -411,13 +468,15 @@ module RubyIndexer
411
468
  return unless old_name_value
412
469
 
413
470
  comments = collect_comments(node)
414
- @index << Entry::UnresolvedMethodAlias.new(
415
- new_name_value,
416
- old_name_value,
417
- @owner_stack.last,
418
- @file_path,
419
- new_name.location,
420
- comments,
471
+ @index.add(
472
+ Entry::UnresolvedMethodAlias.new(
473
+ new_name_value,
474
+ old_name_value,
475
+ @owner_stack.last,
476
+ @file_path,
477
+ new_name.location,
478
+ comments,
479
+ ),
421
480
  )
422
481
  end
423
482
 
@@ -443,22 +502,24 @@ module RubyIndexer
443
502
  value = node.value unless node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode)
444
503
  comments = collect_comments(node)
445
504
 
446
- @index << case value
447
- when Prism::ConstantReadNode, Prism::ConstantPathNode
448
- Entry::UnresolvedAlias.new(value.slice, @stack.dup, name, @file_path, node.location, comments)
449
- when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode,
505
+ @index.add(
506
+ case value
507
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
508
+ Entry::UnresolvedAlias.new(value.slice, @stack.dup, name, @file_path, node.location, comments)
509
+ when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode,
450
510
  Prism::ConstantOperatorWriteNode
451
511
 
452
- # If the right hand side is another constant assignment, we need to visit it because that constant has to be
453
- # indexed too
454
- Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments)
455
- when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
512
+ # If the right hand side is another constant assignment, we need to visit it because that constant has to be
513
+ # indexed too
514
+ Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments)
515
+ when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
456
516
  Prism::ConstantPathAndWriteNode
457
517
 
458
- Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments)
459
- else
460
- Entry::Constant.new(name, @file_path, node.location, comments)
461
- end
518
+ Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments)
519
+ else
520
+ Entry::Constant.new(name, @file_path, node.location, comments)
521
+ end,
522
+ )
462
523
  end
463
524
 
464
525
  sig { params(node: Prism::Node).returns(T::Array[String]) }
@@ -515,15 +576,20 @@ module RubyIndexer
515
576
 
516
577
  next unless name && loc
517
578
 
518
- @index << Entry::Accessor.new(name, @file_path, loc, comments, current_visibility, @owner_stack.last) if reader
519
- @index << Entry::Accessor.new(
579
+ if reader
580
+ @index.add(Entry::Accessor.new(name, @file_path, loc, comments, current_visibility, @owner_stack.last))
581
+ end
582
+
583
+ next unless writer
584
+
585
+ @index.add(Entry::Accessor.new(
520
586
  "#{name}=",
521
587
  @file_path,
522
588
  loc,
523
589
  comments,
524
590
  current_visibility,
525
591
  @owner_stack.last,
526
- ) if writer
592
+ ))
527
593
  end
528
594
  end
529
595
 
@@ -546,7 +612,8 @@ module RubyIndexer
546
612
  when :prepend
547
613
  owner.mixin_operations << Entry::Prepend.new(node.full_name)
548
614
  when :extend
549
- owner.mixin_operations << Entry::Extend.new(node.full_name)
615
+ singleton = @index.existing_or_new_singleton_class(owner.name)
616
+ singleton.mixin_operations << Entry::Include.new(node.full_name)
550
617
  end
551
618
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
552
619
  Prism::ConstantPathNode::MissingNodesInConstantPathError
@@ -558,5 +625,89 @@ module RubyIndexer
558
625
  def current_visibility
559
626
  T.must(@visibility_stack.last)
560
627
  end
628
+
629
+ sig { params(parameters_node: T.nilable(Prism::ParametersNode)).returns(T::Array[Entry::Parameter]) }
630
+ def list_params(parameters_node)
631
+ return [] unless parameters_node
632
+
633
+ parameters = []
634
+
635
+ parameters_node.requireds.each do |required|
636
+ name = parameter_name(required)
637
+ next unless name
638
+
639
+ parameters << Entry::RequiredParameter.new(name: name)
640
+ end
641
+
642
+ parameters_node.optionals.each do |optional|
643
+ name = parameter_name(optional)
644
+ next unless name
645
+
646
+ parameters << Entry::OptionalParameter.new(name: name)
647
+ end
648
+
649
+ rest = parameters_node.rest
650
+
651
+ if rest.is_a?(Prism::RestParameterNode)
652
+ rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME
653
+ parameters << Entry::RestParameter.new(name: rest_name)
654
+ end
655
+
656
+ parameters_node.keywords.each do |keyword|
657
+ name = parameter_name(keyword)
658
+ next unless name
659
+
660
+ case keyword
661
+ when Prism::RequiredKeywordParameterNode
662
+ parameters << Entry::KeywordParameter.new(name: name)
663
+ when Prism::OptionalKeywordParameterNode
664
+ parameters << Entry::OptionalKeywordParameter.new(name: name)
665
+ end
666
+ end
667
+
668
+ keyword_rest = parameters_node.keyword_rest
669
+
670
+ if keyword_rest.is_a?(Prism::KeywordRestParameterNode)
671
+ keyword_rest_name = parameter_name(keyword_rest) || Entry::KeywordRestParameter::DEFAULT_NAME
672
+ parameters << Entry::KeywordRestParameter.new(name: keyword_rest_name)
673
+ end
674
+
675
+ parameters_node.posts.each do |post|
676
+ name = parameter_name(post)
677
+ next unless name
678
+
679
+ parameters << Entry::RequiredParameter.new(name: name)
680
+ end
681
+
682
+ block = parameters_node.block
683
+ parameters << Entry::BlockParameter.new(name: block.name || Entry::BlockParameter::DEFAULT_NAME) if block
684
+
685
+ parameters
686
+ end
687
+
688
+ sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(Symbol)) }
689
+ def parameter_name(node)
690
+ case node
691
+ when Prism::RequiredParameterNode, Prism::OptionalParameterNode,
692
+ Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
693
+ Prism::RestParameterNode, Prism::KeywordRestParameterNode
694
+ node.name
695
+ when Prism::MultiTargetNode
696
+ names = node.lefts.map { |parameter_node| parameter_name(parameter_node) }
697
+
698
+ rest = node.rest
699
+ if rest.is_a?(Prism::SplatNode)
700
+ name = rest.expression&.slice
701
+ names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym)
702
+ end
703
+
704
+ names << nil if rest.is_a?(Prism::ImplicitRestNode)
705
+
706
+ names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) })
707
+
708
+ names_with_commas = names.join(", ")
709
+ :"(#{names_with_commas})"
710
+ end
711
+ end
561
712
  end
562
713
  end