ruby-lsp 0.17.3 → 0.17.5

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 (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