sord 1.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  # typed: strong
2
2
  require 'sord/version'
3
- require 'sord/rbi_generator'
3
+ require 'sord/generator'
4
4
  require 'sord/parlour_plugin'
5
5
  require 'yard'
6
6
  require 'sorbet-runtime'
@@ -0,0 +1,601 @@
1
+ # typed: true
2
+ require 'yard'
3
+ require 'sord/type_converter'
4
+ require 'sord/logging'
5
+ require 'parlour'
6
+ require 'rainbow'
7
+
8
+ module Sord
9
+ # Converts the current working directory's YARD registry into an type
10
+ # signature file.
11
+ class Generator
12
+ VALID_MODES = [:rbi, :rbs]
13
+
14
+ # @return [Integer] The number of objects this generator has processed so
15
+ # far.
16
+ def object_count
17
+ @namespace_count + @method_count
18
+ end
19
+
20
+ # @return [Array<Array(String, YARD::CodeObjects::Base, Integer)>] The
21
+ # errors encountered by by the generator. Each element is of the form
22
+ # [message, item, line].
23
+ attr_reader :warnings
24
+
25
+ # Create a new generator.
26
+ # @param [Hash] options
27
+ # @option options [Symbol] mode
28
+ # @option options [Integer] break_params
29
+ # @option options [Boolean] replace_errors_with_untyped
30
+ # @option options [Boolean] replace_unresolved_with_untyped
31
+ # @option options [Boolean] comments
32
+ # @option options [Parlour::Generator] generator
33
+ # @option options [Parlour::TypedObject] root
34
+ # @return [void]
35
+ def initialize(options)
36
+ @mode = options[:mode].to_sym rescue options[:mode]
37
+ raise "invalid mode #{@mode}, expected one of: #{VALID_MODES.join(', ')}" \
38
+ unless VALID_MODES.include?(@mode)
39
+
40
+ @parlour = options[:parlour] || \
41
+ case @mode
42
+ when :rbi
43
+ Parlour::RbiGenerator.new
44
+ when :rbs
45
+ Parlour::RbsGenerator.new
46
+ end
47
+ @current_object = options[:root] || @parlour.root
48
+
49
+ @namespace_count = 0
50
+ @method_count = 0
51
+ @warnings = []
52
+
53
+ @replace_errors_with_untyped = options[:replace_errors_with_untyped]
54
+ @replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
55
+ @keep_original_comments = options[:keep_original_comments]
56
+ @skip_constants = options[:skip_constants]
57
+ @use_original_initialize_return = options[:use_original_initialize_return]
58
+
59
+ # Hook the logger so that messages are added as comments
60
+ Logging.add_hook do |type, msg, item|
61
+ @current_object.add_comment_to_next_child("sord #{type} - #{msg}")
62
+ end if options[:sord_comments]
63
+
64
+ # Hook the logger so that warnings are collected
65
+ Logging.add_hook do |type, msg, item|
66
+ # TODO: is it possible to get line numbers here?
67
+ warnings << [msg, item, 0] if type == :warn
68
+ end
69
+ end
70
+
71
+ # Increment the namespace counter.
72
+ # @return [void]
73
+ def count_namespace
74
+ @namespace_count += 1
75
+ end
76
+
77
+ # Increment the method counter.
78
+ # @return [void]
79
+ def count_method
80
+ @method_count += 1
81
+ end
82
+
83
+ # Given a YARD CodeObject, add lines defining its mixins (that is, extends
84
+ # and includes) to the current file. Returns the number of mixins.
85
+ # @param [YARD::CodeObjects::Base] item
86
+ # @return [Integer]
87
+ def add_mixins(item)
88
+ item.instance_mixins.reverse_each do |i|
89
+ @current_object.create_include(i.path.to_s)
90
+ end
91
+
92
+ # YARD 0.9.26 makes extends appear in the same order as code
93
+ # (includes are still reversed)
94
+ if Gem::Version.new(YARD::VERSION) >= Gem::Version.new("0.9.26")
95
+ item.class_mixins.each do |e|
96
+ @current_object.create_extend(e.path.to_s)
97
+ end
98
+ else
99
+ item.class_mixins.reverse_each do |e|
100
+ @current_object.create_extend(e.path.to_s)
101
+ end
102
+ end
103
+
104
+ item.instance_mixins.length + item.class_mixins.length
105
+ end
106
+
107
+ # Given a YARD NamespaceObject, add lines defining constants.
108
+ # @param [YARD::CodeObjects::NamespaceObject] item
109
+ # @return [void]
110
+ def add_constants(item)
111
+ inserted_constant_names = Set.new
112
+
113
+ item.constants(included: false).each do |constant|
114
+ # Take a constant (like "A::B::CONSTANT"), split it on each '::', and
115
+ # set the constant name to the last string in the array.
116
+ constant_name = constant.to_s.split('::').last
117
+ if inserted_constant_names.include?(constant_name) && @mode == :rbs
118
+ Logging.warn("RBS doesn't support duplicate constants, but '#{constant_name}' was duplicated - dropping future occurences", constant)
119
+ next
120
+ end
121
+ inserted_constant_names << constant_name
122
+
123
+ # Add the constant to the current object being generated.
124
+ case @mode
125
+ when :rbi
126
+ @current_object.create_constant(constant_name, value: "T.let(#{constant.value}, T.untyped)") do |c|
127
+ c.add_comments(constant.docstring.all.split("\n"))
128
+ end
129
+ when :rbs
130
+ @current_object.create_constant(constant_name, type: Parlour::Types::Untyped.new) do |c|
131
+ c.add_comments(constant.docstring.all.split("\n"))
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ # Adds comments to an object based on a docstring.
138
+ # @param [YARD::CodeObjects::NamespaceObject] item
139
+ # @param [Parlour::TypedObject] typed_object
140
+ # @return [void]
141
+ def add_comments(item, typed_object)
142
+ if @keep_original_comments
143
+ typed_object.add_comments(item.docstring.all.split("\n"))
144
+ else
145
+ parser = YARD::Docstring.parser
146
+ parser.parse(item.docstring.all)
147
+
148
+ docs_array = parser.text.split("\n")
149
+
150
+ # Add @param tags if there are any with names and descriptions.
151
+ params = parser.tags.select { |tag| tag.tag_name == 'param' && tag.is_a?(YARD::Tags::Tag) && !tag.name.nil? }
152
+ # Add a blank line if there's anything before the params.
153
+ docs_array << '' if docs_array.length.positive? && params.length.positive?
154
+ params.each do |param|
155
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive?
156
+ # Output params in the form of:
157
+ # _@param_ `foo` — Lorem ipsum.
158
+ # _@param_ `foo`
159
+ if param.text.nil? || param.text == ''
160
+ docs_array << "_@param_ `#{param.name}`"
161
+ else
162
+ docs_array << "_@param_ `#{param.name}` — #{param.text.gsub("\n", " ")}"
163
+ end
164
+ end
165
+
166
+ # Add @return tags (there could possibly be more than one, despite this not being supported)
167
+ returns = parser.tags.select { |tag| tag.tag_name == 'return' && tag.is_a?(YARD::Tags::Tag) && !tag.text.nil? && tag.text.strip != '' }
168
+ # Add a blank line if there's anything before the returns.
169
+ docs_array << '' if docs_array.length.positive? && returns.length.positive?
170
+ returns.each do |retn|
171
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive?
172
+ # Output returns in the form of:
173
+ # _@return_ — Lorem ipsum.
174
+ docs_array << "_@return_ — #{retn.text}"
175
+ end
176
+
177
+ # Iterate through the @example tags for a given YARD doc and output them in Markdown codeblocks.
178
+ examples = parser.tags.select { |tag| tag.tag_name == 'example' && tag.is_a?(YARD::Tags::Tag) }
179
+ examples.each do |example|
180
+ # Only add a blank line if there's anything before the example.
181
+ docs_array << '' if docs_array.length.positive?
182
+ # Include the example's 'name' if there is one.
183
+ docs_array << example.name unless example.name.nil? || example.name == ""
184
+ docs_array << "```ruby"
185
+ docs_array.concat(example.text.split("\n"))
186
+ docs_array << "```"
187
+ end if examples.length.positive?
188
+
189
+ # Add @note and @deprecated tags.
190
+ notice_tags = parser.tags.select { |tag| ['note', 'deprecated'].include?(tag.tag_name) && tag.is_a?(YARD::Tags::Tag) }
191
+ # Add a blank line if there's anything before the params.
192
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive? && notice_tags.length.positive?
193
+ notice_tags.each do |notice_tag|
194
+ docs_array << '' if docs_array.last != ''
195
+ # Output note/deprecated/see in the form of:
196
+ # _@note_ — Lorem ipsum.
197
+ # _@note_
198
+ if notice_tag.text.nil?
199
+ docs_array << "_@#{notice_tag.tag_name}_"
200
+ else
201
+ docs_array << "_@#{notice_tag.tag_name}_ — #{notice_tag.text}"
202
+ end
203
+ end
204
+
205
+ # Add @see tags.
206
+ see_tags = parser.tags.select { |tag| tag.tag_name == 'see' && tag.is_a?(YARD::Tags::Tag) }
207
+ # Add a blank line if there's anything before the params.
208
+ docs_array << '' if docs_array.last != '' && docs_array.length.positive? && see_tags.length.positive?
209
+ see_tags.each do |see_tag|
210
+ docs_array << '' if docs_array.last != ''
211
+ # Output note/deprecated/see in the form of:
212
+ # _@see_ `B` — Lorem ipsum.
213
+ # _@see_ `B`
214
+ if see_tag.text.nil?
215
+ docs_array << "_@see_ `#{see_tag.name}`"
216
+ else
217
+ docs_array << "_@see_ `#{see_tag.name}` — #{see_tag.text}"
218
+ end
219
+ end
220
+
221
+ # fix: yard text may contains multiple line. should deal \n.
222
+ # else generate text will be multiple line and only first line is commented
223
+ docs_array = docs_array.flat_map {|line| line.empty? ? [""] : line.split("\n")}
224
+ typed_object.add_comments(docs_array)
225
+ end
226
+ end
227
+
228
+ # Given a YARD NamespaceObject, add lines defining its methods and their
229
+ # signatures to the current file.
230
+ # @param [YARD::CodeObjects::NamespaceObject] item
231
+ # @return [void]
232
+ def add_methods(item)
233
+ item.meths(inherited: false).each do |meth|
234
+ count_method
235
+
236
+ # If the method is an alias, skip it so we don't define it as a
237
+ # separate method.
238
+ if meth.is_alias?
239
+ next
240
+ end
241
+
242
+ # If the method is an attribute, it'll be handled by add_attributes
243
+ if meth.is_attribute?
244
+ next
245
+ end
246
+
247
+ # Sort parameters
248
+ meth.parameters.reverse.sort! { |pair1, pair2| sort_params(pair1, pair2) }
249
+ # This is better than iterating over YARD's "@param" tags directly
250
+ # because it includes parameters without documentation
251
+ # (The gsubs allow for better splat-argument compatibility)
252
+ parameter_names_and_defaults_to_tags = meth.parameters.map do |name, default|
253
+ [[name, default], meth.tags('param')
254
+ .find { |p| p.name&.gsub('*', '')&.gsub(':', '') == name.gsub('*', '').gsub(':', '') }]
255
+ end.to_h
256
+
257
+ parameter_types = parameter_names_and_defaults_to_tags.map do |name_and_default, tag|
258
+ name = name_and_default.first
259
+
260
+ if tag
261
+ TypeConverter.yard_to_parlour(tag.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
262
+ elsif name.start_with? '&'
263
+ # Find yieldparams and yieldreturn
264
+ yieldparams = meth.tags('yieldparam')
265
+ yieldreturn = meth.tag('yieldreturn')&.types
266
+ yieldreturn = nil if yieldreturn&.length == 1 &&
267
+ yieldreturn&.first&.downcase == 'void'
268
+
269
+ # Create strings
270
+ params = yieldparams.map do |param|
271
+ Parlour::Types::Proc::Parameter.new(
272
+ param.name.gsub('*', ''),
273
+ TypeConverter.yard_to_parlour(param.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
274
+ )
275
+ end
276
+ returns = TypeConverter.yard_to_parlour(yieldreturn, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
277
+
278
+ # Create proc types, if possible
279
+ if yieldparams.empty? && yieldreturn.nil?
280
+ Parlour::Types::Untyped.new
281
+ else
282
+ Parlour::Types::Proc.new(params, yieldreturn.nil? ? nil : returns)
283
+ end
284
+ elsif meth.path.end_with? '='
285
+ # Look for the matching getter method
286
+ getter_path = meth.path[0...-1]
287
+ getter = item.meths.find { |m| m.path == getter_path }
288
+
289
+ unless getter
290
+ if parameter_names_and_defaults_to_tags.length == 1 \
291
+ && meth.tags('param').length == 1 \
292
+ && meth.tag('param').types
293
+
294
+ Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
295
+ next TypeConverter.yard_to_parlour(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
296
+ else
297
+ Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
298
+ next Parlour::Types::Untyped.new
299
+ end
300
+ end
301
+
302
+ return_types = getter.tags('return').flat_map(&:types)
303
+ unless return_types.any?
304
+ Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
305
+ next Parlour::Types::Untyped.new
306
+ end
307
+ inferred_type = TypeConverter.yard_to_parlour(
308
+ return_types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
309
+
310
+ Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type.describe} using getter's return type", meth)
311
+ inferred_type
312
+ else
313
+ # Is this the only argument, and was a @param specified without an
314
+ # argument name? If so, infer it
315
+ if parameter_names_and_defaults_to_tags.length == 1 \
316
+ && meth.tags('param').length == 1 \
317
+ && meth.tag('param').types
318
+
319
+ Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
320
+ TypeConverter.yard_to_parlour(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
321
+ else
322
+ Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
323
+ Parlour::Types::Untyped.new
324
+ end
325
+ end
326
+ end
327
+
328
+ return_tags = meth.tags('return')
329
+ returns = if meth.name == :initialize && !@use_original_initialize_return
330
+ nil
331
+ elsif return_tags.length == 0
332
+ Logging.omit("no YARD return type given, using untyped", meth)
333
+ Parlour::Types::Untyped.new
334
+ elsif return_tags.length == 1 && %w{void nil}.include?(return_tags&.first&.types&.first&.downcase)
335
+ nil
336
+ else
337
+ TypeConverter.yard_to_parlour(meth.tag('return').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
338
+ end
339
+
340
+ rbs_block = nil
341
+ parlour_params = parameter_names_and_defaults_to_tags
342
+ .zip(parameter_types)
343
+ .map do |((name, default), _), type|
344
+ # If the default is "nil" but the type is not nilable, then it
345
+ # should become nilable
346
+ # (T.untyped can include nil, so don't alter that)
347
+ type = Parlour::Types::Nilable.new(type) \
348
+ if default == 'nil' && !type.is_a?(Parlour::Types::Nilable) && !type.is_a?(Parlour::Types::Untyped)
349
+
350
+ case @mode
351
+ when :rbi
352
+ Parlour::RbiGenerator::Parameter.new(
353
+ name.to_s,
354
+ type: type,
355
+ default: default
356
+ )
357
+ when :rbs
358
+ if name.start_with?('&')
359
+ rbs_block = type
360
+ nil
361
+ else
362
+ Parlour::RbsGenerator::Parameter.new(
363
+ name.to_s,
364
+ type: type,
365
+ required: default.nil?
366
+ )
367
+ end
368
+ end
369
+ end
370
+ .compact
371
+
372
+ case @mode
373
+ when :rbi
374
+ @current_object.create_method(
375
+ meth.name.to_s,
376
+ parameters: parlour_params,
377
+ returns: returns,
378
+ class_method: meth.scope == :class
379
+ ) do |m|
380
+ add_comments(meth, m)
381
+ end
382
+ when :rbs
383
+ @current_object.create_method(
384
+ meth.name.to_s,
385
+ [Parlour::RbsGenerator::MethodSignature.new(
386
+ parlour_params, returns, block: rbs_block && !rbs_block.is_a?(Parlour::Types::Untyped) \
387
+ ? Parlour::RbsGenerator::Block.new(rbs_block, false)
388
+ : nil
389
+ )],
390
+ class_method: meth.scope == :class
391
+ ) do |m|
392
+ add_comments(meth, m)
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ # Given a YARD NamespaceObject, add lines defining either its class
399
+ # and instance attributes and their signatures to the current file.
400
+ # @param [YARD::CodeObjects::NamespaceObject] item
401
+ # @return [void]
402
+ def add_attributes(item)
403
+ [:class, :instance].each do |attr_loc|
404
+ # Grab attributes for the current location (class or instance)
405
+ attrs = item.attributes[attr_loc]
406
+ attrs.each do |name, attribute|
407
+ reader = attribute[:read]
408
+ writer = attribute[:write]
409
+
410
+ unless reader || writer
411
+ Logging.warn("attribute is not readable or writable somehow, skipping", attribute)
412
+ next
413
+ end
414
+
415
+ # Get all given types
416
+ yard_types = []
417
+ if reader
418
+ yard_types += reader.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
419
+ reader.tags('param').flat_map(&:types)
420
+ end
421
+ if writer
422
+ yard_types += writer.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
423
+ writer.tags('param').flat_map(&:types)
424
+ end
425
+
426
+ # Use untyped if not types specified anywhere, otherwise try to
427
+ # compute Parlour type given all these types
428
+ if yard_types.empty?
429
+ Logging.omit("no YARD type given for #{name.inspect}, using untyped", reader || writer)
430
+ parlour_type = Parlour::Types::Untyped.new
431
+ elsif yard_types.all? { |x| x == 'nil' }
432
+ # Nil attributes are extremely unusual, so just use untyped
433
+ parlour_type = Parlour::Types::Untyped.new
434
+ else
435
+ parlour_type = TypeConverter.yard_to_parlour(
436
+ yard_types, reader || writer, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
437
+ end
438
+
439
+ # Generate attribute
440
+ if reader && writer
441
+ kind = :accessor
442
+ elsif reader
443
+ kind = :reader
444
+ elsif writer
445
+ kind = :writer
446
+ end
447
+
448
+ case @mode
449
+ when :rbi
450
+ @current_object.create_attribute(
451
+ name.to_s,
452
+ kind: kind,
453
+ type: parlour_type,
454
+ class_attribute: (attr_loc == :class)
455
+ ) do |m|
456
+ add_comments(reader || writer, m)
457
+ end
458
+ when :rbs
459
+ if attr_loc == :class
460
+ Logging.warn("RBS doesn't support class attributes, dropping", reader || writer)
461
+ else
462
+ @current_object.create_attribute(
463
+ name.to_s,
464
+ kind: kind,
465
+ type: parlour_type,
466
+ ) do |m|
467
+ add_comments(reader || writer, m)
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ # Given a YARD NamespaceObject, add lines defining its mixins, methods
476
+ # and children to the file.
477
+ # @param [YARD::CodeObjects::NamespaceObject] item
478
+ # @return [void]
479
+ def add_namespace(item)
480
+ count_namespace
481
+
482
+ superclass = nil
483
+ superclass = item.superclass.path.to_s if item.type == :class && item.superclass.to_s != "Object"
484
+
485
+ parent = @current_object
486
+ @current_object = item.type == :class \
487
+ ? parent.create_class(item.name.to_s, superclass: superclass)
488
+ : parent.create_module(item.name.to_s)
489
+ @current_object.add_comments(item.docstring.all.split("\n"))
490
+
491
+ add_mixins(item)
492
+ add_methods(item)
493
+ add_attributes(item)
494
+ add_constants(item) unless @skip_constants
495
+
496
+ item.children.select { |x| [:class, :module].include?(x.type) }
497
+ .each { |child| add_namespace(child) }
498
+
499
+ @current_object = parent
500
+ end
501
+
502
+ # Populates the generator with the contents of the YARD registry. You
503
+ # must load the YARD registry first!
504
+ # @return [void]
505
+ def populate
506
+ # Generate top-level modules, which recurses to all modules
507
+ YARD::Registry.root.children
508
+ .select { |x| [:class, :module].include?(x.type) }
509
+ .each { |child| add_namespace(child) }
510
+ end
511
+
512
+ # Populates the generator with the contents of the YARD registry, then
513
+ # uses the loaded Parlour::Generator to generate the file. You must
514
+ # load the YARD registry first!
515
+ # @return [void]
516
+ def generate
517
+ populate
518
+ @parlour.send(@mode)
519
+ end
520
+
521
+ # Loads the YARD registry, populates the file, and prints any relevant
522
+ # final logs.
523
+ # @return [void]
524
+ def run
525
+ # Get YARD ready
526
+ YARD::Registry.load!
527
+
528
+ # Populate the type information file
529
+ populate
530
+
531
+ if object_count.zero?
532
+ Logging.warn("No objects processed.")
533
+ Logging.warn("Have you definitely generated the YARD documentation for this project?")
534
+ Logging.warn("Run `yard` to generate docs.")
535
+ end
536
+
537
+ Logging.done("Processed #{object_count} objects (#{@namespace_count} namespaces and #{@method_count} methods)")
538
+
539
+ Logging.hooks.clear
540
+
541
+ unless warnings.empty?
542
+ Logging.warn("There were #{warnings.length} important warnings in the output file, listed below.")
543
+ if @replace_errors_with_untyped
544
+ Logging.warn("The types which caused them have been replaced with untyped.")
545
+ else
546
+ Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
547
+ end
548
+ Logging.warn("Please edit the file to fix these errors.")
549
+ Logging.warn("Alternatively, edit your YARD documentation so that your types are valid and re-run Sord.")
550
+ warnings.each do |(msg, item, _)|
551
+ puts " (#{Rainbow(item&.path).bold}) #{msg}"
552
+ end
553
+ end
554
+ rescue
555
+ Logging.error($!)
556
+ $@.each do |line|
557
+ puts " #{line}"
558
+ end
559
+ end
560
+
561
+ # Given two pairs of arrays representing method parameters, in the form
562
+ # of ["variable_name", "default_value"], sort the parameters so they're
563
+ # valid for Sorbet. Sorbet requires that, e.g. required kwargs go before
564
+ # optional kwargs.
565
+ #
566
+ # @param [Array] pair1
567
+ # @param [Array] pair2
568
+ # @return Integer
569
+ def sort_params(pair1, pair2)
570
+ pair1_type, pair2_type = [pair1, pair2].map do |pair|
571
+ if pair[0].start_with?('&')
572
+ :blk
573
+ elsif pair[0].start_with?('**')
574
+ :doublesplat
575
+ elsif pair[0].start_with?('*')
576
+ :splat
577
+ elsif !pair[0].end_with?(':') && pair[1].nil?
578
+ :required_ordered_param
579
+ elsif !pair[0].end_with?(':') && !pair[1].nil?
580
+ :optional_ordered_param
581
+ elsif pair[0].end_with?(':') && pair[1].nil?
582
+ :required_kwarg
583
+ elsif pair[0].end_with?(':') && !pair[1].nil?
584
+ :optional_kwarg
585
+ end
586
+ end
587
+
588
+ pair_type_order = {
589
+ required_ordered_param: 1,
590
+ optional_ordered_param: 2,
591
+ splat: 3,
592
+ required_kwarg: 4,
593
+ optional_kwarg: 5,
594
+ doublesplat: 6,
595
+ blk: 7
596
+ }
597
+
598
+ return pair_type_order[pair1_type] <=> pair_type_order[pair2_type]
599
+ end
600
+ end
601
+ end