sord 4.0.0 → 5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc4c58d9ccea9406b38e20c3d6d92357a962e49aa91884b421a496d9302566d2
4
- data.tar.gz: cdcc1158ce99895796423ba6b833f68e261e09d6a61b6892104d5948a3c64625
3
+ metadata.gz: 12567f3b935a14271aedb28b62acfcfef3cb1e3bfb80fd294732947490dfb765
4
+ data.tar.gz: c40c2790500508fd2e575ab01e471cf7fe0e90f0c6d255285c7474433b65952c
5
5
  SHA512:
6
- metadata.gz: 984cf951dc9a0ee9ea6180402e60817c866d171f313c102a34f4d8d20cc36ca95d11c24082b46810b70eb35fa6e67643bd4b9e3007889ec7a87f25fa0db3c44d
7
- data.tar.gz: b9f25ab1affc00c9c2a25b90db7735c1ee8c89e53cd98ef1a04087b58d9efe2f8878e54f6a1cbdc913c38142507aaff74a9e437151cd74fd5f7b612cbab4f38f
6
+ metadata.gz: 85435a86928840302557121d0deeafde94a0b70905da93403c642a303ec773f5f4503b316beed31709052839a597e671df9ac554410d4c169843e859aebf361d
7
+ data.tar.gz: 77795ca64b509b9c7e7da210fac12d2b27bb8fe9e952ab61dc1d87c92189a1d0e457a4546bce2cebac4230a643f96072559c39bd477fc7e345819c98dd0b3bdd
@@ -6,10 +6,10 @@ jobs:
6
6
  test:
7
7
  strategy:
8
8
  matrix:
9
- ruby: [2.4, 2.5, 2.6, 2.7, 3.0]
9
+ ruby: [2.7, 3.0, 3.1]
10
10
  continue-on-error: false
11
-
12
- runs-on: ubuntu-latest
11
+
12
+ runs-on: ubuntu-latest
13
13
  steps:
14
14
  - uses: actions/checkout@v2
15
15
  - name: Set up Ruby
data/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [5.0.0] - 2022-10-06
7
+ ### Added
8
+ - If a derived class does not provide tags for a method, but it is overridden from a base class
9
+ which does, then the base class' documentation will be used to generate types for the derived
10
+ method.
11
+ - When generating RBS, if a duck type matches one of RBS' built-in interfaces, this type will be
12
+ generated instead. (For example, `#to_s` will generate the type `_ToS`.)
13
+ - Added the `--hide-private` flag, which will omit outputting items with private visibility.
14
+ - To improve resolution, types for gems are now loaded from the RBS collection.
15
+ - If you are using custom YARD tags, Sord can now be made aware of these by passing the `--tags`
16
+ option.
17
+
18
+ ### Changed
19
+ - **Breaking change**: Support for versions of Ruby prior to 2.7 has been dropped.
20
+ - When Sord runs YARD automatically, it no longer generates HTML documentation, since this isn't
21
+ necessary for Sord's analysis. If you were relying on this as part of your workflow, then this
22
+ could be a **breaking change**.
23
+
24
+ ### Fixed
25
+ - Duck-typed methods ending with `?` or `!`, and operator methods like `#[]=`, are now correctly
26
+ recognised as duck types.
27
+ - Fixed an exception when referring to built-in types with root namespace (`::Array<Foo>`) syntax.
28
+ - `@yieldparams` without a parameter name no longer cause an exception, and instead use default
29
+ names of the pattern: `arg0`, `arg1`, ...
30
+
6
31
  ## [4.0.0] - 2022-07-19
7
32
  ### Added
8
33
  - Constants are now assigned types when generating RBS, using `@return`.
data/README.md CHANGED
@@ -85,6 +85,8 @@ Sord also takes some flags to alter the generated file:
85
85
  (You cannot specify both `--include-messages` and `--exclude-messages`.)
86
86
  - `--exclude-untyped`: Exclude methods and attributes with untyped return
87
87
  values.
88
+ - `--tags TAGS`: Provide a list of comma-separated tags as understood by the
89
+ `yard` command. E.g. `--tags 'mytag:My Description,mytag2:My New Description'
88
90
 
89
91
  ## Example
90
92
 
data/exe/sord CHANGED
@@ -22,8 +22,10 @@ command :gen do |c|
22
22
  c.option '--include-messages STRING', String, 'Whitelists a comma-separated string of log message types'
23
23
  c.option '--keep-original-comments', 'Retains original YARD comments rather than converting them to Markdown'
24
24
  c.option '--skip-constants', 'Excludes constants from generated file'
25
+ c.option '--hide-private', 'Exclude any object marked with @!visibility private'
25
26
  c.option '--use-original-initialize-return', 'Uses the specified return type for #initialize rather than void'
26
27
  c.option '--exclude-untyped', 'Exclude methods and attributes with untyped return values'
28
+ c.option '--tags TAGS', Array, 'Tag parameters for the YARD command'
27
29
 
28
30
  c.action do |args, options|
29
31
  options.default(
@@ -34,12 +36,14 @@ command :gen do |c|
34
36
  break_params: 4,
35
37
  replace_errors_with_untyped: false,
36
38
  replace_unresolved_with_untyped: false,
39
+ hide_private: false,
37
40
  exclude_messages: nil,
38
41
  include_messages: nil,
39
42
  keep_original_comments: false,
40
43
  skip_constants: false,
41
44
  use_original_initialize_return: false,
42
45
  exclude_untyped: false,
46
+ tags: [],
43
47
  )
44
48
 
45
49
  if args.length != 1
@@ -86,7 +90,7 @@ command :gen do |c|
86
90
 
87
91
  plugin.parlour = klass.new(break_params: plugin_options[:break_params])
88
92
  plugin.generate(plugin.parlour.root)
89
-
93
+
90
94
  File.write(args.first, plugin.parlour.send(generator_method))
91
95
  end
92
96
  end
@@ -28,6 +28,7 @@ module Sord
28
28
  # @option options [Integer] break_params
29
29
  # @option options [Boolean] replace_errors_with_untyped
30
30
  # @option options [Boolean] replace_unresolved_with_untyped
31
+ # @option options [Boolean] hide_private
31
32
  # @option options [Boolean] comments
32
33
  # @option options [Parlour::Generator] generator
33
34
  # @option options [Parlour::TypedObject] root
@@ -53,18 +54,25 @@ module Sord
53
54
  @replace_errors_with_untyped = options[:replace_errors_with_untyped]
54
55
  @replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
55
56
  @keep_original_comments = options[:keep_original_comments]
57
+ @hide_private = options[:hide_private]
56
58
  @skip_constants = options[:skip_constants]
57
59
  @use_original_initialize_return = options[:use_original_initialize_return]
58
60
  @exclude_untyped = options[:exclude_untyped]
59
61
 
62
+ @type_converter_config = TypeConverter::Configuration.new(
63
+ output_language: @mode,
64
+ replace_errors_with_untyped: @replace_errors_with_untyped,
65
+ replace_unresolved_with_untyped: @replace_unresolved_with_untyped,
66
+ )
67
+
60
68
  # Hook the logger so that messages are added as comments
61
69
  Logging.add_hook do |type, msg, item, **opts|
62
70
  # Hack: the "exclude untyped" log message needs to print somewhere, but
63
71
  # if there's no future object for that comment to associate with, it'll
64
72
  # never be printed!
65
- # Instead, add an arbitrary containing the comment
73
+ # Instead, add an arbitrary containing the comment
66
74
  if opts[:immediate]
67
- @current_object.create_arbitrary(code: "# sord #{type} - #{msg}")
75
+ @current_object.create_arbitrary(code: "# sord #{type} - #{msg}")
68
76
  else
69
77
  @current_object.add_comment_to_next_child("sord #{type} - #{msg}")
70
78
  end
@@ -120,6 +128,7 @@ module Sord
120
128
  inserted_constant_names = Set.new
121
129
 
122
130
  item.constants(included: false).each do |constant|
131
+ next if @hide_private && constant.visibility == :private
123
132
  # Take a constant (like "A::B::CONSTANT"), split it on each '::', and
124
133
  # set the constant name to the last string in the array.
125
134
  constant_name = constant.to_s.split('::').last
@@ -144,8 +153,7 @@ module Sord
144
153
  TypeConverter.yard_to_parlour(
145
154
  return_tags.map(&:types).flatten,
146
155
  constant,
147
- @replace_errors_with_untyped,
148
- @replace_unresolved_with_untyped
156
+ @type_converter_config,
149
157
  )
150
158
  end
151
159
  @current_object.create_constant(constant_name, type: returns) do |c|
@@ -246,12 +254,33 @@ module Sord
246
254
  end
247
255
  end
248
256
 
257
+ # @param method [YARD::CodeObjects::MethodObject]
258
+ # @param tag_name [String]
259
+ # @return [Array<YARD::Tags::Tag>]
260
+ def method_tags(method, tag_name)
261
+ tags = method.tags(tag_name)
262
+ return tags if tags.any? || method.overridden_method.nil?
263
+
264
+ method.overridden_method.tags(tag_name)
265
+ end
266
+
267
+ # @param method [YARD::CodeObjects::MethodObject]
268
+ # @param tag_name [String]
269
+ # @return [YARD::Tags::Tag, nil]
270
+ def method_tag(method, tag_name)
271
+ tag = method.tag(tag_name)
272
+ return tag if tag || method.overridden_method.nil?
273
+
274
+ method.overridden_method.tag(tag_name)
275
+ end
276
+
249
277
  # Given a YARD NamespaceObject, add lines defining its methods and their
250
278
  # signatures to the current file.
251
279
  # @param [YARD::CodeObjects::NamespaceObject] item
252
280
  # @return [void]
253
281
  def add_methods(item)
254
282
  item.meths(inherited: false).each do |meth|
283
+ next if @hide_private && meth.visibility == :private
255
284
  count_method
256
285
 
257
286
  # If the method is an alias, skip it so we don't define it as a
@@ -267,17 +296,18 @@ module Sord
267
296
 
268
297
  # Sort parameters
269
298
  meth.parameters.reverse.sort! { |pair1, pair2| sort_params(pair1, pair2) }
299
+
270
300
  # This is better than iterating over YARD's "@param" tags directly
271
301
  # because it includes parameters without documentation
272
302
  # (The gsubs allow for better splat-argument compatibility)
273
303
  parameter_names_and_defaults_to_tags = meth.parameters.map do |name, default|
274
- [[name, fix_default_if_unary_minus(default)], meth.tags('param')
304
+ [[name, fix_default_if_unary_minus(default)], method_tags(meth, 'param')
275
305
  .find { |p| p.name&.gsub('*', '')&.gsub(':', '') == name.gsub('*', '').gsub(':', '') }]
276
306
  end.to_h
277
307
 
278
308
  # Add block param if there is no named param but YARD tags are present
279
309
  if !parameter_names_and_defaults_to_tags.any? { |((name, _), _)| name.start_with? '&' } \
280
- && (meth.tags('yieldparam').any? || meth.tag('yieldreturn'))
310
+ && (method_tags(meth, 'yieldparam').any? || method_tag(meth, 'yieldreturn'))
281
311
  parameter_names_and_defaults_to_tags[['&blk']] = nil
282
312
  end
283
313
 
@@ -285,22 +315,22 @@ module Sord
285
315
  name = name_and_default.first
286
316
 
287
317
  if tag
288
- TypeConverter.yard_to_parlour(tag.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
318
+ TypeConverter.yard_to_parlour(tag.types, meth, @type_converter_config)
289
319
  elsif name.start_with? '&'
290
320
  # Find yieldparams and yieldreturn
291
- yieldparams = meth.tags('yieldparam')
292
- yieldreturn = meth.tag('yieldreturn')&.types
321
+ yieldparams = method_tags(meth, 'yieldparam')
322
+ yieldreturn = method_tag(meth, 'yieldreturn')&.types
293
323
  yieldreturn = nil if yieldreturn&.length == 1 &&
294
324
  yieldreturn&.first&.downcase == 'void'
295
325
 
296
326
  # Create strings
297
- params = yieldparams.map do |param|
327
+ params = yieldparams.map.with_index do |param, i|
298
328
  Parlour::Types::Proc::Parameter.new(
299
- param.name.gsub('*', ''),
300
- TypeConverter.yard_to_parlour(param.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
329
+ param.name&.gsub('*', '') || "arg#{i}",
330
+ TypeConverter.yard_to_parlour(param.types, meth, @type_converter_config)
301
331
  )
302
332
  end
303
- returns = TypeConverter.yard_to_parlour(yieldreturn, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
333
+ returns = TypeConverter.yard_to_parlour(yieldreturn, meth, @type_converter_config)
304
334
 
305
335
  # Create proc types, if possible
306
336
  if yieldparams.empty? && yieldreturn.nil?
@@ -315,24 +345,24 @@ module Sord
315
345
 
316
346
  unless getter
317
347
  if parameter_names_and_defaults_to_tags.length == 1 \
318
- && meth.tags('param').length == 1 \
319
- && meth.tag('param').types
348
+ && method_tags(meth, 'param').length == 1 \
349
+ && method_tag(meth, 'param').types
320
350
 
321
351
  Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
322
- next TypeConverter.yard_to_parlour(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
352
+ next TypeConverter.yard_to_parlour(method_tag(meth, 'param').types, meth, @type_converter_config)
323
353
  else
324
354
  Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
325
355
  next Parlour::Types::Untyped.new
326
356
  end
327
357
  end
328
358
 
329
- return_types = getter.tags('return').flat_map(&:types)
359
+ return_types = method_tags(getter, 'return').flat_map(&:types)
330
360
  unless return_types.any?
331
361
  Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
332
362
  next Parlour::Types::Untyped.new
333
363
  end
334
364
  inferred_type = TypeConverter.yard_to_parlour(
335
- return_types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
365
+ return_types, meth, @type_converter_config)
336
366
 
337
367
  Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type.describe} using getter's return type", meth)
338
368
  inferred_type
@@ -340,11 +370,11 @@ module Sord
340
370
  # Is this the only argument, and was a @param specified without an
341
371
  # argument name? If so, infer it
342
372
  if parameter_names_and_defaults_to_tags.length == 1 \
343
- && meth.tags('param').length == 1 \
344
- && meth.tag('param').types
373
+ && method_tags(meth, 'param').length == 1 \
374
+ && method_tag(meth, 'param').types
345
375
 
346
376
  Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
347
- TypeConverter.yard_to_parlour(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
377
+ TypeConverter.yard_to_parlour(method_tag(meth, 'param').types, meth, @type_converter_config)
348
378
  else
349
379
  Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
350
380
  Parlour::Types::Untyped.new
@@ -352,7 +382,7 @@ module Sord
352
382
  end
353
383
  end
354
384
 
355
- return_tags = meth.tags('return')
385
+ return_tags = method_tags(meth, 'return')
356
386
  returns = if meth.name == :initialize && !@use_original_initialize_return
357
387
  nil
358
388
  elsif return_tags.length == 0
@@ -361,7 +391,7 @@ module Sord
361
391
  elsif return_tags.length == 1 && %w{void nil}.include?(return_tags&.first&.types&.first&.downcase)
362
392
  nil
363
393
  else
364
- TypeConverter.yard_to_parlour(meth.tag('return').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
394
+ TypeConverter.yard_to_parlour(method_tag(meth, 'return').types, meth, @type_converter_config)
365
395
  end
366
396
 
367
397
  rbs_block = nil
@@ -447,10 +477,12 @@ module Sord
447
477
  # Get all given types
448
478
  yard_types = []
449
479
  if reader
480
+ next if @hide_private && reader.visibility == :private
450
481
  yard_types += reader.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
451
482
  reader.tags('param').flat_map(&:types)
452
483
  end
453
484
  if writer
485
+ next if @hide_private && writer.visibility == :private
454
486
  yard_types += writer.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
455
487
  writer.tags('param').flat_map(&:types)
456
488
  end
@@ -465,7 +497,7 @@ module Sord
465
497
  parlour_type = Parlour::Types::Untyped.new
466
498
  else
467
499
  parlour_type = TypeConverter.yard_to_parlour(
468
- yard_types, reader || writer, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
500
+ yard_types, reader || writer, @type_converter_config)
469
501
  end
470
502
 
471
503
  # Generate attribute
@@ -538,6 +570,7 @@ module Sord
538
570
  # @param [YARD::CodeObjects::NamespaceObject] item
539
571
  # @return [void]
540
572
  def add_namespace(item)
573
+ return if @hide_private && item.visibility == :private
541
574
  count_namespace
542
575
 
543
576
  superclass = nil
@@ -1,5 +1,6 @@
1
1
  # typed: true
2
2
  require 'parlour'
3
+ require 'yard/tags/library'
3
4
 
4
5
  module Sord
5
6
  class ParlourPlugin < Parlour::Plugin
@@ -42,17 +43,21 @@ module Sord
42
43
  Sord::Logging.error('No output format given; please specify --rbi or --rbs')
43
44
  exit 1
44
45
  end
45
-
46
+
46
47
  if (options[:rbi] && options[:rbs])
47
48
  Sord::Logging.error('You cannot specify both --rbi and --rbs; please use only one')
48
49
  exit 1
49
50
  end
50
-
51
+
51
52
  if options[:regenerate]
52
53
  begin
53
54
  Sord::Logging.info('Running YARD...')
54
55
  Sord::ParlourPlugin.with_clean_env do
55
- system('bundle exec yard')
56
+ tag_param = ''
57
+ options[:tags]&.each do |tag|
58
+ tag_param += "--tag #{tag} "
59
+ end
60
+ system("bundle exec yard #{tag_param} --no-output")
56
61
  end
57
62
  rescue Errno::ENOENT
58
63
  Sord::Logging.error('The YARD tool could not be found on your PATH.')
@@ -63,15 +68,26 @@ module Sord
63
68
  end
64
69
 
65
70
  options[:mode] = \
66
- if options[:rbi] then :rbi elsif options[:rbs] then :rbs end
71
+ if options[:rbi] then :rbi elsif options[:rbs] then :rbs end
67
72
  options[:parlour] = @parlour
68
73
  options[:root] = root
69
74
 
75
+ add_custom_tags
76
+
70
77
  Sord::Generator.new(options).run
71
78
 
72
79
  true
73
80
  end
74
81
 
82
+ def add_custom_tags
83
+ return if options[:tags].empty?
84
+
85
+ options[:tags].each do |tag|
86
+ name, description = tag.split(':')
87
+ YARD::Tags::Library.define_tag(description, name)
88
+ end
89
+ end
90
+
75
91
  def self.with_clean_env &block
76
92
  meth = if Bundler.respond_to?(:with_unbundled_env)
77
93
  :with_unbundled_env
@@ -81,4 +97,4 @@ module Sord
81
97
  Bundler.send meth, &block
82
98
  end
83
99
  end
84
- end
100
+ end
data/lib/sord/resolver.rb CHANGED
@@ -1,23 +1,80 @@
1
1
  # typed: false
2
2
  require 'stringio'
3
+ require 'rbs'
4
+ require 'rbs/cli'
3
5
 
4
6
  module Sord
5
7
  module Resolver
6
8
  # @return [void]
7
9
  def self.prepare
10
+ return @names_to_paths if @names_to_paths
11
+
12
+ gem_objects = {}
13
+ load_gem_objects(gem_objects)
14
+
8
15
  # Construct a hash of class names to full paths
9
- @@names_to_paths ||= YARD::Registry.all(:class)
16
+ @names_to_paths = YARD::Registry.all(:class)
10
17
  .group_by(&:name)
11
- .map { |k, v| [k.to_s, v.map(&:path)] }
18
+ .map { |k, v| [k.to_s, v.map(&:path).to_set] }
12
19
  .to_h
13
- .merge(builtin_classes.map { |x| [x, [x]] }.to_h) do |k, a, b|
14
- a | b
20
+ .merge(builtin_classes.map { |x| [x, Set.new([x])] }.to_h) { |_k, a, b| a.union(b) }
21
+ .merge(gem_objects) { |_k, a, b| a.union(b) }
22
+ end
23
+
24
+ def self.load_gem_objects(hash)
25
+ all_decls = []
26
+ begin
27
+ RBS::CLI::LibraryOptions.new.loader.load(env: all_decls)
28
+ rescue RBS::Collection::Config::CollectionNotAvailable
29
+ Sord::Logging.warn("Could not load RBS collection - run rbs collection install for dependencies")
30
+ end
31
+ add_rbs_objects_to_paths(all_decls, hash)
32
+
33
+ gem_paths = Bundler.load.specs.map(&:full_gem_path)
34
+ gem_paths.each do |path|
35
+ if File.exists?("#{path}/rbi")
36
+ Dir["#{path}/rbi/**/*.rbi"].each do |sigfile|
37
+ tree = Parlour::TypeLoader.load_file(sigfile)
38
+ add_rbi_objects_to_paths(tree.children, hash)
39
+ end
15
40
  end
41
+ end
42
+ end
43
+
44
+ def self.add_rbs_objects_to_paths(all_decls, names_to_paths, path=[])
45
+ klasses = [
46
+ RBS::AST::Declarations::Module,
47
+ RBS::AST::Declarations::Class,
48
+ RBS::AST::Declarations::Constant
49
+ ]
50
+ all_decls.each do |decl|
51
+ next unless klasses.include?(decl.class)
52
+ name = decl.name.to_s
53
+ new_path = path + [name]
54
+ names_to_paths[name] ||= Set.new
55
+ names_to_paths[name] << new_path.join('::')
56
+ add_rbs_objects_to_paths(decl.members, names_to_paths, new_path) if decl.respond_to?(:members)
57
+ end
58
+ end
59
+
60
+ def self.add_rbi_objects_to_paths(nodes, names_to_paths, path=[])
61
+ klasses = [
62
+ Parlour::RbiGenerator::Constant,
63
+ Parlour::RbiGenerator::ModuleNamespace,
64
+ Parlour::RbiGenerator::ClassNamespace
65
+ ]
66
+ nodes.each do |node|
67
+ next unless klasses.include?(node.class)
68
+ new_path = path + [node.name]
69
+ names_to_paths[node.name] ||= Set.new
70
+ names_to_paths[node.name] << new_path.join('::')
71
+ add_rbi_objects_to_paths(node.children, names_to_paths, new_path) if node.respond_to?(:children)
72
+ end
16
73
  end
17
74
 
18
75
  # @return [void]
19
76
  def self.clear
20
- @@names_to_paths = nil
77
+ @names_to_paths = nil
21
78
  end
22
79
 
23
80
  # @param [String] name
@@ -28,7 +85,7 @@ module Sord
28
85
  # If the name starts with ::, then we've been given an explicit path from root - just use that
29
86
  return [name] if name.start_with?('::')
30
87
 
31
- (@@names_to_paths[name.split('::').last] || [])
88
+ (@names_to_paths[name.split('::').last] || [])
32
89
  .select { |x| x.end_with?(name) }
33
90
  end
34
91
 
@@ -43,9 +100,9 @@ module Sord
43
100
  # This prints some deprecation warnings, so suppress them
44
101
  prev_stderr = $stderr
45
102
  $stderr = StringIO.new
46
-
103
+
47
104
  major = RUBY_VERSION.split('.').first.to_i
48
- sorted_set_removed = major >= 3
105
+ sorted_set_removed = major >= 3
49
106
 
50
107
  Object.constants
51
108
  .reject { |x| sorted_set_removed && x == :SortedSet }
@@ -21,10 +21,15 @@ module Sord
21
21
  GENERIC_TYPE_REGEX =
22
22
  /(#{SIMPLE_TYPE_REGEX})\s*[<{]\s*(.*)\s*[>}]/
23
23
 
24
+ # Matches valid method names.
25
+ # From: https://stackoverflow.com/a/4379197/2626000
26
+ METHOD_NAME_REGEX =
27
+ /(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+\*\/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)/i
28
+
24
29
  # Match duck types which require the object implement one or more methods,
25
30
  # like '#foo', '#foo & #bar', '#foo&#bar&#baz', and '#foo&#bar&#baz&#foo_bar'.
26
31
  DUCK_TYPE_REGEX =
27
- /^\#[a-zA-Z_][\w]*(?:[a-zA-Z_][\w=]*)*(?:( ?\& ?\#)*[a-zA-Z_][\w=]*)*$/
32
+ /^\##{METHOD_NAME_REGEX}(?:\s*\&\s*\##{METHOD_NAME_REGEX})*$/
28
33
 
29
34
  # A regular expression which matches ordered lists in the format of
30
35
  # either "Array(String, Symbol)" or "(String, Symbol)".
@@ -90,17 +95,35 @@ module Sord
90
95
  result
91
96
  end
92
97
 
98
+ # Configuration for how the type converter should work in particular cases.
99
+ class Configuration
100
+ def initialize(replace_errors_with_untyped:, replace_unresolved_with_untyped:, output_language:)
101
+ @output_language = output_language
102
+ @replace_errors_with_untyped = replace_errors_with_untyped
103
+ @replace_unresolved_with_untyped = replace_unresolved_with_untyped
104
+ end
105
+
106
+ # The language which the generated types will be converted to - one of
107
+ # `:rbi` or `:rbs`.
108
+ attr_accessor :output_language
109
+
110
+ # @return [Boolean] If true, T.untyped is used instead of SORD_ERROR_
111
+ # constants for unknown types.
112
+ attr_accessor :replace_errors_with_untyped
113
+
114
+ # @param [Boolean] replace_unresolved_with_untyped If true, T.untyped is
115
+ # used when Sord is unable to resolve a constant.
116
+ attr_accessor :replace_unresolved_with_untyped
117
+ end
118
+
93
119
  # Converts a YARD type into a Parlour type.
94
120
  # @param [Boolean, Array, String] yard The YARD type.
95
121
  # @param [YARD::CodeObjects::Base] item The CodeObject which the YARD type
96
122
  # is associated with. This is used for logging and can be nil, but this
97
123
  # will lead to less informative log messages.
98
- # @param [Boolean] replace_errors_with_untyped If true, T.untyped is used
99
- # instead of SORD_ERROR_ constants for unknown types.
100
- # @param [Boolean] replace_unresolved_with_untyped If true, T.untyped is used
101
- # when Sord is unable to resolve a constant.
124
+ # @param [Configuration] config The generation configuration.
102
125
  # @return [Parlour::Types::Type]
103
- def self.yard_to_parlour(yard, item = nil, replace_errors_with_untyped = false, replace_unresolved_with_untyped = false)
126
+ def self.yard_to_parlour(yard, item, config)
104
127
  case yard
105
128
  when nil # Type not specified
106
129
  Parlour::Types::Untyped.new
@@ -113,7 +136,7 @@ module Sord
113
136
  # selection of any of the types
114
137
  types = yard
115
138
  .reject { |x| x == 'nil' }
116
- .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
139
+ .map { |x| yard_to_parlour(x, item, config) }
117
140
  .uniq(&:hash)
118
141
  result = types.length == 1 \
119
142
  ? types.first
@@ -142,7 +165,7 @@ module Sord
142
165
  unless yard == new_path
143
166
  Parlour::Types::Raw.new(new_path)
144
167
  else
145
- if replace_unresolved_with_untyped
168
+ if config.replace_unresolved_with_untyped
146
169
  Logging.warn("#{yard} wasn't able to be resolved to a constant in this project, replaced with untyped", item)
147
170
  Parlour::Types::Untyped.new
148
171
  else
@@ -154,33 +177,43 @@ module Sord
154
177
  Parlour::Types::Raw.new(yard)
155
178
  end
156
179
  when DUCK_TYPE_REGEX
157
- Logging.duck("#{yard} looks like a duck type, replacing with untyped", item)
158
- Parlour::Types::Untyped.new
180
+ if config.output_language == :rbs && (type = duck_type_to_rbs_type(yard))
181
+ Logging.duck("#{yard} looks like a duck type with an equivalent RBS interface, replacing with #{type.generate_rbs}", item)
182
+ type
183
+ else
184
+ Logging.duck("#{yard} looks like a duck type, replacing with untyped", item)
185
+ Parlour::Types::Untyped.new
186
+ end
159
187
  when /^#{GENERIC_TYPE_REGEX}$/
160
188
  generic_type = $1
161
189
  type_parameters = $2
162
190
 
191
+ # If we don't do this, `const_defined?` will resolve "::Array" as the actual Ruby `Array`
192
+ # type, not `Parlour::Types::Array`!
193
+ relative_generic_type = generic_type.start_with?('::') \
194
+ ? generic_type[2..-1] : generic_type
195
+
163
196
  parameters = split_type_parameters(type_parameters)
164
- .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
165
- if SINGLE_ARG_GENERIC_TYPES.include?(generic_type) && parameters.length > 1
166
- Parlour::Types.const_get(generic_type).new(Parlour::Types::Union.new(parameters))
167
- elsif generic_type == 'Class' && parameters.length == 1
197
+ .map { |x| yard_to_parlour(x, item, config) }
198
+ if SINGLE_ARG_GENERIC_TYPES.include?(relative_generic_type) && parameters.length > 1
199
+ Parlour::Types.const_get(relative_generic_type).new(Parlour::Types::Union.new(parameters))
200
+ elsif relative_generic_type == 'Class' && parameters.length == 1
168
201
  Parlour::Types::Class.new(parameters.first)
169
- elsif generic_type == 'Hash'
202
+ elsif relative_generic_type == 'Hash'
170
203
  if parameters.length == 2
171
204
  Parlour::Types::Hash.new(*parameters)
172
205
  else
173
- handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
206
+ handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, config.replace_errors_with_untyped)
174
207
  end
175
208
  else
176
- if Parlour::Types.const_defined?(generic_type)
209
+ if Parlour::Types.constants.include?(relative_generic_type.to_sym)
177
210
  # This generic is built in to parlour, but sord doesn't
178
211
  # explicitly know about it.
179
- Parlour::Types.const_get(generic_type).new(*parameters)
212
+ Parlour::Types.const_get(relative_generic_type).new(*parameters)
180
213
  else
181
214
  # This is a user defined generic
182
215
  Parlour::Types::Generic.new(
183
- yard_to_parlour(generic_type),
216
+ yard_to_parlour(generic_type, nil, config),
184
217
  parameters
185
218
  )
186
219
  end
@@ -190,22 +223,22 @@ module Sord
190
223
  when ORDERED_LIST_REGEX
191
224
  type_parameters = $1
192
225
  parameters = split_type_parameters(type_parameters)
193
- .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
226
+ .map { |x| yard_to_parlour(x, item, config) }
194
227
  Parlour::Types::Tuple.new(parameters)
195
228
  when SHORTHAND_HASH_SYNTAX
196
229
  type_parameters = $1
197
230
  parameters = split_type_parameters(type_parameters)
198
- .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
231
+ .map { |x| yard_to_parlour(x, item, config) }
199
232
  # Return a warning about an invalid hash when it has more or less than two elements.
200
233
  if parameters.length == 2
201
234
  Parlour::Types::Hash.new(*parameters)
202
235
  else
203
- handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
236
+ handle_sord_error(parameters.map(&:describe).join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, config.replace_errors_with_untyped)
204
237
  end
205
238
  when SHORTHAND_ARRAY_SYNTAX
206
239
  type_parameters = $1
207
240
  parameters = split_type_parameters(type_parameters)
208
- .map { |x| yard_to_parlour(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
241
+ .map { |x| yard_to_parlour(x, item, config) }
209
242
  parameters.one? \
210
243
  ? Parlour::Types::Array.new(parameters.first)
211
244
  : Parlour::Types::Array.new(Parlour::Types::Union.new(parameters))
@@ -215,7 +248,7 @@ module Sord
215
248
  return Parlour::Types::Raw.new(from_yaml.class.to_s) \
216
249
  if [Symbol, Float, Integer].include?(from_yaml.class)
217
250
 
218
- return handle_sord_error(yard.to_s, "#{yard.inspect} does not appear to be a type", item, replace_errors_with_untyped)
251
+ return handle_sord_error(yard.to_s, "#{yard.inspect} does not appear to be a type", item, config.replace_errors_with_untyped)
219
252
  end
220
253
  end
221
254
 
@@ -233,5 +266,50 @@ module Sord
233
266
  ? Parlour::Types::Untyped.new
234
267
  : Parlour::Types::Raw.new("SORD_ERROR_#{name.gsub(/[^0-9A-Za-z_]/i, '')}")
235
268
  end
269
+
270
+ # Taken from: https://github.com/ruby/rbs/blob/master/core/builtin.rbs
271
+ # When the latest commit was: 6c847d1
272
+ #
273
+ # Interfaces which use generic arguments have those arguments as `untyped`, since I'm not aware
274
+ # of any standard way that these are specified.
275
+ DUCK_TYPES_TO_RBS_TYPE_NAMES = {
276
+ # Concrete
277
+ "#to_i" => "_ToI",
278
+ "#to_int" => "_ToInt",
279
+ "#to_r" => "_ToR",
280
+ "#to_s" => "_ToS",
281
+ "#to_str" => "_ToStr",
282
+ "#to_proc" => "_ToProc",
283
+ "#to_path" => "_ToPath",
284
+ "#read" => "_Reader",
285
+ "#readpartial" => "_ReaderPartial",
286
+ "#write" => "_Writer",
287
+ "#rewind" => "_Rewindable",
288
+ "#to_io" => "_ToIO",
289
+ "#exception" => "_Exception",
290
+
291
+ # Generic - these will be put in a `Types::Raw`, so writing RBS syntax is a little devious,
292
+ # but by their nature we know they'll only be used in an RBS file, so it's probably fine
293
+ "#to_hash" => "_ToHash[untyped, untyped]",
294
+ "#each" => "_Each[untyped]",
295
+ }
296
+
297
+ # Given a YARD duck type string, attempts to convert it to one of a list of pre-defined RBS
298
+ # built-in interfaces.
299
+ #
300
+ # For example, the common duck type `#to_s` has a built-in RBS equivalent `_ToS`.
301
+ #
302
+ # If no such interface exists, returns `nil`.
303
+ #
304
+ # @param [String] type
305
+ # @return [Parlour::Types::Type, nil]
306
+ def self.duck_type_to_rbs_type(type)
307
+ type_name = DUCK_TYPES_TO_RBS_TYPE_NAMES[type]
308
+ if !type_name.nil?
309
+ Parlour::Types::Raw.new(type_name)
310
+ else
311
+ nil
312
+ end
313
+ end
236
314
  end
237
315
  end
data/lib/sord/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # typed: strong
2
2
  module Sord
3
- VERSION = '4.0.0'
3
+ VERSION = '5.0.0'
4
4
  end
data/rbi/sord.rbi CHANGED
@@ -1,7 +1,7 @@
1
1
  # typed: strong
2
-
2
+ # typed: strong
3
3
  module Sord
4
- VERSION = T.let('3.0.1', T.untyped)
4
+ VERSION = T.let('5.0.0', T.untyped)
5
5
 
6
6
  # Handles writing logs to stdout and any other classes which request them.
7
7
  module Logging
@@ -42,6 +42,7 @@ module Sord
42
42
  def self.valid_types?(value); end
43
43
 
44
44
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
45
+ # sord omit - no YARD type given for "**opts", using untyped
45
46
  # A generic log message writer which is called by all other specific logging
46
47
  # methods. This shouldn't be called outside of the Logging class itself.
47
48
  #
@@ -57,32 +58,36 @@ module Sord
57
58
  kind: Symbol,
58
59
  header: String,
59
60
  msg: String,
60
- item: YARD::CodeObjects::Base
61
+ item: YARD::CodeObjects::Base,
62
+ opts: T.untyped
61
63
  ).void
62
64
  end
63
- def self.generic(kind, header, msg, item); end
65
+ def self.generic(kind, header, msg, item, **opts); end
64
66
 
65
67
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
68
+ # sord omit - no YARD type given for "**opts", using untyped
66
69
  # Print a warning message. This should be used for things which require the
67
70
  # user's attention but do not prevent the process from stopping.
68
71
  #
69
72
  # _@param_ `msg` — The log message to write.
70
73
  #
71
74
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
72
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
73
- def self.warn(msg, item = nil); end
75
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
76
+ def self.warn(msg, item = nil, **opts); end
74
77
 
75
78
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
79
+ # sord omit - no YARD type given for "**opts", using untyped
76
80
  # Print an info message. This should be used for generic informational
77
81
  # messages which the user doesn't need to act on.
78
82
  #
79
83
  # _@param_ `msg` — The log message to write.
80
84
  #
81
85
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
82
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
83
- def self.info(msg, item = nil); end
86
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
87
+ def self.info(msg, item = nil, **opts); end
84
88
 
85
89
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
90
+ # sord omit - no YARD type given for "**opts", using untyped
86
91
  # Print a duck-typing message. This should be used when the YARD
87
92
  # documentation contains duck typing, which isn't supported by Sorbet, so
88
93
  # it is substituted for something different.
@@ -90,20 +95,22 @@ module Sord
90
95
  # _@param_ `msg` — The log message to write.
91
96
  #
92
97
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
93
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
94
- def self.duck(msg, item = nil); end
98
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
99
+ def self.duck(msg, item = nil, **opts); end
95
100
 
96
101
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
102
+ # sord omit - no YARD type given for "**opts", using untyped
97
103
  # Print an error message. This should be used for things which require the
98
104
  # current process to stop.
99
105
  #
100
106
  # _@param_ `msg` — The log message to write.
101
107
  #
102
108
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
103
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
104
- def self.error(msg, item = nil); end
109
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
110
+ def self.error(msg, item = nil, **opts); end
105
111
 
106
112
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
113
+ # sord omit - no YARD type given for "**opts", using untyped
107
114
  # Print an infer message. This should be used when the user should be told
108
115
  # that some information has been filled in or guessed for them, and that
109
116
  # information is likely correct.
@@ -111,10 +118,11 @@ module Sord
111
118
  # _@param_ `msg` — The log message to write.
112
119
  #
113
120
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
114
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
115
- def self.infer(msg, item = nil); end
121
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
122
+ def self.infer(msg, item = nil, **opts); end
116
123
 
117
124
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
125
+ # sord omit - no YARD type given for "**opts", using untyped
118
126
  # Print an omit message. This should be used as a special type of warning
119
127
  # to alert the user that there is some information missing, but this
120
128
  # information is not critical to the completion of the process.
@@ -122,20 +130,22 @@ module Sord
122
130
  # _@param_ `msg` — The log message to write.
123
131
  #
124
132
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
125
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
126
- def self.omit(msg, item = nil); end
133
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
134
+ def self.omit(msg, item = nil, **opts); end
127
135
 
128
136
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
137
+ # sord omit - no YARD type given for "**opts", using untyped
129
138
  # Print a done message. This should be used when a process completes
130
139
  # successfully.
131
140
  #
132
141
  # _@param_ `msg` — The log message to write.
133
142
  #
134
143
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
135
- sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base)).void }
136
- def self.done(msg, item = nil); end
144
+ sig { params(msg: String, item: T.nilable(YARD::CodeObjects::Base), opts: T.untyped).void }
145
+ def self.done(msg, item = nil, **opts); end
137
146
 
138
147
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
148
+ # sord omit - no YARD type given for "**opts", using untyped
139
149
  # Invokes all registered hooks on the logger.
140
150
  #
141
151
  # _@param_ `kind` — The kind of log message this is.
@@ -143,8 +153,15 @@ module Sord
143
153
  # _@param_ `msg` — The log message to write.
144
154
  #
145
155
  # _@param_ `item` — The CodeObject which this log is associated with, if any. This is shown before the log message if it is specified.
146
- sig { params(kind: Symbol, msg: String, item: YARD::CodeObjects::Base).void }
147
- def self.invoke_hooks(kind, msg, item); end
156
+ sig do
157
+ params(
158
+ kind: Symbol,
159
+ msg: String,
160
+ item: YARD::CodeObjects::Base,
161
+ opts: T.untyped
162
+ ).void
163
+ end
164
+ def self.invoke_hooks(kind, msg, item, **opts); end
148
165
 
149
166
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
150
167
  # Adds a hook to the logger.
@@ -156,6 +173,25 @@ module Sord
156
173
  sig { void }
157
174
  def self.prepare; end
158
175
 
176
+ # sord omit - no YARD type given for "hash", using untyped
177
+ # sord omit - no YARD return type given, using untyped
178
+ sig { params(hash: T.untyped).returns(T.untyped) }
179
+ def self.load_gem_objects(hash); end
180
+
181
+ # sord omit - no YARD type given for "all_decls", using untyped
182
+ # sord omit - no YARD type given for "names_to_paths", using untyped
183
+ # sord omit - no YARD type given for "path", using untyped
184
+ # sord omit - no YARD return type given, using untyped
185
+ sig { params(all_decls: T.untyped, names_to_paths: T.untyped, path: T.untyped).returns(T.untyped) }
186
+ def self.add_rbs_objects_to_paths(all_decls, names_to_paths, path = []); end
187
+
188
+ # sord omit - no YARD type given for "nodes", using untyped
189
+ # sord omit - no YARD type given for "names_to_paths", using untyped
190
+ # sord omit - no YARD type given for "path", using untyped
191
+ # sord omit - no YARD return type given, using untyped
192
+ sig { params(nodes: T.untyped, names_to_paths: T.untyped, path: T.untyped).returns(T.untyped) }
193
+ def self.add_rbi_objects_to_paths(nodes, names_to_paths, path = []); end
194
+
159
195
  sig { void }
160
196
  def self.clear; end
161
197
 
@@ -182,7 +218,7 @@ module Sord
182
218
  class Generator
183
219
  VALID_MODES = T.let([:rbi, :rbs], T.untyped)
184
220
 
185
- # _@return_ — The number of objects this generator has processed so
221
+ # _@return_ — The number of objects this generator has processed so
186
222
  # far.
187
223
  sig { returns(Integer) }
188
224
  def object_count; end
@@ -217,7 +253,6 @@ module Sord
217
253
  def add_constants(item); end
218
254
 
219
255
  # sord warn - YARD::CodeObjects::NamespaceObject wasn't able to be resolved to a constant in this project
220
- # sord warn - Parlour::TypedObject wasn't able to be resolved to a constant in this project
221
256
  # Adds comments to an object based on a docstring.
222
257
  #
223
258
  # _@param_ `item`
@@ -226,6 +261,22 @@ module Sord
226
261
  sig { params(item: YARD::CodeObjects::NamespaceObject, typed_object: Parlour::TypedObject).void }
227
262
  def add_comments(item, typed_object); end
228
263
 
264
+ # sord warn - YARD::CodeObjects::MethodObject wasn't able to be resolved to a constant in this project
265
+ # sord warn - YARD::Tags::Tag wasn't able to be resolved to a constant in this project
266
+ # _@param_ `method`
267
+ #
268
+ # _@param_ `tag_name`
269
+ sig { params(method: YARD::CodeObjects::MethodObject, tag_name: String).returns(T::Array[YARD::Tags::Tag]) }
270
+ def method_tags(method, tag_name); end
271
+
272
+ # sord warn - YARD::CodeObjects::MethodObject wasn't able to be resolved to a constant in this project
273
+ # sord warn - YARD::Tags::Tag wasn't able to be resolved to a constant in this project
274
+ # _@param_ `method`
275
+ #
276
+ # _@param_ `tag_name`
277
+ sig { params(method: YARD::CodeObjects::MethodObject, tag_name: String).returns(T.nilable(YARD::Tags::Tag)) }
278
+ def method_tag(method, tag_name); end
279
+
229
280
  # sord warn - YARD::CodeObjects::NamespaceObject wasn't able to be resolved to a constant in this project
230
281
  # Given a YARD NamespaceObject, add lines defining its methods and their
231
282
  # signatures to the current file.
@@ -250,7 +301,7 @@ module Sord
250
301
  sig { params(item: YARD::CodeObjects::NamespaceObject).void }
251
302
  def add_namespace(item); end
252
303
 
253
- # Populates the generator with the contents of the YARD registry. You
304
+ # Populates the generator with the contents of the YARD registry. You
254
305
  # must load the YARD registry first!
255
306
  sig { void }
256
307
  def populate; end
@@ -279,8 +330,15 @@ module Sord
279
330
  sig { params(pair1: T::Array[T.untyped], pair2: T::Array[T.untyped]).returns(T.untyped) }
280
331
  def sort_params(pair1, pair2); end
281
332
 
333
+ # Removes the last character of a default parameter value if it begins with
334
+ # '-', working around a bug in YARD. (See lsegal/yard #894)
335
+ #
336
+ # _@param_ `default`
337
+ sig { params(default: String).returns(T.nilable(String)) }
338
+ def fix_default_if_unary_minus(default); end
339
+
282
340
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
283
- # _@return_ — The
341
+ # _@return_ — The
284
342
  # errors encountered by by the generator. Each element is of the form
285
343
  # [message, item, line].
286
344
  sig { returns(T::Array[[String, YARD::CodeObjects::Base, Integer]]) }
@@ -297,6 +355,10 @@ module Sord
297
355
  sig { params(root: T.untyped).returns(T.untyped) }
298
356
  def generate(root); end
299
357
 
358
+ # sord omit - no YARD return type given, using untyped
359
+ sig { returns(T.untyped) }
360
+ def add_custom_tags; end
361
+
300
362
  # sord omit - no YARD return type given, using untyped
301
363
  sig { params(block: T.untyped).returns(T.untyped) }
302
364
  def self.with_clean_env(&block); end
@@ -315,11 +377,33 @@ module Sord
315
377
  module TypeConverter
316
378
  SIMPLE_TYPE_REGEX = T.let(/(?:\:\:)?[a-zA-Z_][\w]*(?:\:\:[a-zA-Z_][\w]*)*/, T.untyped)
317
379
  GENERIC_TYPE_REGEX = T.let(/(#{SIMPLE_TYPE_REGEX})\s*[<{]\s*(.*)\s*[>}]/, T.untyped)
318
- DUCK_TYPE_REGEX = T.let(/^\#[a-zA-Z_][\w]*(?:[a-zA-Z_][\w=]*)*(?:( ?\& ?\#)*[a-zA-Z_][\w=]*)*$/, T.untyped)
380
+ METHOD_NAME_REGEX = T.let(/(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+\*\/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)/i, T.untyped)
381
+ DUCK_TYPE_REGEX = T.let(/^\##{METHOD_NAME_REGEX}(?:\s*\&\s*\##{METHOD_NAME_REGEX})*$/, T.untyped)
319
382
  ORDERED_LIST_REGEX = T.let(/^(?:Array|)\((.*)\s*\)$/, T.untyped)
320
383
  SHORTHAND_HASH_SYNTAX = T.let(/^{\s*(.*)\s*}$/, T.untyped)
321
384
  SHORTHAND_ARRAY_SYNTAX = T.let(/^<\s*(.*)\s*>$/, T.untyped)
322
385
  SINGLE_ARG_GENERIC_TYPES = T.let(%w{Array Set Enumerable Enumerator Range}, T.untyped)
386
+ DUCK_TYPES_TO_RBS_TYPE_NAMES = T.let({
387
+ # Concrete
388
+ "#to_i" => "_ToI",
389
+ "#to_int" => "_ToInt",
390
+ "#to_r" => "_ToR",
391
+ "#to_s" => "_ToS",
392
+ "#to_str" => "_ToStr",
393
+ "#to_proc" => "_ToProc",
394
+ "#to_path" => "_ToPath",
395
+ "#read" => "_Reader",
396
+ "#readpartial" => "_ReaderPartial",
397
+ "#write" => "_Writer",
398
+ "#rewind" => "_Rewindable",
399
+ "#to_io" => "_ToIO",
400
+ "#exception" => "_Exception",
401
+
402
+ # Generic - these will be put in a `Types::Raw`, so writing RBS syntax is a little devious,
403
+ # but by their nature we know they'll only be used in an RBS file, so it's probably fine
404
+ "#to_hash" => "_ToHash[untyped, untyped]",
405
+ "#each" => "_Each[untyped]",
406
+ }, T.untyped)
323
407
 
324
408
  # Given a string of YARD type parameters (without angle brackets), splits
325
409
  # the string into an array of each type parameter.
@@ -331,29 +415,17 @@ module Sord
331
415
  def self.split_type_parameters(params); end
332
416
 
333
417
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
334
- # sord warn - Parlour::Types::Type wasn't able to be resolved to a constant in this project
335
418
  # Converts a YARD type into a Parlour type.
336
419
  #
337
420
  # _@param_ `yard` — The YARD type.
338
421
  #
339
422
  # _@param_ `item` — The CodeObject which the YARD type is associated with. This is used for logging and can be nil, but this will lead to less informative log messages.
340
423
  #
341
- # _@param_ `replace_errors_with_untyped` — If true, T.untyped is used instead of SORD_ERROR_ constants for unknown types.
342
- #
343
- # _@param_ `replace_unresolved_with_untyped` — If true, T.untyped is used when Sord is unable to resolve a constant.
344
- sig do
345
- params(
346
- yard: T.any(T::Boolean, T::Array[T.untyped], String),
347
- item: T.nilable(YARD::CodeObjects::Base),
348
- replace_errors_with_untyped: T::Boolean,
349
- replace_unresolved_with_untyped: T::Boolean
350
- ).returns(Parlour::Types::Type)
351
- end
352
- def self.yard_to_parlour(yard, item = nil, replace_errors_with_untyped = false, replace_unresolved_with_untyped = false); end
424
+ # _@param_ `config` — The generation configuration.
425
+ sig { params(yard: T.any(T::Boolean, T::Array[T.untyped], String), item: YARD::CodeObjects::Base, config: Configuration).returns(Parlour::Types::Type) }
426
+ def self.yard_to_parlour(yard, item, config); end
353
427
 
354
- # sord warn - Parlour::Types::Type wasn't able to be resolved to a constant in this project
355
428
  # sord warn - YARD::CodeObjects::Base wasn't able to be resolved to a constant in this project
356
- # sord warn - Parlour::Types::Type wasn't able to be resolved to a constant in this project
357
429
  # Handles SORD_ERRORs.
358
430
  #
359
431
  # _@param_ `name`
@@ -372,5 +444,40 @@ module Sord
372
444
  ).returns(Parlour::Types::Type)
373
445
  end
374
446
  def self.handle_sord_error(name, log_warning, item, replace_errors_with_untyped); end
447
+
448
+ # Given a YARD duck type string, attempts to convert it to one of a list of pre-defined RBS
449
+ # built-in interfaces.
450
+ #
451
+ # For example, the common duck type `#to_s` has a built-in RBS equivalent `_ToS`.
452
+ #
453
+ # If no such interface exists, returns `nil`.
454
+ #
455
+ # _@param_ `type`
456
+ sig { params(type: String).returns(T.nilable(Parlour::Types::Type)) }
457
+ def self.duck_type_to_rbs_type(type); end
458
+
459
+ # Configuration for how the type converter should work in particular cases.
460
+ class Configuration
461
+ # sord omit - no YARD type given for "replace_errors_with_untyped:", using untyped
462
+ # sord omit - no YARD type given for "replace_unresolved_with_untyped:", using untyped
463
+ # sord omit - no YARD type given for "output_language:", using untyped
464
+ sig { params(replace_errors_with_untyped: T.untyped, replace_unresolved_with_untyped: T.untyped, output_language: T.untyped).void }
465
+ def initialize(replace_errors_with_untyped:, replace_unresolved_with_untyped:, output_language:); end
466
+
467
+ # sord omit - no YARD type given for :output_language, using untyped
468
+ # The language which the generated types will be converted to - one of
469
+ # `:rbi` or `:rbs`.
470
+ sig { returns(T.untyped) }
471
+ attr_accessor :output_language
472
+
473
+ # _@return_ — If true, T.untyped is used instead of SORD_ERROR_
474
+ # constants for unknown types.
475
+ sig { returns(T::Boolean) }
476
+ attr_accessor :replace_errors_with_untyped
477
+
478
+ # _@param_ `replace_unresolved_with_untyped` — If true, T.untyped is used when Sord is unable to resolve a constant.
479
+ sig { returns(T::Boolean) }
480
+ attr_accessor :replace_unresolved_with_untyped
481
+ end
375
482
  end
376
483
  end
data/sord.gemspec CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'sorbet-runtime'
27
27
  spec.add_dependency 'commander', '~> 4.5'
28
28
  spec.add_dependency 'parlour', '~> 5.0'
29
+ spec.add_dependency 'rbs', '~> 2.0'
29
30
 
30
31
  spec.add_development_dependency "bundler", "~> 2.0"
31
32
  spec.add_development_dependency "rake", "~> 10.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sord
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Christiansen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-19 00:00:00.000000000 Z
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: yard
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rbs
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: bundler
71
85
  requirement: !ruby/object:Gem::Requirement