sord 1.0.0 → 3.0.1

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.
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency 'yard'
26
26
  spec.add_dependency 'sorbet-runtime'
27
27
  spec.add_dependency 'commander', '~> 4.5'
28
- spec.add_dependency 'parlour', '~> 2.0'
28
+ spec.add_dependency 'parlour', '~> 5.0'
29
29
 
30
30
  spec.add_development_dependency "bundler", "~> 2.0"
31
31
  spec.add_development_dependency "rake", "~> 10.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sord
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Christiansen
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-16 00:00:00.000000000 Z
11
+ date: 2020-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: yard
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.0'
61
+ version: '5.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '2.0'
68
+ version: '5.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: bundler
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -136,7 +136,7 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
- description:
139
+ description:
140
140
  email:
141
141
  - aaronc20000@gmail.com
142
142
  executables:
@@ -158,9 +158,9 @@ files:
158
158
  - Rakefile
159
159
  - exe/sord
160
160
  - lib/sord.rb
161
+ - lib/sord/generator.rb
161
162
  - lib/sord/logging.rb
162
163
  - lib/sord/parlour_plugin.rb
163
- - lib/sord/rbi_generator.rb
164
164
  - lib/sord/resolver.rb
165
165
  - lib/sord/type_converter.rb
166
166
  - lib/sord/version.rb
@@ -170,7 +170,7 @@ homepage: https://github.com/AaronC81/sord
170
170
  licenses:
171
171
  - MIT
172
172
  metadata: {}
173
- post_install_message:
173
+ post_install_message:
174
174
  rdoc_options: []
175
175
  require_paths:
176
176
  - lib
@@ -185,8 +185,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
187
  requirements: []
188
- rubygems_version: 3.1.2
189
- signing_key:
188
+ rubygems_version: 3.0.3
189
+ signing_key:
190
190
  specification_version: 4
191
191
  summary: Generate Sorbet RBI files from YARD documentation
192
192
  test_files: []
@@ -1,437 +0,0 @@
1
- # typed: true
2
- require 'yard'
3
- require 'sord/type_converter'
4
- require 'sord/logging'
5
- require 'parlour'
6
- require 'rainbow'
7
-
8
- module Sord
9
- # Converts the current working directory's YARD registry into an RBI file.
10
- class RbiGenerator
11
- # @return [Integer] The number of objects this generator has processed so
12
- # far.
13
- def object_count
14
- @namespace_count + @method_count
15
- end
16
-
17
- # @return [Array<Array(String, YARD::CodeObjects::Base, Integer)>] The
18
- # errors encountered by by the generator. Each element is of the form
19
- # [message, item, line].
20
- attr_reader :warnings
21
-
22
- # Create a new RBI generator.
23
- # @param [Hash] options
24
- # @option options [Integer] break_params
25
- # @option options [Boolean] replace_errors_with_untyped
26
- # @option options [Boolean] replace_unresolved_with_untyped
27
- # @option options [Boolean] comments
28
- # @option options [Parlour::RbiGenerator] generator
29
- # @option options [Parlour::RbiGenerator::Namespace] root
30
- # @return [void]
31
- def initialize(options)
32
- @parlour = options[:parlour] || Parlour::RbiGenerator.new
33
- @current_object = options[:root] || @parlour.root
34
-
35
- @namespace_count = 0
36
- @method_count = 0
37
- @warnings = []
38
-
39
- @replace_errors_with_untyped = options[:replace_errors_with_untyped]
40
- @replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
41
- @keep_original_comments = options[:keep_original_comments]
42
- @skip_constants = options[:skip_constants]
43
-
44
- # Hook the logger so that messages are added as comments to the RBI file
45
- Logging.add_hook do |type, msg, item|
46
- @current_object.add_comment_to_next_child("sord #{type} - #{msg}")
47
- end if options[:sord_comments]
48
-
49
- # Hook the logger so that warnings are collected
50
- Logging.add_hook do |type, msg, item|
51
- # TODO: is it possible to get line numbers here?
52
- warnings << [msg, item, 0] if type == :warn
53
- end
54
- end
55
-
56
- # Increment the namespace counter.
57
- # @return [void]
58
- def count_namespace
59
- @namespace_count += 1
60
- end
61
-
62
- # Increment the method counter.
63
- # @return [void]
64
- def count_method
65
- @method_count += 1
66
- end
67
-
68
- # Given a YARD CodeObject, add lines defining its mixins (that is, extends
69
- # and includes) to the current RBI file. Returns the number of mixins.
70
- # @param [YARD::CodeObjects::Base] item
71
- # @return [Integer]
72
- def add_mixins(item)
73
- item.instance_mixins.reverse_each do |i|
74
- @current_object.create_include(i.path.to_s)
75
- end
76
- item.class_mixins.reverse_each do |e|
77
- @current_object.create_extend(e.path.to_s)
78
- end
79
-
80
- item.instance_mixins.length + item.class_mixins.length
81
- end
82
-
83
- # Given a YARD NamespaceObject, add lines defining constants.
84
- # @param [YARD::CodeObjects::NamespaceObject] item
85
- # @return [void]
86
- def add_constants(item)
87
- item.constants.each do |constant|
88
- # Take a constant (like "A::B::CONSTANT"), split it on each '::', and
89
- # set the constant name to the last string in the array.
90
- constant_name = constant.to_s.split('::').last
91
-
92
- # Add the constant to the current object being generated.
93
- @current_object.create_constant(constant_name, value: "T.let(#{constant.value}, T.untyped)") do |c|
94
- c.add_comments(constant.docstring.all.split("\n"))
95
- end
96
- end
97
- end
98
-
99
- # Given a YARD NamespaceObject, add lines defining its methods and their
100
- # signatures to the current RBI file.
101
- # @param [YARD::CodeObjects::NamespaceObject] item
102
- # @return [void]
103
- def add_methods(item)
104
- item.meths(inherited: false).each do |meth|
105
- count_method
106
-
107
- # If the method is an alias, skip it so we don't define it as a
108
- # separate method. Sorbet will handle it automatically.
109
- if meth.is_alias?
110
- next
111
- end
112
-
113
- # Sort parameters
114
- meth.parameters.reverse.sort! { |pair1, pair2| sort_params(pair1, pair2) }
115
- # This is better than iterating over YARD's "@param" tags directly
116
- # because it includes parameters without documentation
117
- # (The gsubs allow for better splat-argument compatibility)
118
- parameter_names_and_defaults_to_tags = meth.parameters.map do |name, default|
119
- [[name, default], meth.tags('param')
120
- .find { |p| p.name&.gsub('*', '')&.gsub(':', '') == name.gsub('*', '').gsub(':', '') }]
121
- end.to_h
122
-
123
- parameter_types = parameter_names_and_defaults_to_tags.map do |name_and_default, tag|
124
- name = name_and_default.first
125
-
126
- if tag
127
- TypeConverter.yard_to_sorbet(tag.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
128
- elsif name.start_with? '&'
129
- # Find yieldparams and yieldreturn
130
- yieldparams = meth.tags('yieldparam')
131
- yieldreturn = meth.tag('yieldreturn')&.types
132
- yieldreturn = nil if yieldreturn&.length == 1 &&
133
- yieldreturn&.first&.downcase == 'void'
134
-
135
- # Create strings
136
- params_string = yieldparams.map do |param|
137
- "#{param.name.gsub('*', '')}: #{TypeConverter.yard_to_sorbet(param.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)}" unless param.name.nil?
138
- end.join(', ')
139
- return_string = TypeConverter.yard_to_sorbet(yieldreturn, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
140
-
141
- # Create proc types, if possible
142
- if yieldparams.empty? && yieldreturn.nil?
143
- 'T.untyped'
144
- elsif yieldreturn.nil?
145
- "T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.void"
146
- else
147
- "T.proc#{params_string.empty? ? '' : ".params(#{params_string})"}.returns(#{return_string})"
148
- end
149
- elsif meth.path.end_with? '='
150
- # Look for the matching getter method
151
- getter_path = meth.path[0...-1]
152
- getter = item.meths.find { |m| m.path == getter_path }
153
-
154
- unless getter
155
- if parameter_names_and_defaults_to_tags.length == 1 \
156
- && meth.tags('param').length == 1 \
157
- && meth.tag('param').types
158
-
159
- Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
160
- next TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
161
- else
162
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
163
- next 'T.untyped'
164
- end
165
- end
166
-
167
- return_types = getter.tags('return').flat_map(&:types)
168
- unless return_types.any?
169
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
170
- next 'T.untyped'
171
- end
172
- inferred_type = TypeConverter.yard_to_sorbet(
173
- return_types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
174
-
175
- Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type} using getter's return type", meth)
176
- inferred_type
177
- else
178
- # Is this the only argument, and was a @param specified without an
179
- # argument name? If so, infer it
180
- if parameter_names_and_defaults_to_tags.length == 1 \
181
- && meth.tags('param').length == 1 \
182
- && meth.tag('param').types
183
-
184
- Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
185
- TypeConverter.yard_to_sorbet(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
186
- else
187
- Logging.omit("no YARD type given for #{name.inspect}, using T.untyped", meth)
188
- 'T.untyped'
189
- end
190
- end
191
- end
192
-
193
- return_tags = meth.tags('return')
194
- returns = if return_tags.length == 0
195
- Logging.omit("no YARD return type given, using T.untyped", meth)
196
- 'T.untyped'
197
- elsif return_tags.length == 1 && return_tags&.first&.types&.first&.downcase == "void"
198
- nil
199
- else
200
- TypeConverter.yard_to_sorbet(meth.tag('return').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
201
- end
202
-
203
- parlour_params = parameter_names_and_defaults_to_tags
204
- .zip(parameter_types)
205
- .map do |((name, default), _), type|
206
- # If the default is "nil" but the type is not nilable, then it
207
- # should become nilable
208
- # (T.untyped can include nil, so don't alter that)
209
- type = "T.nilable(#{type})" \
210
- if default == 'nil' && !type.start_with?('T.nilable') && type != 'T.untyped'
211
- Parlour::RbiGenerator::Parameter.new(
212
- name.to_s,
213
- type: type,
214
- default: default
215
- )
216
- end
217
-
218
- @current_object.create_method(
219
- meth.name.to_s,
220
- parameters: parlour_params,
221
- returns: returns,
222
- class_method: meth.scope == :class
223
- ) do |m|
224
- if @keep_original_comments
225
- m.add_comments(meth.docstring.all.split("\n"))
226
- else
227
- parser = YARD::Docstring.parser
228
- parser.parse(meth.docstring.all)
229
-
230
- docs_array = parser.text.split("\n")
231
-
232
- # Add @param tags if there are any with names and descriptions.
233
- params = parser.tags.select { |tag| tag.tag_name == 'param' && tag.is_a?(YARD::Tags::Tag) && !tag.name.nil? }
234
- # Add a blank line if there's anything before the params.
235
- docs_array << '' if docs_array.length.positive? && params.length.positive?
236
- params.each do |param|
237
- docs_array << '' if docs_array.last != '' && docs_array.length.positive?
238
- # Output params in the form of:
239
- # _@param_ `foo` — Lorem ipsum.
240
- # _@param_ `foo`
241
- if param.text.nil? || param.text == ''
242
- docs_array << "_@param_ `#{param.name}`"
243
- else
244
- docs_array << "_@param_ `#{param.name}` — #{param.text.gsub("\n", " ")}"
245
- end
246
- end
247
-
248
- # Add @return tags (there could possibly be more than one, despite this not being supported)
249
- returns = parser.tags.select { |tag| tag.tag_name == 'return' && tag.is_a?(YARD::Tags::Tag) && !tag.text.nil? && tag.text.strip != '' }
250
- # Add a blank line if there's anything before the returns.
251
- docs_array << '' if docs_array.length.positive? && returns.length.positive?
252
- returns.each do |retn|
253
- docs_array << '' if docs_array.last != '' && docs_array.length.positive?
254
- # Output returns in the form of:
255
- # _@return_ — Lorem ipsum.
256
- docs_array << "_@return_ — #{retn.text}"
257
- end
258
-
259
- # Iterate through the @example tags for a given YARD doc and output them in Markdown codeblocks.
260
- examples = parser.tags.select { |tag| tag.tag_name == 'example' && tag.is_a?(YARD::Tags::Tag) }
261
- examples.each do |example|
262
- # Only add a blank line if there's anything before the example.
263
- docs_array << '' if docs_array.length.positive?
264
- # Include the example's 'name' if there is one.
265
- docs_array << example.name unless example.name.nil? || example.name == ""
266
- docs_array << "```ruby"
267
- docs_array.concat(example.text.split("\n"))
268
- docs_array << "```"
269
- end if examples.length.positive?
270
-
271
- # Add @note and @deprecated tags.
272
- notice_tags = parser.tags.select { |tag| ['note', 'deprecated'].include?(tag.tag_name) && tag.is_a?(YARD::Tags::Tag) }
273
- # Add a blank line if there's anything before the params.
274
- docs_array << '' if docs_array.last != '' && docs_array.length.positive? && notice_tags.length.positive?
275
- notice_tags.each do |notice_tag|
276
- docs_array << '' if docs_array.last != ''
277
- # Output note/deprecated/see in the form of:
278
- # _@note_ — Lorem ipsum.
279
- # _@note_
280
- if notice_tag.text.nil?
281
- docs_array << "_@#{notice_tag.tag_name}_"
282
- else
283
- docs_array << "_@#{notice_tag.tag_name}_ — #{notice_tag.text}"
284
- end
285
- end
286
-
287
- # Add @see tags.
288
- see_tags = parser.tags.select { |tag| tag.tag_name == 'see' && tag.is_a?(YARD::Tags::Tag) }
289
- # Add a blank line if there's anything before the params.
290
- docs_array << '' if docs_array.last != '' && docs_array.length.positive? && see_tags.length.positive?
291
- see_tags.each do |see_tag|
292
- docs_array << '' if docs_array.last != ''
293
- # Output note/deprecated/see in the form of:
294
- # _@see_ `B` — Lorem ipsum.
295
- # _@see_ `B`
296
- if see_tag.text.nil?
297
- docs_array << "_@see_ `#{see_tag.name}`"
298
- else
299
- docs_array << "_@see_ `#{see_tag.name}` — #{see_tag.text}"
300
- end
301
- end
302
-
303
- # fix: yard text may contains multiple line. should deal \n.
304
- # else generate text will be multiple line and only first line is commented
305
- docs_array = docs_array.flat_map {|line| line.empty? ? [""] : line.split("\n")}
306
- m.add_comments(docs_array)
307
- end
308
- end
309
- end
310
- end
311
-
312
- # Given a YARD NamespaceObject, add lines defining its mixins, methods
313
- # and children to the RBI file.
314
- # @param [YARD::CodeObjects::NamespaceObject] item
315
- # @return [void]
316
- def add_namespace(item)
317
- count_namespace
318
-
319
- superclass = nil
320
- superclass = item.superclass.path.to_s if item.type == :class && item.superclass.to_s != "Object"
321
-
322
- parent = @current_object
323
- @current_object = item.type == :class \
324
- ? parent.create_class(item.name.to_s, superclass: superclass)
325
- : parent.create_module(item.name.to_s)
326
- @current_object.add_comments(item.docstring.all.split("\n"))
327
-
328
- add_mixins(item)
329
- add_methods(item)
330
- add_constants(item) unless @skip_constants
331
-
332
- item.children.select { |x| [:class, :module].include?(x.type) }
333
- .each { |child| add_namespace(child) }
334
-
335
- @current_object = parent
336
- end
337
-
338
- # Populates the RBI generator with the contents of the YARD registry. You
339
- # must load the YARD registry first!
340
- # @return [void]
341
- def populate
342
- # Generate top-level modules, which recurses to all modules
343
- YARD::Registry.root.children
344
- .select { |x| [:class, :module].include?(x.type) }
345
- .each { |child| add_namespace(child) }
346
- end
347
-
348
- # Populates the RBI generator with the contents of the YARD registry, then
349
- # uses the loaded Parlour::RbiGenerator to generate the RBI file. You must
350
- # load the YARD registry first!
351
- # @return [void]
352
- def generate
353
- populate
354
- @parlour.rbi
355
- end
356
-
357
- # Loads the YARD registry, populates the RBI file, and prints any relevant
358
- # final logs.
359
- # @return [void]
360
- def run
361
- # Get YARD ready
362
- YARD::Registry.load!
363
-
364
- # Populate the RBI
365
- populate
366
-
367
- if object_count.zero?
368
- Logging.warn("No objects processed.")
369
- Logging.warn("Have you definitely generated the YARD documentation for this project?")
370
- Logging.warn("Run `yard` to generate docs.")
371
- end
372
-
373
- Logging.done("Processed #{object_count} objects (#{@namespace_count} namespaces and #{@method_count} methods)")
374
-
375
- Logging.hooks.clear
376
-
377
- unless warnings.empty?
378
- Logging.warn("There were #{warnings.length} important warnings in the RBI file, listed below.")
379
- if @replace_errors_with_untyped
380
- Logging.warn("The types which caused them have been replaced with T.untyped.")
381
- else
382
- Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
383
- end
384
- Logging.warn("Please edit the file to fix these errors.")
385
- Logging.warn("Alternatively, edit your YARD documentation so that your types are valid and re-run Sord.")
386
- warnings.each do |(msg, item, _)|
387
- puts " (#{Rainbow(item&.path).bold}) #{msg}"
388
- end
389
- end
390
- rescue
391
- Logging.error($!)
392
- $@.each do |line|
393
- puts " #{line}"
394
- end
395
- end
396
-
397
- # Given two pairs of arrays representing method parameters, in the form
398
- # of ["variable_name", "default_value"], sort the parameters so they're
399
- # valid for Sorbet. Sorbet requires that, e.g. required kwargs go before
400
- # optional kwargs.
401
- #
402
- # @param [Array] pair1
403
- # @param [Array] pair2
404
- # @return Integer
405
- def sort_params(pair1, pair2)
406
- pair1_type, pair2_type = [pair1, pair2].map do |pair|
407
- if pair[0].start_with?('&')
408
- :blk
409
- elsif pair[0].start_with?('**')
410
- :doublesplat
411
- elsif pair[0].start_with?('*')
412
- :splat
413
- elsif !pair[0].end_with?(':') && pair[1].nil?
414
- :required_ordered_param
415
- elsif !pair[0].end_with?(':') && !pair[1].nil?
416
- :optional_ordered_param
417
- elsif pair[0].end_with?(':') && pair[1].nil?
418
- :required_kwarg
419
- elsif pair[0].end_with?(':') && !pair[1].nil?
420
- :optional_kwarg
421
- end
422
- end
423
-
424
- pair_type_order = {
425
- required_ordered_param: 1,
426
- optional_ordered_param: 2,
427
- splat: 3,
428
- required_kwarg: 4,
429
- optional_kwarg: 5,
430
- doublesplat: 6,
431
- blk: 7
432
- }
433
-
434
- return pair_type_order[pair1_type] <=> pair_type_order[pair2_type]
435
- end
436
- end
437
- end