sord 1.0.0 → 3.0.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.
@@ -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