sord 4.0.0 → 5.0.0

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