sord 0.7.1 → 0.8.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 +4 -4
- data/.gitignore +2 -0
- data/README.md +62 -25
- data/Rakefile +72 -0
- data/exe/sord +39 -4
- data/lib/sord/logging.rb +43 -0
- data/lib/sord/rbi_generator.rb +144 -40
- data/lib/sord/resolver.rb +82 -0
- data/lib/sord/type_converter.rb +55 -13
- data/lib/sord/version.rb +1 -1
- data/rbi/sord.rbi +165 -0
- data/sorbet/config +0 -2
- data/sorbet/rbi/gems/colorize.rbi +81 -0
- data/sorbet/rbi/gems/docile.rbi +31 -0
- data/sorbet/rbi/gems/rake.rbi +7 -0
- data/sorbet/rbi/gems/rspec-core.rbi +16 -0
- data/sorbet/rbi/gems/rspec-expectations.rbi +389 -0
- data/sorbet/rbi/gems/rspec-mocks.rbi +823 -0
- data/sorbet/rbi/gems/rspec-support.rbi +143 -1
- data/sorbet/rbi/gems/rspec.rbi +1 -1
- data/sorbet/rbi/gems/simplecov-html.rbi +30 -0
- data/sorbet/rbi/gems/simplecov.rbi +223 -0
- data/sorbet/rbi/gems/sorbet-runtime.rbi +647 -0
- data/sorbet/rbi/gems/yard.rbi +13 -13
- data/sorbet/rbi/hidden-definitions/errors.txt +1579 -2264
- data/sorbet/rbi/hidden-definitions/hidden.rbi +2918 -845
- data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +15 -5
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +1 -1
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +1 -1
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4302b93ba07124b24e91c6db6a2757d2fe148c573f3b86a918c72155e83d0d5
|
4
|
+
data.tar.gz: 610ce8222970fb4da8f5d01368a21b1aef4067936f6883b2eb5522e567e8fcce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a3798003e636dc6baba752bbae52718eefb3f43a5562d7fb3659e39d0a0245c39dd10d297433f05df12f60ac102b93a6ad1b71bc0e1fcb2bf3405233dfb834c
|
7
|
+
data.tar.gz: 0b9a87eea2db2c948006c89c0970175c98471f0e98358a190c17e6c8e064ff59c5849ea73b0081ef6cd09975488884e333b359a44f8d79a8dac78d6f7f461c3e
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
## Overview
|
4
4
|
|
5
|
-
Sord is a **So**rbet and YA**RD**
|
6
|
-
Sorbet type signatures files by
|
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
|
36
|
-
fix any issues in the YARD documentation, as any edits
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
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
|
-
|
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
|
data/lib/sord/logging.rb
CHANGED
@@ -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
|
data/lib/sord/rbi_generator.rb
CHANGED
@@ -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
|
-
# @
|
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
|
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 [
|
80
|
+
# @return [Integer]
|
62
81
|
def add_mixins(item, indent_level)
|
63
|
-
|
64
|
-
|
82
|
+
includes = item.instance_mixins
|
83
|
+
extends = item.class_mixins
|
65
84
|
|
66
|
-
extends.
|
85
|
+
extends.reverse_each do |this_extend|
|
67
86
|
rbi_contents << "#{' ' * (indent_level + 1)}extend #{this_extend.path}"
|
68
87
|
end
|
69
|
-
includes.
|
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
|
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
|
-
|
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
|
-
|
128
|
-
|
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
|
-
|
140
|
-
#
|
141
|
-
|
142
|
-
|
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
|
230
|
+
end
|
145
231
|
|
146
232
|
return_tags = meth.tags('return')
|
147
233
|
returns = if return_tags.length == 0
|
148
|
-
"
|
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
|
-
|
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
|
-
|
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
|
186
|
-
#
|
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
|
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,
|
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
|
-
|
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
|
324
|
+
puts " #{"Line #{line} |".light_black} (#{item&.path&.bold}) #{msg}"
|
221
325
|
end
|
222
326
|
end
|
223
327
|
rescue
|