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