sord 0.10.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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