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 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