sord 0.10.0 → 3.0.0

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