sord 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'