sord 0.7.1 → 0.8.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: 0f5c3d1135923a51f529b350dc7fd3c9f42300ab1fda040269b4d9a4d44bd271
4
- data.tar.gz: 3bbd67ff8649ffb37cb50b2082ca62ae2affaea24ed69c9f502a52ccfbf42315
3
+ metadata.gz: a4302b93ba07124b24e91c6db6a2757d2fe148c573f3b86a918c72155e83d0d5
4
+ data.tar.gz: 610ce8222970fb4da8f5d01368a21b1aef4067936f6883b2eb5522e567e8fcce
5
5
  SHA512:
6
- metadata.gz: 1b54a2809ce34de42b24abd32b5a69df07f4461dde01089eb2be35ed480b1e63e57be424882d4d934ca4ecca6de4783b116b913a49484af05df22a30f1c01589
7
- data.tar.gz: 2bc7a9731b69a894432ed65859376169dfdd61bebc9ed87b69b0951b7f7c2b2f41a937a7ecbb48a1f67c0baf34f47cbc0b5c23b8ce39d7551e6124fc41a64022
6
+ metadata.gz: 2a3798003e636dc6baba752bbae52718eefb3f43a5562d7fb3659e39d0a0245c39dd10d297433f05df12f60ac102b93a6ad1b71bc0e1fcb2bf3405233dfb834c
7
+ data.tar.gz: 0b9a87eea2db2c948006c89c0970175c98471f0e98358a190c17e6c8e064ff59c5849ea73b0081ef6cd09975488884e333b359a44f8d79a8dac78d6f7f461c3e
data/.gitignore CHANGED
@@ -11,3 +11,5 @@
11
11
  .rspec_status
12
12
  .vscode/
13
13
  Gemfile.lock
14
+
15
+ sord_examples/
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- Sord is a **So**rbet and YA**RD** crossover. It can automatically generate
6
- Sorbet type signatures files by looking at the types specified in YARD
7
- documentation comments.
5
+ Sord is a [**So**rbet](https://sorbet.org) and [YA**RD**](https://sorbet.org)
6
+ crossover. It can automatically generate Sorbet type signatures files by
7
+ looking at the types specified in YARD documentation comments.
8
8
 
9
9
  If your project is already YARD documented, then this can generate most of the
10
10
  Sorbet signatures you need!
@@ -16,14 +16,17 @@ Sord has the following features:
16
16
  - Can infer setter parameter type from the corresponding getter's return type
17
17
  - Recognises mixins (`include` and `extend`)
18
18
  - Support for generic types such as `Array<T>` and `Hash<K, V>`
19
+ - Can infer namespaced classes (`[Bar]` can become `GemName::Foo::Bar`)
20
+ - Handles return types which can be `nil` (`T.nilable`)
21
+ - Handles duck types (`T.untyped`)
22
+ - Support for ordered list types (`[Array(Integer, Symbol)]` becomes `[Integer, Symbol]`)
23
+ - Support for boolean types (`[true, false]` becomes `T::Boolean`)
24
+ - Support for `&block` parameters documented with `@yieldparam` and `@yieldreturn`
19
25
 
20
26
  ## Usage
21
27
 
22
28
  Install Sord with `gem install sord`.
23
29
 
24
- **NOTE**: You need to run `yard` before you generate the `.rbi` file or
25
- Sord won't have any information to work with.
26
-
27
30
  Sord is a command line tool. To use it, open a terminal in the root directory
28
31
  of your project and invoke `sord`, passing a path where you'd like to save your
29
32
  `.rbi` (this file will be overwritten):
@@ -32,9 +35,18 @@ of your project and invoke `sord`, passing a path where you'd like to save your
32
35
  sord defs.rbi
33
36
  ```
34
37
 
35
- Sord will print information about what it's inferred as it runs. It is best to
36
- fix any issues in the YARD documentation, as any edits made to the resulting
37
- RBI file will be replaced if you re-run Sord.
38
+ Sord will generate YARD docs and then print information about what it's inferred
39
+ as it runs. It is best to fix any issues in the YARD documentation, as any edits
40
+ made to the resulting RBI file will be replaced if you re-run Sord.
41
+
42
+ RBI files generated by Sord can be used in two main ways:
43
+
44
+ - [Shipped in the gem itself](https://sorbet.org/docs/rbi#rbis-within-gems).
45
+ - Contributed to [sorbet-typed](https://github.com/sorbet/sorbet-typed).
46
+
47
+ Generally, you should ship the type signatures with your gem if possible.
48
+ sorbet-typed is meant to be a place for gems that are no longer updated or
49
+ where the maintainer is unwilling to ship type signatures with the gem itself.
38
50
 
39
51
  ### Flags
40
52
 
@@ -42,6 +54,19 @@ Sord also takes some flags to alter the generated `.rbi` file:
42
54
 
43
55
  - `--no-comments`: Generates the `.rbi` file without any comments about
44
56
  warnings/inferences/errors.
57
+ - `--no-regenerate`: By default, Sord will regenerate a repository's YARD
58
+ docs for you. This option skips regenerating the YARD docs.
59
+ - `--break-params`: Determines how many parameters are necessary before
60
+ the signature is changed from a single-line to a multi-line block.
61
+ (Default: 4)
62
+ - `--replace-errors-with-untyped`: Uses `T.untyped` instead of `SORD_ERROR_*` constants.
63
+ - `--include-messages` and `--exclude-messages`: Used to filter the logging
64
+ messages given by Sord. `--include-messages` acts as a whitelist, printing
65
+ only messages of the specified logging kinds, whereas `--exclude-messages`
66
+ acts as a blacklist and suppresses the specified logging kinds. Both flags
67
+ take a comma-separated list of logging kinds, for example `omit,infer`.
68
+ When using `--include-messages`, the `done` kind is included by default.
69
+ (You cannot specify both `--include-messages` and `--exclude-messages`.)
45
70
 
46
71
  ## Example
47
72
 
@@ -89,22 +114,27 @@ The `test.rbi` file then contains a complete RBI file for `test.rb`:
89
114
  ```ruby
90
115
  # typed: strong
91
116
  module Example
92
- class Person
93
- sig { params(name: String, age: Integer).returns(Example::Person) }
94
- def initialize(name, age); end
95
- sig { returns(String) }
96
- def name(); end
97
- # sord infer - inferred type of parameter "value" as String using getter's return type
98
- sig { params(value: String).returns(String) }
99
- def name=(value); end
100
- sig { returns(Integer) }
101
- def age(); end
102
- # sord infer - inferred type of parameter "value" as Integer using getter's return type
103
- sig { params(value: Integer).returns(Integer) }
104
- def age=(value); end
105
- sig { params(possible_names: T::Array[String], possible_ages: T::Array[Integer]).returns(Example::Person) }
106
- def self.construct_randomly(possible_names, possible_ages); end
107
- end
117
+ class Person
118
+ sig { params(name: String, age: Integer).returns(Example::Person) }
119
+ def initialize(name, age); end
120
+
121
+ sig { returns(String) }
122
+ def name(); end
123
+
124
+ # sord infer - inferred type of parameter "value" as String using getter's return type
125
+ sig { params(value: String).returns(String) }
126
+ def name=(value); end
127
+
128
+ sig { returns(Integer) }
129
+ def age(); end
130
+
131
+ # sord infer - inferred type of parameter "value" as Integer using getter's return type
132
+ sig { params(value: Integer).returns(Integer) }
133
+ def age=(value); end
134
+
135
+ sig { params(possible_names: T::Array[String], possible_ages: T::Array[Integer]).returns(Example::Person) }
136
+ def self.construct_randomly(possible_names, possible_ages); end
137
+ end
108
138
  end
109
139
  ```
110
140
 
@@ -124,6 +154,13 @@ The general rule of thumb for type conversions is:
124
154
 
125
155
  Bug reports and pull requests are welcome on GitHub at https://github.com/AaronC81/sord. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
126
156
 
157
+ While contributing, if you want to see the results of your changes to Sord you
158
+ can use the `examples:seed` Rake task. The task uses Sord to generate RBIs for
159
+ a number of open source Ruby gems, including Bundler, Haml, Rouge, and RSpec.
160
+ `rake examples:seed` (and `rake examples:reseed` to regenerate the RBI files)
161
+ will clone the repositories of these gems into `sord_examples/` and then
162
+ generate the RBI files into the same directory.
163
+
127
164
  ## License
128
165
 
129
166
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -4,3 +4,75 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+
8
+ REPOS = {
9
+ addressable: 'https://github.com/sporkmonger/addressable',
10
+ bundler: 'https://github.com/bundler/bundler',
11
+ discordrb: 'https://github.com/meew0/discordrb',
12
+ gitlab: 'https://github.com/NARKOZ/gitlab',
13
+ haml: 'https://github.com/haml/haml',
14
+ oga: 'https://gitlab.com/yorickpeterse/oga',
15
+ rouge: 'https://github.com/rouge-ruby/rouge',
16
+ 'rspec-core': 'https://github.com/rspec/rspec-core',
17
+ yard: 'https://github.com/lsegal/yard',
18
+ zeitwerk: 'https://github.com/fxn/zeitwerk'
19
+ }
20
+
21
+ namespace :examples do
22
+ require 'fileutils'
23
+ require 'colorize'
24
+
25
+ desc "Clone git repositories and run Sord on them as examples"
26
+ task :seed do
27
+ if File.directory?('sord_examples')
28
+ puts 'sord_examples directory already exists, please delete the directory or run a reseed!'.red
29
+ exit
30
+ end
31
+
32
+ FileUtils.mkdir 'sord_examples'
33
+ FileUtils.cd 'sord_examples'
34
+
35
+ Bundler.with_clean_env do
36
+ # Shallow clone each of the repositories, then bundle install and run sord.
37
+ REPOS.each do |name, url|
38
+ puts "Cloning #{name}..."
39
+ system("git clone #{url} --depth=1")
40
+ FileUtils.cd name.to_s
41
+ # Add sord to gemfile.
42
+ `echo "gem 'sord', path: '../../'" >> Gemfile`
43
+ # Run bundle install.
44
+ system('bundle install')
45
+ # Generate sri
46
+ puts "Generating rbi for #{name}..."
47
+ system("bundle exec sord ../#{name}.rbi")
48
+ puts "#{name}.rbi generated!"
49
+ FileUtils.cd '..'
50
+ end
51
+ end
52
+
53
+ puts "Seeding complete!".green
54
+ end
55
+
56
+ desc 'Regenerate the rbi files in sord_examples.'
57
+ task :reseed do
58
+ FileUtils.cd 'sord_examples'
59
+
60
+ REPOS.keys.each do |name|
61
+ FileUtils.cd name.to_s
62
+ puts "Regenerating rbi file for #{name}..."
63
+ Bundler.with_clean_env do
64
+ system("bundle exec sord ../#{name}.rbi --no-regenerate")
65
+ end
66
+ FileUtils.cd '..'
67
+ end
68
+
69
+ puts "Re-seeding complete!".green
70
+ end
71
+
72
+ desc 'Delete the sord_examples directory to allow the seeder to run again.'
73
+ task :reset do
74
+ FileUtils.rm_rf 'sord_examples' if File.directory?('sord_examples')
75
+ puts 'Reset complete.'.green
76
+ end
77
+ end
78
+
data/exe/sord CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'sord'
3
3
  require 'commander/import'
4
+ require 'bundler'
4
5
 
5
6
  program :name, 'sord'
6
7
  program :version, Sord::VERSION
@@ -12,19 +13,53 @@ command :gen do |c|
12
13
  c.description = 'Generates an RBI file from this directory\'s YARD docs'
13
14
  c.option '--[no-]comments', 'Controls informational/warning comments in the RBI file'
14
15
  c.option '--[no-]regenerate', 'Controls whether YARD is executed before Sord runs'
16
+ c.option '--break-params INTEGER', Integer, 'Break params onto their own lines if there are this many'
17
+ c.option '--replace-errors-with-untyped', 'Uses T.untyped rather than SORD_ERROR_ constants'
18
+ c.option '--exclude-messages STRING', String, 'Blacklists a comma-separated string of log message types'
19
+ c.option '--include-messages STRING', String, 'Whitelists a comma-separated string of log message types'
15
20
 
16
21
  c.action do |args, options|
17
- options.default comments: true, regenerate: true
22
+ options.default(
23
+ comments: true,
24
+ regenerate: true,
25
+ break_params: 4,
26
+ replace_errors_with_untyped: false,
27
+ exclude_messages: nil,
28
+ include_messages: nil,
29
+ )
18
30
 
19
31
  if args.length != 1
20
32
  Sord::Logging.error('Must specify filename')
21
33
  exit 1
22
34
  end
23
35
 
36
+ if options.include_messages && options.exclude_messages
37
+ Sord::Logging.error('Please specify only one of --include-messages and --exclude-messages.')
38
+ exit 1
39
+ elsif options.include_messages
40
+ whitelist = options.include_messages.split(',').map { |x| x.downcase.to_sym }
41
+ unless Sord::Logging.valid_types?(whitelist)
42
+ Sord::Logging.error('Not all types on your --include-messages list are valid.')
43
+ Sord::Logging.error("Valid options are: #{Sord::Logging::AVAILABLE_TYPES.map(&:to_s).join(', ')}")
44
+ exit 1
45
+ end
46
+ Sord::Logging.enabled_types = whitelist | [:done]
47
+ elsif options.exclude_messages
48
+ blacklist = options.exclude_messages.split(',').map { |x| x.downcase.to_sym }
49
+ unless Sord::Logging.valid_types?(blacklist)
50
+ Sord::Logging.error('Not all types on your --include-messages list are valid.')
51
+ Sord::Logging.error("Valid options are: #{Sord::Logging::AVAILABLE_TYPES.map(&:to_s).join(', ')}")
52
+ exit 1
53
+ end
54
+ Sord::Logging.enabled_types = Sord::Logging::AVAILABLE_TYPES - blacklist
55
+ end
56
+
24
57
  if options.regenerate
25
58
  begin
26
59
  Sord::Logging.info('Running YARD...')
27
- `yard`
60
+ Bundler.with_clean_env do
61
+ system('bundle exec yard')
62
+ end
28
63
  rescue Errno::ENOENT
29
64
  Sord::Logging.error('The YARD tool could not be found on your PATH.')
30
65
  Sord::Logging.error('You may need to run \'gem install yard\'.')
@@ -33,6 +68,6 @@ command :gen do |c|
33
68
  end
34
69
  end
35
70
 
36
- Sord::RbiGenerator.new(options).run(args.first)
71
+ Sord::RbiGenerator.new(options.__hash__).run(args.first)
37
72
  end
38
- end
73
+ end
@@ -28,6 +28,36 @@ module Sord
28
28
  @@silent = value
29
29
  end
30
30
 
31
+ # An array of all available logging types.
32
+ AVAILABLE_TYPES = [:warn, :info, :duck, :error, :infer, :omit, :done].freeze
33
+
34
+ @@enabled_types = AVAILABLE_TYPES
35
+
36
+ # Sets the array of log messages types which should be processed. Any not on
37
+ # this list will be discarded. This should be a subset of AVAILABLE_TYPES.
38
+ # @param [Array<Symbol>] value
39
+ # @return [void]
40
+ def self.enabled_types=(value)
41
+ raise 'invalid types' unless valid_types?(value)
42
+ @@enabled_types = value
43
+ end
44
+
45
+ # Gets the array of log messages types which should be processed. Any not on
46
+ # this list will be discarded.
47
+ # @return [Array<Symbol>]
48
+ # @return [void]
49
+ def self.enabled_types
50
+ @@enabled_types
51
+ end
52
+
53
+ # Returns a boolean indicating whether a given array is a valid value for
54
+ # #enabled_types.
55
+ # @param [Array<Symbol>] value
56
+ # @return [void]
57
+ def self.valid_types?(value)
58
+ (value - AVAILABLE_TYPES).empty?
59
+ end
60
+
31
61
  # A generic log message writer which is called by all other specific logging
32
62
  # methods. This shouldn't be called outside of the Logging class itself.
33
63
  # @param [Symbol] kind The kind of log message this is.
@@ -39,7 +69,10 @@ module Sord
39
69
  # is associated with, if any. This is shown before the log message if it is
40
70
  # specified.
41
71
  # @param [Integer] indent_level The level at which to indent the code.
72
+ # @return [void]
42
73
  def self.generic(kind, header, msg, item, indent_level = 0)
74
+ return unless enabled_types.include?(kind)
75
+
43
76
  if item
44
77
  puts "#{header} (#{item.path.bold}) #{msg}" unless silent?
45
78
  else
@@ -56,6 +89,7 @@ module Sord
56
89
  # is associated with, if any. This is shown before the log message if it is
57
90
  # specified.
58
91
  # @param [Integer] indent_level The level at which to indent the code.
92
+ # @return [void]
59
93
  def self.warn(msg, item = nil, indent_level = 0)
60
94
  generic(:warn, '[WARN ]'.yellow, msg, item, indent_level)
61
95
  end
@@ -67,6 +101,7 @@ module Sord
67
101
  # is associated with, if any. This is shown before the log message if it is
68
102
  # specified.
69
103
  # @param [Integer] indent_level The level at which to indent the code.
104
+ # @return [void]
70
105
  def self.info(msg, item = nil, indent_level = 0)
71
106
  generic(:info, '[INFO ]', msg, item, indent_level)
72
107
  end
@@ -79,6 +114,7 @@ module Sord
79
114
  # is associated with, if any. This is shown before the log message if it is
80
115
  # specified.
81
116
  # @param [Integer] indent_level The level at which to indent the code.
117
+ # @return [void]
82
118
  def self.duck(msg, item = nil, indent_level = 0)
83
119
  generic(:duck, '[DUCK ]'.cyan, msg, item, indent_level)
84
120
  end
@@ -90,6 +126,7 @@ module Sord
90
126
  # is associated with, if any. This is shown before the log message if it is
91
127
  # specified.
92
128
  # @param [Integer] indent_level The level at which to indent the code.
129
+ # @return [void]
93
130
  def self.error(msg, item = nil, indent_level = 0)
94
131
  generic(:error, '[ERROR]'.red, msg, item, indent_level)
95
132
  end
@@ -102,6 +139,7 @@ module Sord
102
139
  # is associated with, if any. This is shown before the log message if it is
103
140
  # specified.
104
141
  # @param [Integer] indent_level The level at which to indent the code.
142
+ # @return [void]
105
143
  def self.infer(msg, item = nil, indent_level = 0)
106
144
  generic(:infer, '[INFER]'.light_blue, msg, item, indent_level)
107
145
  end
@@ -114,16 +152,19 @@ module Sord
114
152
  # is associated with, if any. This is shown before the log message if it is
115
153
  # specified.
116
154
  # @param [Integer] indent_level The level at which to indent the code.
155
+ # @return [void]
117
156
  def self.omit(msg, item = nil, indent_level = 0)
118
157
  generic(:omit, '[OMIT ]'.magenta, msg, item, indent_level)
119
158
  end
120
159
 
121
160
  # Print a done message. This should be used when a process completes
122
161
  # successfully.
162
+ # @param [String] msg The log message to write.
123
163
  # @param [YARD::CodeObjects::Base] item The CodeObject which this log
124
164
  # is associated with, if any. This is shown before the log message if it is
125
165
  # specified.
126
166
  # @param [Integer] indent_level The level at which to indent the code.
167
+ # @return [void]
127
168
  def self.done(msg, item = nil, indent_level = 0)
128
169
  generic(:done, '[DONE ]'.green, msg, item)
129
170
  end
@@ -135,6 +176,7 @@ module Sord
135
176
  # is associated with, if any. This is shown before the log message if it is
136
177
  # specified.
137
178
  # @param [Integer] indent_level The level at which to indent the code.
179
+ # @return [void]
138
180
  def self.invoke_hooks(kind, msg, item, indent_level = 0)
139
181
  @@hooks.each do |hook|
140
182
  hook.(kind, msg, item, indent_level) rescue nil
@@ -149,6 +191,7 @@ module Sord
149
191
  # specified.
150
192
  # @yieldparam [Integer] indent_level The level at which to indent the code.
151
193
  # @yieldreturn [void]
194
+ # @return [void]
152
195
  def self.add_hook(&blk)
153
196
  @@hooks << blk
154
197
  end
@@ -21,19 +21,30 @@ module Sord
21
21
  # [message, item, line].
22
22
  attr_reader :warnings
23
23
 
24
+ # @return [Boolean] A boolean indicating whether the next item is the first
25
+ # in its namespace. This is used to determine whether to insert a blank
26
+ # line before it or not.
27
+ attr_accessor :next_item_is_first_in_namespace
28
+
24
29
  # Create a new RBI generator.
25
30
  # @param [Hash] options
26
- # @return [RbiGenerator]
31
+ # @option options [Integer] break_params
32
+ # @option options [Boolean] replace_errors_with_untyped
33
+ # @option options [Boolean] comments
34
+ # @return [void]
27
35
  def initialize(options)
28
36
  @rbi_contents = ['# typed: strong']
29
37
  @namespace_count = 0
30
38
  @method_count = 0
39
+ @break_params = options[:break_params]
40
+ @replace_errors_with_untyped = options[:replace_errors_with_untyped]
31
41
  @warnings = []
42
+ @next_item_is_first_in_namespace = true
32
43
 
33
44
  # Hook the logger so that messages are added as comments to the RBI file
34
45
  Logging.add_hook do |type, msg, item, indent_level = 0|
35
46
  rbi_contents << "#{' ' * (indent_level + 1)}# sord #{type} - #{msg}"
36
- end if options.comments
47
+ end if options[:comments]
37
48
 
38
49
  # Hook the logger so that warnings are collected
39
50
  Logging.add_hook do |type, msg, item, indent_level = 0|
@@ -54,21 +65,57 @@ module Sord
54
65
  @method_count += 1
55
66
  end
56
67
 
68
+ # Adds a single blank line to the RBI file, unless this item is the first
69
+ # in its namespace.
70
+ # @return [void]
71
+ def add_blank
72
+ rbi_contents << '' unless next_item_is_first_in_namespace
73
+ self.next_item_is_first_in_namespace = false
74
+ end
75
+
57
76
  # Given a YARD CodeObject, add lines defining its mixins (that is, extends
58
- # and includes) to the current RBI file.
77
+ # and includes) to the current RBI file. Returns the number of mixins.
59
78
  # @param [YARD::CodeObjects::Base] item
60
79
  # @param [Integer] indent_level
61
- # @return [void]
80
+ # @return [Integer]
62
81
  def add_mixins(item, indent_level)
63
- extends = item.instance_mixins
64
- includes = item.class_mixins
82
+ includes = item.instance_mixins
83
+ extends = item.class_mixins
65
84
 
66
- extends.each do |this_extend|
85
+ extends.reverse_each do |this_extend|
67
86
  rbi_contents << "#{' ' * (indent_level + 1)}extend #{this_extend.path}"
68
87
  end
69
- includes.each do |this_include|
88
+ includes.reverse_each do |this_include|
70
89
  rbi_contents << "#{' ' * (indent_level + 1)}include #{this_include.path}"
71
90
  end
91
+
92
+ extends.length + includes.length
93
+ end
94
+
95
+ # Given an array of parameters and a return type, inserts the signature for
96
+ # a method with those properties into the current RBI file.
97
+ # @param [Array<String>] params
98
+ # @param [String] returns
99
+ # @param [Integer] indent_level
100
+ # @return [void]
101
+ def add_signature(params, returns, indent_level)
102
+ if params.empty?
103
+ rbi_contents << "#{' ' * (indent_level + 1)}sig { #{returns} }"
104
+ return
105
+ end
106
+
107
+ if params.length >= @break_params
108
+ rbi_contents << "#{' ' * (indent_level + 1)}sig do"
109
+ rbi_contents << "#{' ' * (indent_level + 2)}params("
110
+ params.each.with_index do |param, i|
111
+ terminator = params.length - 1 == i ? '' : ','
112
+ rbi_contents << "#{' ' * (indent_level + 3)}#{param}#{terminator}"
113
+ end
114
+ rbi_contents << "#{' ' * (indent_level + 2)}).#{returns}"
115
+ rbi_contents << "#{' ' * (indent_level + 1)}end"
116
+ else
117
+ rbi_contents << "#{' ' * (indent_level + 1)}sig { params(#{params.join(', ')}).#{returns} }"
118
+ end
72
119
  end
73
120
 
74
121
  # Given a YARD NamespaceObject, add lines defining its methods and their
@@ -77,8 +124,6 @@ module Sord
77
124
  # @param [Integer] indent_level
78
125
  # @return [void]
79
126
  def add_methods(item, indent_level)
80
- # TODO: block documentation
81
-
82
127
  item.meths.each do |meth|
83
128
  count_method
84
129
 
@@ -88,6 +133,8 @@ module Sord
88
133
  next
89
134
  end
90
135
 
136
+ add_blank
137
+
91
138
  parameter_list = meth.parameters.map do |name, default|
92
139
  # Handle these three main cases:
93
140
  # - def method(param) or def method(param:)
@@ -107,55 +154,94 @@ module Sord
107
154
  # (The gsubs allow for better splat-argument compatibility)
108
155
  parameter_names_to_tags = meth.parameters.map do |name, _|
109
156
  [name, meth.tags('param')
110
- .find { |p| p.name.gsub('*', '') == name.gsub('*', '') }]
157
+ .find { |p| p.name&.gsub('*', '') == name.gsub('*', '') }]
111
158
  end.to_h
112
159
 
113
160
  sig_params_list = parameter_names_to_tags.map do |name, tag|
114
161
  name = name.gsub('*', '')
115
162
 
116
163
  if tag
117
- "#{name}: #{TypeConverter.yard_to_sorbet(tag.types, meth)}"
164
+ "#{name}: #{TypeConverter.yard_to_sorbet(tag.types, meth, indent_level, @replace_errors_with_untyped)}"
118
165
  elsif name.start_with? '&'
119
- # Cut the ampersand from the block parameter name.
120
- "#{name[1..-1]}: T.untyped"
166
+ # Cut the ampersand from the block parameter name
167
+ name = name.gsub('&', '')
168
+
169
+ # Find yieldparams and yieldreturn
170
+ yieldparams = meth.tags('yieldparam')
171
+ yieldreturn = meth.tag('yieldreturn')&.types
172
+ yieldreturn = nil if yieldreturn&.length == 1 &&
173
+ yieldreturn&.first&.downcase == 'void'
174
+
175
+ # Create strings
176
+ params_string = yieldparams.map do |param|
177
+ "#{param.name.gsub('*', '')}: #{TypeConverter.yard_to_sorbet(param.types, meth, indent_level, @replace_errors_with_untyped)}" unless param.name.nil?
178
+ end.join(', ')
179
+ return_string = TypeConverter.yard_to_sorbet(yieldreturn, meth, indent_level, @replace_errors_with_untyped)
180
+
181
+ # Create proc types, if possible
182
+ if yieldparams.empty? && yieldreturn.nil?
183
+ "#{name}: T.untyped"
184
+ elsif yieldreturn.nil?
185
+ "#{name}: T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.void"
186
+ else
187
+ "#{name}: T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.returns(#{return_string})"
188
+ end
121
189
  elsif meth.path.end_with? '='
122
190
  # Look for the matching getter method
123
191
  getter_path = meth.path[0...-1]
124
192
  getter = item.meths.find { |m| m.path == getter_path }
125
193
 
126
194
  unless getter
127
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level)
128
- next "#{name}: T.untyped"
195
+ if parameter_names_to_tags.length == 1 \
196
+ && meth.tags('param').length == 1 \
197
+ && meth.tag('param').types
198
+
199
+ Logging.infer("argument name in single @param inferred as #{parameter_names_to_tags.first.first.inspect}", meth, indent_level)
200
+ next "#{name}: #{TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, indent_level, @replace_errors_with_untyped)}"
201
+ else
202
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level)
203
+ next "#{name}: T.untyped"
204
+ end
129
205
  end
130
206
 
131
207
  inferred_type = TypeConverter.yard_to_sorbet(
132
- getter.tags('return').flat_map(&:types), meth)
208
+ getter.tags('return').flat_map(&:types), meth, indent_level, @replace_errors_with_untyped)
133
209
 
134
210
  Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth, indent_level)
135
211
  # Get rid of : on keyword arguments.
136
212
  name = name.chop if name.end_with?(':')
137
213
  "#{name}: #{inferred_type}"
138
214
  else
139
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level)
140
- # Get rid of : on keyword arguments.
141
- name = name.chop if name.end_with?(':')
142
- "#{name}: T.untyped"
215
+ # Is this the only argument, and was a @param specified without an
216
+ # argument name? If so, infer it
217
+ if parameter_names_to_tags.length == 1 \
218
+ && meth.tags('param').length == 1 \
219
+ && meth.tag('param').types
220
+
221
+ Logging.infer("argument name in single @param inferred as #{parameter_names_to_tags.first.first.inspect}", meth, indent_level)
222
+ "#{name}: #{TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, indent_level, @replace_errors_with_untyped)}"
223
+ else
224
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level)
225
+ # Get rid of : on keyword arguments.
226
+ name = name.chop if name.end_with?(':')
227
+ "#{name}: T.untyped"
228
+ end
143
229
  end
144
- end.join(", ")
230
+ end
145
231
 
146
232
  return_tags = meth.tags('return')
147
233
  returns = if return_tags.length == 0
148
- "void"
234
+ Logging.omit("no YARD return type given, using T.untyped", meth, indent_level)
235
+ "returns(T.untyped)"
149
236
  elsif return_tags.length == 1 && return_tags&.first&.types&.first&.downcase == "void"
150
237
  "void"
151
238
  else
152
- "returns(#{TypeConverter.yard_to_sorbet(meth.tag('return').types, meth)})"
239
+ "returns(#{TypeConverter.yard_to_sorbet(meth.tag('return').types, meth, indent_level, @replace_errors_with_untyped)})"
153
240
  end
154
241
 
155
242
  prefix = meth.scope == :class ? 'self.' : ''
156
243
 
157
- sig = sig_params_list.empty? ? "#{' ' * (indent_level + 1)}sig { #{returns} }" : "#{' ' * (indent_level + 1)}sig { params(#{sig_params_list}).#{returns} }"
158
- rbi_contents << sig
244
+ add_signature(sig_params_list, returns, indent_level)
159
245
 
160
246
  rbi_contents << "#{' ' * (indent_level + 1)}def #{prefix}#{meth.name}(#{parameter_list}); end"
161
247
  end
@@ -165,41 +251,55 @@ module Sord
165
251
  # and children to the RBI file.
166
252
  # @param [YARD::CodeObjects::NamespaceObject] item
167
253
  # @param [Integer] indent_level
254
+ # @return [void]
168
255
  def add_namespace(item, indent_level = 0)
169
256
  count_namespace
257
+ add_blank
170
258
 
171
259
  if item.type == :class && item.superclass.to_s != "Object"
172
260
  rbi_contents << "#{' ' * indent_level}class #{item.name} < #{item.superclass.path}"
173
261
  else
174
262
  rbi_contents << "#{' ' * indent_level}#{item.type} #{item.name}"
175
263
  end
176
- add_mixins(item, indent_level)
264
+
265
+ self.next_item_is_first_in_namespace = true
266
+ if add_mixins(item, indent_level) > 0
267
+ self.next_item_is_first_in_namespace = false
268
+ end
177
269
  add_methods(item, indent_level)
178
270
 
179
271
  item.children.select { |x| [:class, :module].include?(x.type) }
180
272
  .each { |child| add_namespace(child, indent_level + 1) }
181
273
 
274
+ self.next_item_is_first_in_namespace = false
275
+
182
276
  rbi_contents << "#{' ' * indent_level}end"
183
277
  end
184
278
 
185
- # Generates the RBI file and writes it to the given file path.
186
- # @param [String] filename
279
+ # Generates the RBI file from the loading registry and returns its contents.
280
+ # You must load a registry first!
281
+ # @return [String]
282
+ def generate
283
+ # Generate top-level modules, which recurses to all modules
284
+ YARD::Registry.root.children
285
+ .select { |x| [:class, :module].include?(x.type) }
286
+ .each { |child| add_namespace(child) }
287
+
288
+ rbi_contents.join("\n")
289
+ end
290
+
291
+ # Generates the RBI file and writes it to the given file path, printing a
292
+ # summary and any warnings at the end. The registry is also loaded.
293
+ # @param [String, nil] filename
187
294
  # @return [void]
188
295
  def run(filename)
189
- raise "No filename specified" unless filename
296
+ raise 'No filename specified' unless filename
190
297
 
191
298
  # Get YARD ready
192
299
  YARD::Registry.load!
193
300
 
194
- # TODO: constants?
195
-
196
- # Generate top-level modules, which recurses to all modules
197
- YARD::Registry.root.children
198
- .select { |x| [:class, :module].include?(x.type) }
199
- .each { |child| add_namespace(child) }
200
-
201
301
  # Write the file
202
- File.write(filename, rbi_contents.join(?\n))
302
+ File.write(filename, generate)
203
303
 
204
304
  if object_count.zero?
205
305
  Logging.warn("No objects processed.")
@@ -213,11 +313,15 @@ module Sord
213
313
 
214
314
  unless warnings.empty?
215
315
  Logging.warn("There were #{warnings.length} important warnings in the RBI file, listed below.")
216
- Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
316
+ if @replace_errors_with_untyped
317
+ Logging.warn("The types which caused them have been replaced with T.untyped.")
318
+ else
319
+ Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
320
+ end
217
321
  Logging.warn("Please edit the file near the line numbers given to fix these errors.")
218
322
  Logging.warn("Alternatively, edit your YARD documentation so that your types are valid and re-run Sord.")
219
323
  warnings.each do |(msg, item, line)|
220
- puts " #{"Line #{line} |".light_black} (#{item.path.bold}) #{msg}"
324
+ puts " #{"Line #{line} |".light_black} (#{item&.path&.bold}) #{msg}"
221
325
  end
222
326
  end
223
327
  rescue