sord 0.1.0 → 0.2.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/.rspec +1 -0
- data/README.md +77 -1
- data/lib/sord/logging.rb +74 -4
- data/lib/sord/rbi_generator.rb +28 -4
- data/lib/sord/type_converter.rb +55 -13
- data/lib/sord/version.rb +1 -1
- data/sord.gemspec +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f23de5349a9a01e6a4f4f4282b30197bb2bfa41714350da2223019ad7760f375
|
4
|
+
data.tar.gz: d99869924be64e52ac543c8a376c906aef074932c912a403992d79728cf638de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9db224cd69010f54a30282612e5e0e37eada990376c8016abacd36d9ce4c7a6e6d8154b50d1aabc226700acde74dc342b152d8085d15943700954df24e0bc024
|
7
|
+
data.tar.gz: 0a3ea8a0215c79a975e792291446422f65646ec8153340e045639ea0d166a2fd91febf806e9315168166915c800e0e832477c49d78021728ead6c6bc44e2b734
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/README.md
CHANGED
@@ -15,10 +15,12 @@ Sord has the following features:
|
|
15
15
|
- Gracefully handles missing YARD types (`T.untyped`)
|
16
16
|
- Can infer setter parameter type from the corresponding getter's return type
|
17
17
|
- Recognises mixins (`include` and `extend`)
|
18
|
-
- Support for
|
18
|
+
- Support for generic types such as `Array<T>` and `Hash<K, V>`
|
19
19
|
|
20
20
|
## Usage
|
21
21
|
|
22
|
+
Install Sord with `gem install sord`.
|
23
|
+
|
22
24
|
Sord is a command line tool. To use it, open a terminal in the root directory
|
23
25
|
of your project, and run `yard` to generate a YARD registry if you haven't
|
24
26
|
already. Then, invoke `sord`, passing a path of where you'd like to save your
|
@@ -34,6 +36,80 @@ RBI file will be replaced if you re-run Sord.
|
|
34
36
|
|
35
37
|
## Example
|
36
38
|
|
39
|
+
Say we have this file, called `test.rb`:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
module Example
|
43
|
+
class Person
|
44
|
+
# @param [String] name
|
45
|
+
# @param [Integer] age
|
46
|
+
# @return [Example::Person]
|
47
|
+
def initialize(name, age)
|
48
|
+
@name = name
|
49
|
+
@age = age
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [String] name
|
53
|
+
attr_accessor :name
|
54
|
+
|
55
|
+
# @return [Integer] age
|
56
|
+
attr_accessor :age
|
57
|
+
|
58
|
+
# @param [Array<String>] possible_names
|
59
|
+
# @param [Array<Integer>] possible_ages
|
60
|
+
# @return [Example::Person]
|
61
|
+
def self.construct_randomly(possible_names, possible_ages)
|
62
|
+
Person.new(possible_names.sample, possible_ages.sample)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
First, generate a YARD registry by running `yardoc test.rb`. Then, we can run
|
69
|
+
`sord test.rbi` to generate the RBI file. (Careful not to overwrite your code
|
70
|
+
files! Note the `.rbi` file extension.) In doing this, Sord prints:
|
71
|
+
|
72
|
+
```
|
73
|
+
[INFER] (Example::Person#name=) inferred type of parameter "value" as String using getter's return type
|
74
|
+
[INFER] (Example::Person#age=) inferred type of parameter "value" as Integer using getter's return type
|
75
|
+
[DONE ] Processed 8 objects
|
76
|
+
```
|
77
|
+
|
78
|
+
The `test.rbi` file then contains a complete RBI file for `test.rb`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# typed: true
|
82
|
+
module Example
|
83
|
+
end
|
84
|
+
class Example::Person
|
85
|
+
sig { params(name: String, age: Integer).returns(Example::Person) }
|
86
|
+
def initialize(name, age) end
|
87
|
+
sig { params().returns(String) }
|
88
|
+
def name() end
|
89
|
+
# sord infer - inferred type of parameter "value" as String using getter's return type
|
90
|
+
sig { params(value: String).returns(String) }
|
91
|
+
def name=(value) end
|
92
|
+
sig { params().returns(Integer) }
|
93
|
+
def age() end
|
94
|
+
# sord infer - inferred type of parameter "value" as Integer using getter's return type
|
95
|
+
sig { params(value: Integer).returns(Integer) }
|
96
|
+
def age=(value) end
|
97
|
+
sig { params(possible_names: T::Array[String], possible_ages: T::Array[Integer]).returns(Example::Person) }
|
98
|
+
def self.construct_randomly(possible_names, possible_ages) end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
## Things to work on
|
103
|
+
|
104
|
+
- I'm not 100% sure how this handles undocumented methods and classes.
|
105
|
+
- More inference systems would be nice.
|
106
|
+
- This won't generate type parameter definitions for things which mix-in
|
107
|
+
`Enumerable`.
|
108
|
+
- Module scoping is an issue - if `Example::Person` is replaced with `Person`
|
109
|
+
in the YARD comments in the above example, Sorbet won't be able to resolve
|
110
|
+
it.
|
111
|
+
- Tests!!
|
112
|
+
|
37
113
|
## Contributing
|
38
114
|
|
39
115
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/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.
|
data/lib/sord/logging.rb
CHANGED
@@ -1,45 +1,115 @@
|
|
1
1
|
require 'colorize'
|
2
2
|
|
3
3
|
module Sord
|
4
|
+
# Handles writing logs to stdout and any other classes which request them.
|
4
5
|
module Logging
|
6
|
+
# This is an Array of callables which are all executed upon a log message.
|
7
|
+
# The callables should take three parameters: (kind, msg, item).
|
5
8
|
@@hooks = []
|
6
9
|
|
10
|
+
# @return [Boolean] Whether log messages should be printed or not. This is
|
11
|
+
# used for testing.
|
12
|
+
def self.silent?
|
13
|
+
@@silent || false
|
14
|
+
end
|
15
|
+
|
16
|
+
# Sets whether log messages should be printed or not.
|
17
|
+
# @param [Boolean] value
|
18
|
+
# @return [void]
|
19
|
+
def self.silent=(value)
|
20
|
+
@@silent = value
|
21
|
+
end
|
22
|
+
|
23
|
+
# A generic log message writer which is called by all other specific logging
|
24
|
+
# methods. This shouldn't be called outside of the Logging class itself.
|
25
|
+
# @param [Symbol] kind The kind of log message this is.
|
26
|
+
# @param [String] header The prefix for this log message. For consistency,
|
27
|
+
# it should be up to five uppercase characters wrapped in square brackets,
|
28
|
+
# with some unique colour applied.
|
29
|
+
# @param [String] msg The log message to write.
|
30
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
31
|
+
# is associated with, if any. This is shown before the log message if it is
|
32
|
+
# specified.
|
7
33
|
def self.generic(kind, header, msg, item)
|
8
34
|
if item
|
9
|
-
puts "#{header} (#{item.path.light_white}) #{msg}"
|
35
|
+
puts "#{header} (#{item.path.light_white}) #{msg}" unless silent?
|
10
36
|
else
|
11
|
-
puts "#{header} #{msg}"
|
37
|
+
puts "#{header} #{msg}" unless silent?
|
12
38
|
end
|
13
39
|
|
14
40
|
invoke_hooks(kind, msg, item)
|
15
41
|
end
|
16
42
|
|
43
|
+
# Print a warning message. This should be used for things which require the
|
44
|
+
# user's attention but do not prevent the process from stopping.
|
45
|
+
# @param [String] msg The log message to write.
|
46
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
47
|
+
# is associated with, if any. This is shown before the log message if it is
|
48
|
+
# specified.
|
17
49
|
def self.warn(msg, item=nil)
|
18
50
|
generic(:warn, '[WARN ]'.yellow, msg, item)
|
19
51
|
end
|
20
52
|
|
53
|
+
# Print an error message. This should be used for things which require the
|
54
|
+
# current process to stop.
|
55
|
+
# @param [String] msg The log message to write.
|
56
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
57
|
+
# is associated with, if any. This is shown before the log message if it is
|
58
|
+
# specified.
|
21
59
|
def self.error(msg, item=nil)
|
22
60
|
generic(:error, '[ERROR]'.red, msg, item)
|
23
61
|
end
|
24
62
|
|
63
|
+
# Print an infer message. This should be used when the user should be told
|
64
|
+
# that some information has been filled in or guessed for them, and that
|
65
|
+
# information is likely correct.
|
66
|
+
# @param [String] msg The log message to write.
|
67
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
68
|
+
# is associated with, if any. This is shown before the log message if it is
|
69
|
+
# specified.
|
25
70
|
def self.infer(msg, item=nil)
|
26
71
|
generic(:infer, '[INFER]'.light_blue, msg, item)
|
27
72
|
end
|
28
73
|
|
74
|
+
# Print an omit message. This should be used as a special type of warning
|
75
|
+
# to alert the user that there is some information missing, but this
|
76
|
+
# information is not critical to the completion of the process.
|
77
|
+
# @param [String] msg The log message to write.
|
78
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
79
|
+
# is associated with, if any. This is shown before the log message if it is
|
80
|
+
# specified.
|
29
81
|
def self.omit(msg, item=nil)
|
30
82
|
generic(:omit, '[OMIT ]'.magenta, msg, item)
|
31
83
|
end
|
32
84
|
|
85
|
+
# Print a done message. This should be used when a process completes
|
86
|
+
# successfully.
|
87
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
88
|
+
# is associated with, if any. This is shown before the log message if it is
|
89
|
+
# specified.
|
33
90
|
def self.done(msg, item=nil)
|
34
91
|
generic(:done, '[DONE ]'.green, msg, item)
|
35
92
|
end
|
36
93
|
|
37
|
-
|
94
|
+
# Invokes all registered hooks on the logger.
|
95
|
+
# @param [Symbol] kind The kind of log message this is.
|
96
|
+
# @param [String] msg The log message to write.
|
97
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which this log
|
98
|
+
# is associated with, if any. This is shown before the log message if it is
|
99
|
+
# specified.
|
100
|
+
def self.invoke_hooks(kind, msg, item)
|
38
101
|
@@hooks.each do |hook|
|
39
|
-
hook.(
|
102
|
+
hook.(kind, msg, item) rescue nil
|
40
103
|
end
|
41
104
|
end
|
42
105
|
|
106
|
+
# Adds a hook to the logger.
|
107
|
+
# @yieldparam [Symbol] kind The kind of log message this is.
|
108
|
+
# @yieldparam [String] msg The log message to write.
|
109
|
+
# @yieldparam [YARD::CodeObjects::Base] item The CodeObject which this log
|
110
|
+
# is associated with, if any. This is shown before the log message if it is
|
111
|
+
# specified.
|
112
|
+
# @yieldreturn [void]
|
43
113
|
def self.add_hook(&blk)
|
44
114
|
@@hooks << blk
|
45
115
|
end
|
data/lib/sord/rbi_generator.rb
CHANGED
@@ -5,22 +5,37 @@ require 'colorize'
|
|
5
5
|
require 'sord/logging'
|
6
6
|
|
7
7
|
module Sord
|
8
|
+
# Converts the current working directory's YARD registry into an RBI file.
|
8
9
|
class RbiGenerator
|
9
|
-
|
10
|
-
|
10
|
+
# @return [Array<String>] The lines of the generated RBI file so far.
|
11
|
+
attr_reader :rbi_contents
|
12
|
+
|
13
|
+
# @return [Integer] The number of objects this generator has processed so
|
14
|
+
# far.
|
15
|
+
attr_reader :object_count
|
16
|
+
|
17
|
+
# Create a new RBI generator.
|
18
|
+
# @return [RbiGenerator]
|
11
19
|
def initialize
|
12
|
-
@rbi_contents = ['# typed:
|
20
|
+
@rbi_contents = ['# typed: strong']
|
13
21
|
@object_count = 0
|
14
22
|
|
23
|
+
# Hook the logger so that messages are added as comments to the RBI file
|
15
24
|
Logging.add_hook do |type, msg, item|
|
16
25
|
rbi_contents << " # sord #{type} - #{msg}"
|
17
26
|
end
|
18
27
|
end
|
19
28
|
|
29
|
+
# Increment the object counter.
|
30
|
+
# @return [void]
|
20
31
|
def count_object
|
21
32
|
@object_count += 1
|
22
33
|
end
|
23
34
|
|
35
|
+
# Given a YARD CodeObject, add lines defining its mixins (that is, extends
|
36
|
+
# and includes) to the current RBI file.
|
37
|
+
# @param [YARD::CodeObjects::Base] item
|
38
|
+
# @return [void]
|
24
39
|
def add_mixins(item)
|
25
40
|
extends = item.instance_mixins
|
26
41
|
includes = item.class_mixins
|
@@ -33,8 +48,13 @@ module Sord
|
|
33
48
|
end
|
34
49
|
end
|
35
50
|
|
51
|
+
# Given a YARD NamespaceObject, add lines defining its methods and their
|
52
|
+
# signatures to the current RBI file.
|
53
|
+
# @param [YARD::CodeObjects::NamespaceObject] item
|
54
|
+
# @return [void]
|
36
55
|
def add_methods(item)
|
37
56
|
# TODO: block documentation
|
57
|
+
|
38
58
|
item.meths.each do |meth|
|
39
59
|
count_object
|
40
60
|
|
@@ -42,11 +62,12 @@ module Sord
|
|
42
62
|
"#{name}#{default && " = #{default}"}"
|
43
63
|
end.join(", ")
|
44
64
|
|
65
|
+
# This is better than iterating over YARD's "@param" tags directly
|
66
|
+
# because it includes parameters without documentation
|
45
67
|
parameter_names_to_tags = meth.parameters.map do |name, _|
|
46
68
|
[name, meth.tags('param').find { |p| p.name == name }]
|
47
69
|
end.to_h
|
48
70
|
|
49
|
-
# TODO: if it's a _= method, infer from the _ method
|
50
71
|
sig_params_list = parameter_names_to_tags.map do |name, tag|
|
51
72
|
if tag
|
52
73
|
"#{name}: #{TypeConverter.yard_to_sorbet(tag.types, meth)}"
|
@@ -91,6 +112,9 @@ module Sord
|
|
91
112
|
end
|
92
113
|
end
|
93
114
|
|
115
|
+
# Generates the RBI file and writes it to the given file path.
|
116
|
+
# @param [String] filename
|
117
|
+
# @return [void]
|
94
118
|
def run(filename)
|
95
119
|
# Get YARD ready
|
96
120
|
YARD::Registry.load!
|
data/lib/sord/type_converter.rb
CHANGED
@@ -1,18 +1,62 @@
|
|
1
1
|
require 'sord/logging'
|
2
2
|
|
3
3
|
module Sord
|
4
|
+
# Contains methods to convert YARD types to Sorbet types.
|
4
5
|
module TypeConverter
|
6
|
+
# A regular expression which matches Ruby namespaces and identifiers.
|
7
|
+
# "Foo", "Foo::Bar", and "::Foo::Bar" are all matches, whereas "Foo.Bar"
|
8
|
+
# or "Foo#bar" are not.
|
5
9
|
SIMPLE_TYPE_REGEX =
|
6
10
|
/(?:\:\:)?[a-zA-Z_][a-zA-Z_0-9]*(?:\:\:[a-zA-Z_][a-zA-Z_0-9]*)*/
|
7
11
|
|
8
|
-
#
|
12
|
+
# A regular expression which matches a Ruby namespace immediately followed
|
13
|
+
# by another Ruby namespace in angle brackets. This is the format usually
|
14
|
+
# used in YARD to model generic types, such as "Array<String>".
|
9
15
|
GENERIC_TYPE_REGEX =
|
10
|
-
/(#{SIMPLE_TYPE_REGEX})
|
16
|
+
/(#{SIMPLE_TYPE_REGEX})\s*<\s*(.*)\s*>/
|
11
17
|
|
12
|
-
#
|
13
|
-
SORBET_SUPPORTED_GENERIC_TYPES = %w{Array Set Enumerable Enumerator Range}
|
18
|
+
# An array of built-in generic types supported by Sorbet.
|
19
|
+
SORBET_SUPPORTED_GENERIC_TYPES = %w{Array Set Enumerable Enumerator Range Hash}
|
14
20
|
|
15
|
-
|
21
|
+
# Given a string of YARD type parameters (without angle brackets), splits
|
22
|
+
# the string into an array of each type parameter.
|
23
|
+
# @param [String] params The type parameters.
|
24
|
+
# @return [Array<String>] The split type parameters.
|
25
|
+
def self.split_type_parameters(params)
|
26
|
+
result = []
|
27
|
+
buffer = ""
|
28
|
+
current_bracketing_level = 0
|
29
|
+
character_pointer = 0
|
30
|
+
|
31
|
+
while character_pointer < params.length
|
32
|
+
should_buffer = true
|
33
|
+
|
34
|
+
current_bracketing_level += 1 if params[character_pointer] == ?<
|
35
|
+
current_bracketing_level -= 1 if params[character_pointer] == ?>
|
36
|
+
|
37
|
+
if params[character_pointer] == ?,
|
38
|
+
if current_bracketing_level == 0
|
39
|
+
result << buffer.strip
|
40
|
+
buffer = ""
|
41
|
+
should_buffer = false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
buffer += params[character_pointer] if should_buffer
|
46
|
+
character_pointer += 1
|
47
|
+
end
|
48
|
+
|
49
|
+
result << buffer.strip
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# Converts a YARD type into a Sorbet type.
|
55
|
+
# @param [Boolean, Array, String] yard The YARD type.
|
56
|
+
# @param [YARD::CodeObjects::Base] item The CodeObject which the YARD type
|
57
|
+
# is associated with. This is used for logging and can be nil, but this
|
58
|
+
# will lead to less informative log messages.
|
59
|
+
def self.yard_to_sorbet(yard, item=nil)
|
16
60
|
case yard
|
17
61
|
when nil
|
18
62
|
"T.untyped"
|
@@ -22,23 +66,21 @@ module Sord
|
|
22
66
|
# If there's only one element, unwrap it, otherwise allow for a
|
23
67
|
# selection of any of the types
|
24
68
|
yard.length == 1 \
|
25
|
-
? yard_to_sorbet(yard.first, item
|
26
|
-
: "T.any(#{yard.map { |x| yard_to_sorbet(x, item
|
69
|
+
? yard_to_sorbet(yard.first, item)
|
70
|
+
: "T.any(#{yard.map { |x| yard_to_sorbet(x, item) }.join(', ')})"
|
27
71
|
when /^#{SIMPLE_TYPE_REGEX}$/
|
72
|
+
# If this doesn't begin with an uppercase letter, warn
|
28
73
|
if /^[_a-z]/ === yard
|
29
74
|
Logging.warn("#{yard} is probably not a type, but using anyway", item)
|
30
75
|
end
|
31
76
|
yard
|
32
77
|
when /^#{GENERIC_TYPE_REGEX}$/
|
33
78
|
generic_type = $1
|
34
|
-
|
79
|
+
type_parameters = $2
|
35
80
|
|
36
81
|
if SORBET_SUPPORTED_GENERIC_TYPES.include?(generic_type)
|
37
|
-
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
"T::#{generic_type}[#{yard_to_sorbet(type_parameter, item, &blk)}]"
|
82
|
+
"T::#{generic_type}[#{
|
83
|
+
split_type_parameters(type_parameters).map { |x| yard_to_sorbet(x, item) }.join(', ')}]"
|
42
84
|
else
|
43
85
|
Logging.warn("unsupported generic type #{generic_type.inspect} in #{yard.inspect}", item)
|
44
86
|
"SORD_ERROR_#{generic_type.gsub(/[^0-9A-Za-z_]/i, '')}"
|
data/lib/sord/version.rb
CHANGED
data/sord.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Christiansen
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
description:
|
112
126
|
email:
|
113
127
|
- aaronc20000@gmail.com
|
@@ -117,6 +131,7 @@ extensions: []
|
|
117
131
|
extra_rdoc_files: []
|
118
132
|
files:
|
119
133
|
- ".gitignore"
|
134
|
+
- ".rspec"
|
120
135
|
- CODE_OF_CONDUCT.md
|
121
136
|
- Gemfile
|
122
137
|
- Gemfile.lock
|