sord 0.8.0 → 0.9.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.
@@ -1,15 +1,13 @@
1
1
  # typed: true
2
2
  require 'yard'
3
3
  require 'sord/type_converter'
4
- require 'colorize'
5
4
  require 'sord/logging'
5
+ require 'parlour'
6
+ require 'rainbow'
6
7
 
7
8
  module Sord
8
9
  # Converts the current working directory's YARD registry into an RBI file.
9
- class RbiGenerator
10
- # @return [Array<String>] The lines of the generated RBI file so far.
11
- attr_reader :rbi_contents
12
-
10
+ class RbiGenerator
13
11
  # @return [Integer] The number of objects this generator has processed so
14
12
  # far.
15
13
  def object_count
@@ -21,35 +19,35 @@ module Sord
21
19
  # [message, item, line].
22
20
  attr_reader :warnings
23
21
 
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
-
29
22
  # Create a new RBI generator.
30
23
  # @param [Hash] options
31
24
  # @option options [Integer] break_params
32
25
  # @option options [Boolean] replace_errors_with_untyped
26
+ # @option options [Boolean] replace_unresolved_with_untyped
33
27
  # @option options [Boolean] comments
28
+ # @option options [Parlour::RbiGenerator] generator
29
+ # @option options [Parlour::RbiGenerator::Namespace] root
34
30
  # @return [void]
35
31
  def initialize(options)
36
- @rbi_contents = ['# typed: strong']
32
+ @parlour = options[:parlour] || Parlour::RbiGenerator.new
33
+ @current_object = options[:root] || @parlour.root
34
+
37
35
  @namespace_count = 0
38
36
  @method_count = 0
39
- @break_params = options[:break_params]
40
- @replace_errors_with_untyped = options[:replace_errors_with_untyped]
41
37
  @warnings = []
42
- @next_item_is_first_in_namespace = true
38
+
39
+ @replace_errors_with_untyped = options[:replace_errors_with_untyped]
40
+ @replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
43
41
 
44
42
  # Hook the logger so that messages are added as comments to the RBI file
45
- Logging.add_hook do |type, msg, item, indent_level = 0|
46
- rbi_contents << "#{' ' * (indent_level + 1)}# sord #{type} - #{msg}"
43
+ Logging.add_hook do |type, msg, item|
44
+ @current_object.add_comment_to_next_child("sord #{type} - #{msg}")
47
45
  end if options[:comments]
48
46
 
49
47
  # Hook the logger so that warnings are collected
50
- Logging.add_hook do |type, msg, item, indent_level = 0|
51
- warnings << [msg, item, rbi_contents.length] \
52
- if type == :warn
48
+ Logging.add_hook do |type, msg, item|
49
+ # TODO: is it possible to get line numbers here?
50
+ warnings << [msg, item, 0] if type == :warn
53
51
  end
54
52
  end
55
53
 
@@ -65,66 +63,41 @@ module Sord
65
63
  @method_count += 1
66
64
  end
67
65
 
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
-
76
66
  # Given a YARD CodeObject, add lines defining its mixins (that is, extends
77
67
  # and includes) to the current RBI file. Returns the number of mixins.
78
68
  # @param [YARD::CodeObjects::Base] item
79
- # @param [Integer] indent_level
80
69
  # @return [Integer]
81
- def add_mixins(item, indent_level)
82
- includes = item.instance_mixins
83
- extends = item.class_mixins
84
-
85
- extends.reverse_each do |this_extend|
86
- rbi_contents << "#{' ' * (indent_level + 1)}extend #{this_extend.path}"
70
+ def add_mixins(item)
71
+ item.instance_mixins.reverse_each do |i|
72
+ @current_object.create_include(i.path.to_s)
87
73
  end
88
- includes.reverse_each do |this_include|
89
- rbi_contents << "#{' ' * (indent_level + 1)}include #{this_include.path}"
74
+ item.class_mixins.reverse_each do |e|
75
+ @current_object.create_extend(e.path.to_s)
90
76
  end
91
77
 
92
- extends.length + includes.length
78
+ item.instance_mixins.length + item.class_mixins.length
93
79
  end
94
80
 
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
81
+ # Given a YARD NamespaceObject, add lines defining constants.
82
+ # @param [YARD::CodeObjects::NamespaceObject] item
100
83
  # @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} }"
84
+ def add_constants(item)
85
+ item.constants.each do |constant|
86
+ # Take a constant (like "A::B::CONSTANT"), split it on each '::', and
87
+ # set the constant name to the last string in the array.
88
+ constant_name = constant.to_s.split('::').last
89
+
90
+ # Add the constant to the current object being generated.
91
+ @current_object.create_constant(constant_name, value: "T.let(#{constant.value}, T.untyped)")
118
92
  end
119
93
  end
120
94
 
121
95
  # Given a YARD NamespaceObject, add lines defining its methods and their
122
96
  # signatures to the current RBI file.
123
97
  # @param [YARD::CodeObjects::NamespaceObject] item
124
- # @param [Integer] indent_level
125
98
  # @return [void]
126
- def add_methods(item, indent_level)
127
- item.meths.each do |meth|
99
+ def add_methods(item)
100
+ item.meths(inherited: false).each do |meth|
128
101
  count_method
129
102
 
130
103
  # If the method is an alias, skip it so we don't define it as a
@@ -133,39 +106,20 @@ module Sord
133
106
  next
134
107
  end
135
108
 
136
- add_blank
137
-
138
- parameter_list = meth.parameters.map do |name, default|
139
- # Handle these three main cases:
140
- # - def method(param) or def method(param:)
141
- # - def method(param: 'default')
142
- # - def method(param = 'default')
143
- if default.nil?
144
- "#{name}"
145
- elsif !default.nil? && name.end_with?(':')
146
- "#{name} #{default}"
147
- else
148
- "#{name} = #{default}"
149
- end
150
- end.join(", ")
151
-
152
109
  # This is better than iterating over YARD's "@param" tags directly
153
110
  # because it includes parameters without documentation
154
111
  # (The gsubs allow for better splat-argument compatibility)
155
- parameter_names_to_tags = meth.parameters.map do |name, _|
156
- [name, meth.tags('param')
157
- .find { |p| p.name&.gsub('*', '') == name.gsub('*', '') }]
112
+ parameter_names_and_defaults_to_tags = meth.parameters.map do |name, default|
113
+ [[name, default], meth.tags('param')
114
+ .find { |p| p.name&.gsub('*', '')&.gsub(':', '') == name.gsub('*', '').gsub(':', '') }]
158
115
  end.to_h
159
116
 
160
- sig_params_list = parameter_names_to_tags.map do |name, tag|
161
- name = name.gsub('*', '')
117
+ parameter_types = parameter_names_and_defaults_to_tags.map do |name_and_default, tag|
118
+ name = name_and_default.first
162
119
 
163
120
  if tag
164
- "#{name}: #{TypeConverter.yard_to_sorbet(tag.types, meth, indent_level, @replace_errors_with_untyped)}"
121
+ TypeConverter.yard_to_sorbet(tag.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
165
122
  elsif name.start_with? '&'
166
- # Cut the ampersand from the block parameter name
167
- name = name.gsub('&', '')
168
-
169
123
  # Find yieldparams and yieldreturn
170
124
  yieldparams = meth.tags('yieldparam')
171
125
  yieldreturn = meth.tag('yieldreturn')&.types
@@ -174,17 +128,17 @@ module Sord
174
128
 
175
129
  # Create strings
176
130
  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?
131
+ "#{param.name.gsub('*', '')}: #{TypeConverter.yard_to_sorbet(param.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)}" unless param.name.nil?
178
132
  end.join(', ')
179
- return_string = TypeConverter.yard_to_sorbet(yieldreturn, meth, indent_level, @replace_errors_with_untyped)
133
+ return_string = TypeConverter.yard_to_sorbet(yieldreturn, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
180
134
 
181
135
  # Create proc types, if possible
182
136
  if yieldparams.empty? && yieldreturn.nil?
183
- "#{name}: T.untyped"
137
+ 'T.untyped'
184
138
  elsif yieldreturn.nil?
185
- "#{name}: T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.void"
139
+ "T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.void"
186
140
  else
187
- "#{name}: T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.returns(#{return_string})"
141
+ "T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.returns(#{return_string})"
188
142
  end
189
143
  elsif meth.path.end_with? '='
190
144
  # Look for the matching getter method
@@ -192,114 +146,126 @@ module Sord
192
146
  getter = item.meths.find { |m| m.path == getter_path }
193
147
 
194
148
  unless getter
195
- if parameter_names_to_tags.length == 1 \
149
+ if parameter_names_and_defaults_to_tags.length == 1 \
196
150
  && meth.tags('param').length == 1 \
197
151
  && meth.tag('param').types
198
152
 
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)}"
153
+ Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
154
+ next TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
201
155
  else
202
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth, indent_level)
203
- next "#{name}: T.untyped"
156
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
157
+ next 'T.untyped'
204
158
  end
205
159
  end
206
160
 
207
161
  inferred_type = TypeConverter.yard_to_sorbet(
208
- getter.tags('return').flat_map(&:types), meth, indent_level, @replace_errors_with_untyped)
162
+ getter.tags('return').flat_map(&:types), meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
209
163
 
210
- Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth, indent_level)
211
- # Get rid of : on keyword arguments.
212
- name = name.chop if name.end_with?(':')
213
- "#{name}: #{inferred_type}"
164
+ Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth)
165
+ inferred_type
214
166
  else
215
167
  # Is this the only argument, and was a @param specified without an
216
168
  # argument name? If so, infer it
217
- if parameter_names_to_tags.length == 1 \
169
+ if parameter_names_and_defaults_to_tags.length == 1 \
218
170
  && meth.tags('param').length == 1 \
219
171
  && meth.tag('param').types
220
172
 
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)}"
173
+ Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
174
+ TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
223
175
  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"
176
+ Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
177
+ 'T.untyped'
228
178
  end
229
179
  end
230
180
  end
231
181
 
232
182
  return_tags = meth.tags('return')
233
183
  returns = if return_tags.length == 0
234
- Logging.omit("no YARD return type given, using T.untyped", meth, indent_level)
235
- "returns(T.untyped)"
184
+ Logging.omit("no YARD return type given, using T.untyped", meth)
185
+ 'T.untyped'
236
186
  elsif return_tags.length == 1 && return_tags&.first&.types&.first&.downcase == "void"
237
- "void"
187
+ nil
238
188
  else
239
- "returns(#{TypeConverter.yard_to_sorbet(meth.tag('return').types, meth, indent_level, @replace_errors_with_untyped)})"
189
+ TypeConverter.yard_to_sorbet(meth.tag('return').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
240
190
  end
241
191
 
242
- prefix = meth.scope == :class ? 'self.' : ''
243
-
244
- add_signature(sig_params_list, returns, indent_level)
192
+ parlour_params = parameter_names_and_defaults_to_tags
193
+ .zip(parameter_types)
194
+ .map do |((name, default), _), type|
195
+ # If the default is "nil" but the type is not nilable, then it
196
+ # should become nilable
197
+ # (T.untyped can include nil, so don't alter that)
198
+ type = "T.nilable(#{type})" \
199
+ if default == 'nil' && !type.start_with?('T.nilable') && type != 'T.untyped'
200
+ Parlour::RbiGenerator::Parameter.new(
201
+ name.to_s,
202
+ type: type,
203
+ default: default
204
+ )
205
+ end
245
206
 
246
- rbi_contents << "#{' ' * (indent_level + 1)}def #{prefix}#{meth.name}(#{parameter_list}); end"
207
+ @current_object.create_method(
208
+ meth.name.to_s,
209
+ parameters: parlour_params,
210
+ returns: returns,
211
+ class_method: meth.scope == :class
212
+ )
247
213
  end
248
214
  end
249
215
 
250
216
  # Given a YARD NamespaceObject, add lines defining its mixins, methods
251
217
  # and children to the RBI file.
252
218
  # @param [YARD::CodeObjects::NamespaceObject] item
253
- # @param [Integer] indent_level
254
219
  # @return [void]
255
- def add_namespace(item, indent_level = 0)
220
+ def add_namespace(item)
256
221
  count_namespace
257
- add_blank
258
222
 
259
- if item.type == :class && item.superclass.to_s != "Object"
260
- rbi_contents << "#{' ' * indent_level}class #{item.name} < #{item.superclass.path}"
261
- else
262
- rbi_contents << "#{' ' * indent_level}#{item.type} #{item.name}"
263
- end
223
+ superclass = nil
224
+ superclass = item.superclass.path.to_s if item.type == :class && item.superclass.to_s != "Object"
264
225
 
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
269
- add_methods(item, indent_level)
226
+ parent = @current_object
227
+ @current_object = item.type == :class \
228
+ ? parent.create_class(item.name.to_s, superclass: superclass)
229
+ : parent.create_module(item.name.to_s)
270
230
 
271
- item.children.select { |x| [:class, :module].include?(x.type) }
272
- .each { |child| add_namespace(child, indent_level + 1) }
231
+ add_mixins(item)
232
+ add_methods(item)
233
+ add_constants(item)
273
234
 
274
- self.next_item_is_first_in_namespace = false
235
+ item.children.select { |x| [:class, :module].include?(x.type) }
236
+ .each { |child| add_namespace(child) }
275
237
 
276
- rbi_contents << "#{' ' * indent_level}end"
238
+ @current_object = parent
277
239
  end
278
240
 
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
241
+ # Populates the RBI generator with the contents of the YARD registry. You
242
+ # must load the YARD registry first!
243
+ # @return [void]
244
+ def populate
283
245
  # Generate top-level modules, which recurses to all modules
284
246
  YARD::Registry.root.children
285
247
  .select { |x| [:class, :module].include?(x.type) }
286
248
  .each { |child| add_namespace(child) }
249
+ end
287
250
 
288
- rbi_contents.join("\n")
251
+ # Populates the RBI generator with the contents of the YARD registry, then
252
+ # uses the loaded Parlour::RbiGenerator to generate the RBI file. You must
253
+ # load the YARD registry first!
254
+ # @return [void]
255
+ def generate
256
+ populate
257
+ @parlour.rbi
289
258
  end
290
259
 
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
260
+ # Loads the YARD registry, populates the RBI file, and prints any relevant
261
+ # final logs.
294
262
  # @return [void]
295
- def run(filename)
296
- raise 'No filename specified' unless filename
297
-
263
+ def run
298
264
  # Get YARD ready
299
265
  YARD::Registry.load!
300
266
 
301
- # Write the file
302
- File.write(filename, generate)
267
+ # Populate the RBI
268
+ populate
303
269
 
304
270
  if object_count.zero?
305
271
  Logging.warn("No objects processed.")
@@ -318,10 +284,10 @@ module Sord
318
284
  else
319
285
  Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
320
286
  end
321
- Logging.warn("Please edit the file near the line numbers given to fix these errors.")
287
+ Logging.warn("Please edit the file to fix these errors.")
322
288
  Logging.warn("Alternatively, edit your YARD documentation so that your types are valid and re-run Sord.")
323
- warnings.each do |(msg, item, line)|
324
- puts " #{"Line #{line} |".light_black} (#{item&.path&.bold}) #{msg}"
289
+ warnings.each do |(msg, item, _)|
290
+ puts " (#{Rainbow(item&.path).bold}) #{msg}"
325
291
  end
326
292
  end
327
293
  rescue
data/lib/sord/resolver.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  require 'stringio'
2
3
 
3
4
  module Sord
@@ -50,12 +51,18 @@ module Sord
50
51
  # @param [Object] item
51
52
  # @return [Boolean]
52
53
  def self.resolvable?(name, item)
53
- name_parts = name.split('::')
54
-
55
54
  current_context = item
56
55
  current_context = current_context.parent \
57
56
  until current_context.is_a?(YARD::CodeObjects::NamespaceObject)
58
57
 
58
+ # If there is any matching object directly in the heirarchy, this is
59
+ # always true. Ruby can do the resolution.
60
+ unless name.include?('::')
61
+ return true if current_context.path.split('::').include?(name)
62
+ end
63
+
64
+ name_parts = name.split('::')
65
+
59
66
  matching_paths = []
60
67
 
61
68
  loop do
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'yaml'
2
3
  require 'sord/logging'
3
4
  require 'sord/resolver'
@@ -94,11 +95,12 @@ module Sord
94
95
  # @param [YARD::CodeObjects::Base] item The CodeObject which the YARD type
95
96
  # is associated with. This is used for logging and can be nil, but this
96
97
  # will lead to less informative log messages.
97
- # @param [Integer] indent_level
98
98
  # @param [Boolean] replace_errors_with_untyped If true, T.untyped is used
99
99
  # instead of SORD_ERROR_ constants for unknown types.
100
+ # @param [Boolean] replace_unresolved_with_untyped If true, T.untyped is used
101
+ # when Sord is unable to resolve a constant.
100
102
  # @return [String]
101
- def self.yard_to_sorbet(yard, item = nil, indent_level = 0, replace_errors_with_untyped = false)
103
+ def self.yard_to_sorbet(yard, item = nil, replace_errors_with_untyped = false, replace_unresolved_with_untyped = false)
102
104
  case yard
103
105
  when nil # Type not specified
104
106
  "T.untyped"
@@ -111,15 +113,20 @@ module Sord
111
113
  # selection of any of the types
112
114
  types = yard
113
115
  .reject { |x| x == 'nil' }
114
- .map { |x| yard_to_sorbet(x, item, indent_level, replace_errors_with_untyped) }
116
+ .map { |x| yard_to_sorbet(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
115
117
  .uniq
116
118
  result = types.length == 1 ? types.first : "T.any(#{types.join(', ')})"
117
119
  result = "T.nilable(#{result})" if yard.include?('nil')
118
120
  result
119
121
  when /^#{SIMPLE_TYPE_REGEX}$/
122
+ if SORBET_SINGLE_ARG_GENERIC_TYPES.include?(yard)
123
+ return "T::#{yard}[T.untyped]"
124
+ elsif yard == "Hash"
125
+ return "T::Hash[T.untyped, T.untyped]"
126
+ end
120
127
  # If this doesn't begin with an uppercase letter, warn
121
128
  if /^[_a-z]/ === yard
122
- Logging.warn("#{yard} is probably not a type, but using anyway", item, indent_level)
129
+ Logging.warn("#{yard} is probably not a type, but using anyway", item)
123
130
  end
124
131
 
125
132
  # Check if whatever has been specified is actually resolvable; if not,
@@ -127,18 +134,23 @@ module Sord
127
134
  if item && !Resolver.resolvable?(yard, item)
128
135
  if Resolver.path_for(yard)
129
136
  new_path = Resolver.path_for(yard)
130
- Logging.infer("#{yard} was resolved to #{new_path}", item, indent_level) \
137
+ Logging.infer("#{yard} was resolved to #{new_path}", item) \
131
138
  unless yard == new_path
132
139
  new_path
133
140
  else
134
- Logging.warn("#{yard} wasn't able to be resolved to a constant in this project", item, indent_level)
135
- yard
141
+ if replace_unresolved_with_untyped
142
+ Logging.warn("#{yard} wasn't able to be resolved to a constant in this project, replaced with T.untyped", item)
143
+ 'T.untyped'
144
+ else
145
+ Logging.warn("#{yard} wasn't able to be resolved to a constant in this project", item)
146
+ yard
147
+ end
136
148
  end
137
149
  else
138
150
  yard
139
151
  end
140
152
  when DUCK_TYPE_REGEX
141
- Logging.duck("#{yard} looks like a duck type, replacing with T.untyped", item, indent_level)
153
+ Logging.duck("#{yard} looks like a duck type, replacing with T.untyped", item)
142
154
  'T.untyped'
143
155
  when /^#{GENERIC_TYPE_REGEX}$/
144
156
  generic_type = $1
@@ -146,46 +158,72 @@ module Sord
146
158
 
147
159
  if SORBET_SUPPORTED_GENERIC_TYPES.include?(generic_type)
148
160
  parameters = split_type_parameters(type_parameters)
149
- .map { |x| yard_to_sorbet(x, item, indent_level, replace_errors_with_untyped) }
161
+ .map { |x| yard_to_sorbet(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
150
162
  if SORBET_SINGLE_ARG_GENERIC_TYPES.include?(generic_type) && parameters.length > 1
151
163
  "T::#{generic_type}[T.any(#{parameters.join(', ')})]"
152
164
  elsif generic_type == 'Class' && parameters.length == 1
153
165
  "T.class_of(#{parameters.first})"
166
+ elsif generic_type == 'Hash'
167
+ if parameters.length == 2
168
+ "T::Hash[#{parameters.join(', ')}]"
169
+ else
170
+ handle_sord_error(parameters.join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
171
+ end
154
172
  else
155
173
  "T::#{generic_type}[#{parameters.join(', ')}]"
156
174
  end
157
175
  else
158
- Logging.warn("unsupported generic type #{generic_type.inspect} in #{yard.inspect}", item, indent_level)
159
- replace_errors_with_untyped ? "T.untyped" : "SORD_ERROR_#{generic_type.gsub(/[^0-9A-Za-z_]/i, '')}"
176
+ return handle_sord_error(
177
+ generic_type,
178
+ "unsupported generic type #{generic_type.inspect} in #{yard.inspect}",
179
+ item,
180
+ replace_errors_with_untyped
181
+ )
160
182
  end
161
183
  # Converts ordered lists like Array(Symbol, String) or (Symbol, String)
162
184
  # into Sorbet Tuples like [Symbol, String].
163
185
  when ORDERED_LIST_REGEX
164
186
  type_parameters = $1
165
187
  parameters = split_type_parameters(type_parameters)
166
- .map { |x| yard_to_sorbet(x, item, indent_level, replace_errors_with_untyped) }
188
+ .map { |x| yard_to_sorbet(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
167
189
  "[#{parameters.join(', ')}]"
168
190
  when SHORTHAND_HASH_SYNTAX
169
191
  type_parameters = $1
170
192
  parameters = split_type_parameters(type_parameters)
171
- .map { |x| yard_to_sorbet(x, item, indent_level, replace_errors_with_untyped) }
172
- "T::Hash<#{parameters.join(', ')}>"
193
+ .map { |x| yard_to_sorbet(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
194
+ # Return a warning about an invalid hash when it has more or less than two elements.
195
+ if parameters.length == 2
196
+ "T::Hash[#{parameters.join(', ')}]"
197
+ else
198
+ handle_sord_error(parameters.join, "Invalid hash, must have exactly two types: #{yard.inspect}.", item, replace_errors_with_untyped)
199
+ end
173
200
  when SHORTHAND_ARRAY_SYNTAX
174
201
  type_parameters = $1
175
202
  parameters = split_type_parameters(type_parameters)
176
- .map { |x| yard_to_sorbet(x, item, indent_level, replace_errors_with_untyped) }
203
+ .map { |x| yard_to_sorbet(x, item, replace_errors_with_untyped, replace_unresolved_with_untyped) }
177
204
  parameters.one? \
178
- ? "T::Array<#{parameters.first}>"
179
- : "T::Array<T.any(#{parameters.join(', ')})>"
205
+ ? "T::Array[#{parameters.first}]"
206
+ : "T::Array[T.any(#{parameters.join(', ')})]"
180
207
  else
181
208
  # Check for literals
182
209
  from_yaml = YAML.load(yard) rescue nil
183
210
  return from_yaml.class.to_s \
184
211
  if [Symbol, Float, Integer].include?(from_yaml.class)
185
212
 
186
- Logging.warn("#{yard.inspect} does not appear to be a type", item, indent_level)
187
- replace_errors_with_untyped ? "T.untyped" : "SORD_ERROR_#{yard.gsub(/[^0-9A-Za-z_]/i, '')}"
213
+ return handle_sord_error(yard.to_s, "#{yard.inspect} does not appear to be a type", item, replace_errors_with_untyped)
188
214
  end
189
215
  end
216
+
217
+ # Handles SORD_ERRORs.
218
+ #
219
+ # @param [String] name
220
+ # @param [String] log_warning
221
+ # @param [YARD::CodeObjects::Base] item
222
+ # @param [Boolean] replace_errors_with_untyped
223
+ # @return [String]
224
+ def self.handle_sord_error(name, log_warning, item, replace_errors_with_untyped)
225
+ Logging.warn(log_warning, item)
226
+ return replace_errors_with_untyped ? "T.untyped" : "SORD_ERROR_#{name.gsub(/[^0-9A-Za-z_]/i, '')}"
227
+ end
190
228
  end
191
229
  end
data/lib/sord/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # typed: strong
2
2
  module Sord
3
- VERSION = '0.8.0'
3
+ VERSION = '0.9.0'
4
4
  end
data/lib/sord.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # typed: strong
2
2
  require 'sord/version'
3
3
  require 'sord/rbi_generator'
4
+ require 'sord/parlour_plugin'
4
5
  require 'yard'
5
6
  require 'sorbet-runtime'