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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9a2d105021ccde04330fef38f0a2d3fb270fbd3d2e2c43a258c3bc3f6e0b9f1
4
- data.tar.gz: 7d4ff5718684cfd2505595c77ef0bbc6e7f79c6981b0210f28912a66cb6725eb
3
+ metadata.gz: f23de5349a9a01e6a4f4f4282b30197bb2bfa41714350da2223019ad7760f375
4
+ data.tar.gz: d99869924be64e52ac543c8a376c906aef074932c912a403992d79728cf638de
5
5
  SHA512:
6
- metadata.gz: 8681c3b8c07c397f2f454f0e71546ba33ead6b66efdee7c98ecb57ccfea4908a9fe7f03c43d206d46409d4cf7bcdf3116dbb89b628feaf23bfdb52d9f2168c6a
7
- data.tar.gz: 552beb2ce3b13800f16687967a3d6bc80b6ab7398f54c16e998612241a37b7bb4a6f490e25d17fa427960ccaecd3a4c0964ac224728dc3e13eb8abc0bb6de970
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 single-argument generic types such as `Array` (no hashes yet)
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
- def self.invoke_hooks(type, msg, item)
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.(type, msg, item) rescue nil
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
@@ -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
- attr_reader :rbi_contents, :object_count
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: true']
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!
@@ -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
- # TODO: does not support mulitple type arguments (e.g. Hash<A, B>)
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})<(#{SIMPLE_TYPE_REGEX})>/
16
+ /(#{SIMPLE_TYPE_REGEX})\s*<\s*(.*)\s*>/
11
17
 
12
- # TODO: Hash
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
- def self.yard_to_sorbet(yard, item=nil, &blk)
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, &blk)
26
- : "T.any(#{yard.map { |x| yard_to_sorbet(x, item, &blk) }.join(', ')})"
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
- type_parameter = $2
79
+ type_parameters = $2
35
80
 
36
81
  if SORBET_SUPPORTED_GENERIC_TYPES.include?(generic_type)
37
- if /^[_a-z]/ === type_parameter
38
- Logging.warn("#{type_parameter} is probably not a type, but using anyway", item)
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
@@ -1,4 +1,4 @@
1
1
  # typed: strong
2
2
  module Sord
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
data/sord.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "rake", "~> 10.0"
31
31
  spec.add_development_dependency "rspec", "~> 3.0"
32
32
  spec.add_development_dependency 'sorbet'
33
+ spec.add_development_dependency 'simplecov'
33
34
  end
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.1.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