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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- data/.gitignore +0 -0
- data/.parlour +6 -2
- data/.rspec +0 -0
- data/.travis.yml +0 -0
- data/CHANGELOG.md +58 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +71 -43
- data/Rakefile +25 -7
- data/exe/sord +45 -7
- data/lib/sord.rb +1 -1
- data/lib/sord/generator.rb +601 -0
- data/lib/sord/logging.rb +0 -0
- data/lib/sord/parlour_plugin.rb +23 -2
- data/lib/sord/resolver.rb +4 -0
- data/lib/sord/type_converter.rb +71 -63
- data/lib/sord/version.rb +1 -1
- data/rbi/sord.rbi +211 -30
- data/sord.gemspec +1 -1
- metadata +10 -10
- data/lib/sord/rbi_generator.rb +0 -437
data/lib/sord.rb
CHANGED
@@ -0,0 +1,601 @@
|
|
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 type
|
10
|
+
# signature file.
|
11
|
+
class Generator
|
12
|
+
VALID_MODES = [:rbi, :rbs]
|
13
|
+
|
14
|
+
# @return [Integer] The number of objects this generator has processed so
|
15
|
+
# far.
|
16
|
+
def object_count
|
17
|
+
@namespace_count + @method_count
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Array<Array(String, YARD::CodeObjects::Base, Integer)>] The
|
21
|
+
# errors encountered by by the generator. Each element is of the form
|
22
|
+
# [message, item, line].
|
23
|
+
attr_reader :warnings
|
24
|
+
|
25
|
+
# Create a new generator.
|
26
|
+
# @param [Hash] options
|
27
|
+
# @option options [Symbol] mode
|
28
|
+
# @option options [Integer] break_params
|
29
|
+
# @option options [Boolean] replace_errors_with_untyped
|
30
|
+
# @option options [Boolean] replace_unresolved_with_untyped
|
31
|
+
# @option options [Boolean] comments
|
32
|
+
# @option options [Parlour::Generator] generator
|
33
|
+
# @option options [Parlour::TypedObject] root
|
34
|
+
# @return [void]
|
35
|
+
def initialize(options)
|
36
|
+
@mode = options[:mode].to_sym rescue options[:mode]
|
37
|
+
raise "invalid mode #{@mode}, expected one of: #{VALID_MODES.join(', ')}" \
|
38
|
+
unless VALID_MODES.include?(@mode)
|
39
|
+
|
40
|
+
@parlour = options[:parlour] || \
|
41
|
+
case @mode
|
42
|
+
when :rbi
|
43
|
+
Parlour::RbiGenerator.new
|
44
|
+
when :rbs
|
45
|
+
Parlour::RbsGenerator.new
|
46
|
+
end
|
47
|
+
@current_object = options[:root] || @parlour.root
|
48
|
+
|
49
|
+
@namespace_count = 0
|
50
|
+
@method_count = 0
|
51
|
+
@warnings = []
|
52
|
+
|
53
|
+
@replace_errors_with_untyped = options[:replace_errors_with_untyped]
|
54
|
+
@replace_unresolved_with_untyped = options[:replace_unresolved_with_untyped]
|
55
|
+
@keep_original_comments = options[:keep_original_comments]
|
56
|
+
@skip_constants = options[:skip_constants]
|
57
|
+
@use_original_initialize_return = options[:use_original_initialize_return]
|
58
|
+
|
59
|
+
# Hook the logger so that messages are added as comments
|
60
|
+
Logging.add_hook do |type, msg, item|
|
61
|
+
@current_object.add_comment_to_next_child("sord #{type} - #{msg}")
|
62
|
+
end if options[:sord_comments]
|
63
|
+
|
64
|
+
# Hook the logger so that warnings are collected
|
65
|
+
Logging.add_hook do |type, msg, item|
|
66
|
+
# TODO: is it possible to get line numbers here?
|
67
|
+
warnings << [msg, item, 0] if type == :warn
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Increment the namespace counter.
|
72
|
+
# @return [void]
|
73
|
+
def count_namespace
|
74
|
+
@namespace_count += 1
|
75
|
+
end
|
76
|
+
|
77
|
+
# Increment the method counter.
|
78
|
+
# @return [void]
|
79
|
+
def count_method
|
80
|
+
@method_count += 1
|
81
|
+
end
|
82
|
+
|
83
|
+
# Given a YARD CodeObject, add lines defining its mixins (that is, extends
|
84
|
+
# and includes) to the current file. Returns the number of mixins.
|
85
|
+
# @param [YARD::CodeObjects::Base] item
|
86
|
+
# @return [Integer]
|
87
|
+
def add_mixins(item)
|
88
|
+
item.instance_mixins.reverse_each do |i|
|
89
|
+
@current_object.create_include(i.path.to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
# YARD 0.9.26 makes extends appear in the same order as code
|
93
|
+
# (includes are still reversed)
|
94
|
+
if Gem::Version.new(YARD::VERSION) >= Gem::Version.new("0.9.26")
|
95
|
+
item.class_mixins.each do |e|
|
96
|
+
@current_object.create_extend(e.path.to_s)
|
97
|
+
end
|
98
|
+
else
|
99
|
+
item.class_mixins.reverse_each do |e|
|
100
|
+
@current_object.create_extend(e.path.to_s)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
item.instance_mixins.length + item.class_mixins.length
|
105
|
+
end
|
106
|
+
|
107
|
+
# Given a YARD NamespaceObject, add lines defining constants.
|
108
|
+
# @param [YARD::CodeObjects::NamespaceObject] item
|
109
|
+
# @return [void]
|
110
|
+
def add_constants(item)
|
111
|
+
inserted_constant_names = Set.new
|
112
|
+
|
113
|
+
item.constants(included: false).each do |constant|
|
114
|
+
# Take a constant (like "A::B::CONSTANT"), split it on each '::', and
|
115
|
+
# set the constant name to the last string in the array.
|
116
|
+
constant_name = constant.to_s.split('::').last
|
117
|
+
if inserted_constant_names.include?(constant_name) && @mode == :rbs
|
118
|
+
Logging.warn("RBS doesn't support duplicate constants, but '#{constant_name}' was duplicated - dropping future occurences", constant)
|
119
|
+
next
|
120
|
+
end
|
121
|
+
inserted_constant_names << constant_name
|
122
|
+
|
123
|
+
# Add the constant to the current object being generated.
|
124
|
+
case @mode
|
125
|
+
when :rbi
|
126
|
+
@current_object.create_constant(constant_name, value: "T.let(#{constant.value}, T.untyped)") do |c|
|
127
|
+
c.add_comments(constant.docstring.all.split("\n"))
|
128
|
+
end
|
129
|
+
when :rbs
|
130
|
+
@current_object.create_constant(constant_name, type: Parlour::Types::Untyped.new) do |c|
|
131
|
+
c.add_comments(constant.docstring.all.split("\n"))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Adds comments to an object based on a docstring.
|
138
|
+
# @param [YARD::CodeObjects::NamespaceObject] item
|
139
|
+
# @param [Parlour::TypedObject] typed_object
|
140
|
+
# @return [void]
|
141
|
+
def add_comments(item, typed_object)
|
142
|
+
if @keep_original_comments
|
143
|
+
typed_object.add_comments(item.docstring.all.split("\n"))
|
144
|
+
else
|
145
|
+
parser = YARD::Docstring.parser
|
146
|
+
parser.parse(item.docstring.all)
|
147
|
+
|
148
|
+
docs_array = parser.text.split("\n")
|
149
|
+
|
150
|
+
# Add @param tags if there are any with names and descriptions.
|
151
|
+
params = parser.tags.select { |tag| tag.tag_name == 'param' && tag.is_a?(YARD::Tags::Tag) && !tag.name.nil? }
|
152
|
+
# Add a blank line if there's anything before the params.
|
153
|
+
docs_array << '' if docs_array.length.positive? && params.length.positive?
|
154
|
+
params.each do |param|
|
155
|
+
docs_array << '' if docs_array.last != '' && docs_array.length.positive?
|
156
|
+
# Output params in the form of:
|
157
|
+
# _@param_ `foo` — Lorem ipsum.
|
158
|
+
# _@param_ `foo`
|
159
|
+
if param.text.nil? || param.text == ''
|
160
|
+
docs_array << "_@param_ `#{param.name}`"
|
161
|
+
else
|
162
|
+
docs_array << "_@param_ `#{param.name}` — #{param.text.gsub("\n", " ")}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Add @return tags (there could possibly be more than one, despite this not being supported)
|
167
|
+
returns = parser.tags.select { |tag| tag.tag_name == 'return' && tag.is_a?(YARD::Tags::Tag) && !tag.text.nil? && tag.text.strip != '' }
|
168
|
+
# Add a blank line if there's anything before the returns.
|
169
|
+
docs_array << '' if docs_array.length.positive? && returns.length.positive?
|
170
|
+
returns.each do |retn|
|
171
|
+
docs_array << '' if docs_array.last != '' && docs_array.length.positive?
|
172
|
+
# Output returns in the form of:
|
173
|
+
# _@return_ — Lorem ipsum.
|
174
|
+
docs_array << "_@return_ — #{retn.text}"
|
175
|
+
end
|
176
|
+
|
177
|
+
# Iterate through the @example tags for a given YARD doc and output them in Markdown codeblocks.
|
178
|
+
examples = parser.tags.select { |tag| tag.tag_name == 'example' && tag.is_a?(YARD::Tags::Tag) }
|
179
|
+
examples.each do |example|
|
180
|
+
# Only add a blank line if there's anything before the example.
|
181
|
+
docs_array << '' if docs_array.length.positive?
|
182
|
+
# Include the example's 'name' if there is one.
|
183
|
+
docs_array << example.name unless example.name.nil? || example.name == ""
|
184
|
+
docs_array << "```ruby"
|
185
|
+
docs_array.concat(example.text.split("\n"))
|
186
|
+
docs_array << "```"
|
187
|
+
end if examples.length.positive?
|
188
|
+
|
189
|
+
# Add @note and @deprecated tags.
|
190
|
+
notice_tags = parser.tags.select { |tag| ['note', 'deprecated'].include?(tag.tag_name) && tag.is_a?(YARD::Tags::Tag) }
|
191
|
+
# Add a blank line if there's anything before the params.
|
192
|
+
docs_array << '' if docs_array.last != '' && docs_array.length.positive? && notice_tags.length.positive?
|
193
|
+
notice_tags.each do |notice_tag|
|
194
|
+
docs_array << '' if docs_array.last != ''
|
195
|
+
# Output note/deprecated/see in the form of:
|
196
|
+
# _@note_ — Lorem ipsum.
|
197
|
+
# _@note_
|
198
|
+
if notice_tag.text.nil?
|
199
|
+
docs_array << "_@#{notice_tag.tag_name}_"
|
200
|
+
else
|
201
|
+
docs_array << "_@#{notice_tag.tag_name}_ — #{notice_tag.text}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Add @see tags.
|
206
|
+
see_tags = parser.tags.select { |tag| tag.tag_name == 'see' && tag.is_a?(YARD::Tags::Tag) }
|
207
|
+
# Add a blank line if there's anything before the params.
|
208
|
+
docs_array << '' if docs_array.last != '' && docs_array.length.positive? && see_tags.length.positive?
|
209
|
+
see_tags.each do |see_tag|
|
210
|
+
docs_array << '' if docs_array.last != ''
|
211
|
+
# Output note/deprecated/see in the form of:
|
212
|
+
# _@see_ `B` — Lorem ipsum.
|
213
|
+
# _@see_ `B`
|
214
|
+
if see_tag.text.nil?
|
215
|
+
docs_array << "_@see_ `#{see_tag.name}`"
|
216
|
+
else
|
217
|
+
docs_array << "_@see_ `#{see_tag.name}` — #{see_tag.text}"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# fix: yard text may contains multiple line. should deal \n.
|
222
|
+
# else generate text will be multiple line and only first line is commented
|
223
|
+
docs_array = docs_array.flat_map {|line| line.empty? ? [""] : line.split("\n")}
|
224
|
+
typed_object.add_comments(docs_array)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Given a YARD NamespaceObject, add lines defining its methods and their
|
229
|
+
# signatures to the current file.
|
230
|
+
# @param [YARD::CodeObjects::NamespaceObject] item
|
231
|
+
# @return [void]
|
232
|
+
def add_methods(item)
|
233
|
+
item.meths(inherited: false).each do |meth|
|
234
|
+
count_method
|
235
|
+
|
236
|
+
# If the method is an alias, skip it so we don't define it as a
|
237
|
+
# separate method.
|
238
|
+
if meth.is_alias?
|
239
|
+
next
|
240
|
+
end
|
241
|
+
|
242
|
+
# If the method is an attribute, it'll be handled by add_attributes
|
243
|
+
if meth.is_attribute?
|
244
|
+
next
|
245
|
+
end
|
246
|
+
|
247
|
+
# Sort parameters
|
248
|
+
meth.parameters.reverse.sort! { |pair1, pair2| sort_params(pair1, pair2) }
|
249
|
+
# This is better than iterating over YARD's "@param" tags directly
|
250
|
+
# because it includes parameters without documentation
|
251
|
+
# (The gsubs allow for better splat-argument compatibility)
|
252
|
+
parameter_names_and_defaults_to_tags = meth.parameters.map do |name, default|
|
253
|
+
[[name, default], meth.tags('param')
|
254
|
+
.find { |p| p.name&.gsub('*', '')&.gsub(':', '') == name.gsub('*', '').gsub(':', '') }]
|
255
|
+
end.to_h
|
256
|
+
|
257
|
+
parameter_types = parameter_names_and_defaults_to_tags.map do |name_and_default, tag|
|
258
|
+
name = name_and_default.first
|
259
|
+
|
260
|
+
if tag
|
261
|
+
TypeConverter.yard_to_parlour(tag.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
262
|
+
elsif name.start_with? '&'
|
263
|
+
# Find yieldparams and yieldreturn
|
264
|
+
yieldparams = meth.tags('yieldparam')
|
265
|
+
yieldreturn = meth.tag('yieldreturn')&.types
|
266
|
+
yieldreturn = nil if yieldreturn&.length == 1 &&
|
267
|
+
yieldreturn&.first&.downcase == 'void'
|
268
|
+
|
269
|
+
# Create strings
|
270
|
+
params = yieldparams.map do |param|
|
271
|
+
Parlour::Types::Proc::Parameter.new(
|
272
|
+
param.name.gsub('*', ''),
|
273
|
+
TypeConverter.yard_to_parlour(param.types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
274
|
+
)
|
275
|
+
end
|
276
|
+
returns = TypeConverter.yard_to_parlour(yieldreturn, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
277
|
+
|
278
|
+
# Create proc types, if possible
|
279
|
+
if yieldparams.empty? && yieldreturn.nil?
|
280
|
+
Parlour::Types::Untyped.new
|
281
|
+
else
|
282
|
+
Parlour::Types::Proc.new(params, yieldreturn.nil? ? nil : returns)
|
283
|
+
end
|
284
|
+
elsif meth.path.end_with? '='
|
285
|
+
# Look for the matching getter method
|
286
|
+
getter_path = meth.path[0...-1]
|
287
|
+
getter = item.meths.find { |m| m.path == getter_path }
|
288
|
+
|
289
|
+
unless getter
|
290
|
+
if parameter_names_and_defaults_to_tags.length == 1 \
|
291
|
+
&& meth.tags('param').length == 1 \
|
292
|
+
&& meth.tag('param').types
|
293
|
+
|
294
|
+
Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
|
295
|
+
next TypeConverter.yard_to_parlour(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
296
|
+
else
|
297
|
+
Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
|
298
|
+
next Parlour::Types::Untyped.new
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
return_types = getter.tags('return').flat_map(&:types)
|
303
|
+
unless return_types.any?
|
304
|
+
Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
|
305
|
+
next Parlour::Types::Untyped.new
|
306
|
+
end
|
307
|
+
inferred_type = TypeConverter.yard_to_parlour(
|
308
|
+
return_types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
309
|
+
|
310
|
+
Logging.infer("inferred type of parameter #{name.inspect} as #{inferred_type.describe} using getter's return type", meth)
|
311
|
+
inferred_type
|
312
|
+
else
|
313
|
+
# Is this the only argument, and was a @param specified without an
|
314
|
+
# argument name? If so, infer it
|
315
|
+
if parameter_names_and_defaults_to_tags.length == 1 \
|
316
|
+
&& meth.tags('param').length == 1 \
|
317
|
+
&& meth.tag('param').types
|
318
|
+
|
319
|
+
Logging.infer("argument name in single @param inferred as #{parameter_names_and_defaults_to_tags.first.first.first.inspect}", meth)
|
320
|
+
TypeConverter.yard_to_parlour(meth.tag('param').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
321
|
+
else
|
322
|
+
Logging.omit("no YARD type given for #{name.inspect}, using untyped", meth)
|
323
|
+
Parlour::Types::Untyped.new
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
return_tags = meth.tags('return')
|
329
|
+
returns = if meth.name == :initialize && !@use_original_initialize_return
|
330
|
+
nil
|
331
|
+
elsif return_tags.length == 0
|
332
|
+
Logging.omit("no YARD return type given, using untyped", meth)
|
333
|
+
Parlour::Types::Untyped.new
|
334
|
+
elsif return_tags.length == 1 && %w{void nil}.include?(return_tags&.first&.types&.first&.downcase)
|
335
|
+
nil
|
336
|
+
else
|
337
|
+
TypeConverter.yard_to_parlour(meth.tag('return').types, meth, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
338
|
+
end
|
339
|
+
|
340
|
+
rbs_block = nil
|
341
|
+
parlour_params = parameter_names_and_defaults_to_tags
|
342
|
+
.zip(parameter_types)
|
343
|
+
.map do |((name, default), _), type|
|
344
|
+
# If the default is "nil" but the type is not nilable, then it
|
345
|
+
# should become nilable
|
346
|
+
# (T.untyped can include nil, so don't alter that)
|
347
|
+
type = Parlour::Types::Nilable.new(type) \
|
348
|
+
if default == 'nil' && !type.is_a?(Parlour::Types::Nilable) && !type.is_a?(Parlour::Types::Untyped)
|
349
|
+
|
350
|
+
case @mode
|
351
|
+
when :rbi
|
352
|
+
Parlour::RbiGenerator::Parameter.new(
|
353
|
+
name.to_s,
|
354
|
+
type: type,
|
355
|
+
default: default
|
356
|
+
)
|
357
|
+
when :rbs
|
358
|
+
if name.start_with?('&')
|
359
|
+
rbs_block = type
|
360
|
+
nil
|
361
|
+
else
|
362
|
+
Parlour::RbsGenerator::Parameter.new(
|
363
|
+
name.to_s,
|
364
|
+
type: type,
|
365
|
+
required: default.nil?
|
366
|
+
)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
.compact
|
371
|
+
|
372
|
+
case @mode
|
373
|
+
when :rbi
|
374
|
+
@current_object.create_method(
|
375
|
+
meth.name.to_s,
|
376
|
+
parameters: parlour_params,
|
377
|
+
returns: returns,
|
378
|
+
class_method: meth.scope == :class
|
379
|
+
) do |m|
|
380
|
+
add_comments(meth, m)
|
381
|
+
end
|
382
|
+
when :rbs
|
383
|
+
@current_object.create_method(
|
384
|
+
meth.name.to_s,
|
385
|
+
[Parlour::RbsGenerator::MethodSignature.new(
|
386
|
+
parlour_params, returns, block: rbs_block && !rbs_block.is_a?(Parlour::Types::Untyped) \
|
387
|
+
? Parlour::RbsGenerator::Block.new(rbs_block, false)
|
388
|
+
: nil
|
389
|
+
)],
|
390
|
+
class_method: meth.scope == :class
|
391
|
+
) do |m|
|
392
|
+
add_comments(meth, m)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Given a YARD NamespaceObject, add lines defining either its class
|
399
|
+
# and instance attributes and their signatures to the current file.
|
400
|
+
# @param [YARD::CodeObjects::NamespaceObject] item
|
401
|
+
# @return [void]
|
402
|
+
def add_attributes(item)
|
403
|
+
[:class, :instance].each do |attr_loc|
|
404
|
+
# Grab attributes for the current location (class or instance)
|
405
|
+
attrs = item.attributes[attr_loc]
|
406
|
+
attrs.each do |name, attribute|
|
407
|
+
reader = attribute[:read]
|
408
|
+
writer = attribute[:write]
|
409
|
+
|
410
|
+
unless reader || writer
|
411
|
+
Logging.warn("attribute is not readable or writable somehow, skipping", attribute)
|
412
|
+
next
|
413
|
+
end
|
414
|
+
|
415
|
+
# Get all given types
|
416
|
+
yard_types = []
|
417
|
+
if reader
|
418
|
+
yard_types += reader.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
|
419
|
+
reader.tags('param').flat_map(&:types)
|
420
|
+
end
|
421
|
+
if writer
|
422
|
+
yard_types += writer.tags('return').flat_map(&:types).compact.reject { |x| x.downcase == 'void' } +
|
423
|
+
writer.tags('param').flat_map(&:types)
|
424
|
+
end
|
425
|
+
|
426
|
+
# Use untyped if not types specified anywhere, otherwise try to
|
427
|
+
# compute Parlour type given all these types
|
428
|
+
if yard_types.empty?
|
429
|
+
Logging.omit("no YARD type given for #{name.inspect}, using untyped", reader || writer)
|
430
|
+
parlour_type = Parlour::Types::Untyped.new
|
431
|
+
elsif yard_types.all? { |x| x == 'nil' }
|
432
|
+
# Nil attributes are extremely unusual, so just use untyped
|
433
|
+
parlour_type = Parlour::Types::Untyped.new
|
434
|
+
else
|
435
|
+
parlour_type = TypeConverter.yard_to_parlour(
|
436
|
+
yard_types, reader || writer, @replace_errors_with_untyped, @replace_unresolved_with_untyped)
|
437
|
+
end
|
438
|
+
|
439
|
+
# Generate attribute
|
440
|
+
if reader && writer
|
441
|
+
kind = :accessor
|
442
|
+
elsif reader
|
443
|
+
kind = :reader
|
444
|
+
elsif writer
|
445
|
+
kind = :writer
|
446
|
+
end
|
447
|
+
|
448
|
+
case @mode
|
449
|
+
when :rbi
|
450
|
+
@current_object.create_attribute(
|
451
|
+
name.to_s,
|
452
|
+
kind: kind,
|
453
|
+
type: parlour_type,
|
454
|
+
class_attribute: (attr_loc == :class)
|
455
|
+
) do |m|
|
456
|
+
add_comments(reader || writer, m)
|
457
|
+
end
|
458
|
+
when :rbs
|
459
|
+
if attr_loc == :class
|
460
|
+
Logging.warn("RBS doesn't support class attributes, dropping", reader || writer)
|
461
|
+
else
|
462
|
+
@current_object.create_attribute(
|
463
|
+
name.to_s,
|
464
|
+
kind: kind,
|
465
|
+
type: parlour_type,
|
466
|
+
) do |m|
|
467
|
+
add_comments(reader || writer, m)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Given a YARD NamespaceObject, add lines defining its mixins, methods
|
476
|
+
# and children to the file.
|
477
|
+
# @param [YARD::CodeObjects::NamespaceObject] item
|
478
|
+
# @return [void]
|
479
|
+
def add_namespace(item)
|
480
|
+
count_namespace
|
481
|
+
|
482
|
+
superclass = nil
|
483
|
+
superclass = item.superclass.path.to_s if item.type == :class && item.superclass.to_s != "Object"
|
484
|
+
|
485
|
+
parent = @current_object
|
486
|
+
@current_object = item.type == :class \
|
487
|
+
? parent.create_class(item.name.to_s, superclass: superclass)
|
488
|
+
: parent.create_module(item.name.to_s)
|
489
|
+
@current_object.add_comments(item.docstring.all.split("\n"))
|
490
|
+
|
491
|
+
add_mixins(item)
|
492
|
+
add_methods(item)
|
493
|
+
add_attributes(item)
|
494
|
+
add_constants(item) unless @skip_constants
|
495
|
+
|
496
|
+
item.children.select { |x| [:class, :module].include?(x.type) }
|
497
|
+
.each { |child| add_namespace(child) }
|
498
|
+
|
499
|
+
@current_object = parent
|
500
|
+
end
|
501
|
+
|
502
|
+
# Populates the generator with the contents of the YARD registry. You
|
503
|
+
# must load the YARD registry first!
|
504
|
+
# @return [void]
|
505
|
+
def populate
|
506
|
+
# Generate top-level modules, which recurses to all modules
|
507
|
+
YARD::Registry.root.children
|
508
|
+
.select { |x| [:class, :module].include?(x.type) }
|
509
|
+
.each { |child| add_namespace(child) }
|
510
|
+
end
|
511
|
+
|
512
|
+
# Populates the generator with the contents of the YARD registry, then
|
513
|
+
# uses the loaded Parlour::Generator to generate the file. You must
|
514
|
+
# load the YARD registry first!
|
515
|
+
# @return [void]
|
516
|
+
def generate
|
517
|
+
populate
|
518
|
+
@parlour.send(@mode)
|
519
|
+
end
|
520
|
+
|
521
|
+
# Loads the YARD registry, populates the file, and prints any relevant
|
522
|
+
# final logs.
|
523
|
+
# @return [void]
|
524
|
+
def run
|
525
|
+
# Get YARD ready
|
526
|
+
YARD::Registry.load!
|
527
|
+
|
528
|
+
# Populate the type information file
|
529
|
+
populate
|
530
|
+
|
531
|
+
if object_count.zero?
|
532
|
+
Logging.warn("No objects processed.")
|
533
|
+
Logging.warn("Have you definitely generated the YARD documentation for this project?")
|
534
|
+
Logging.warn("Run `yard` to generate docs.")
|
535
|
+
end
|
536
|
+
|
537
|
+
Logging.done("Processed #{object_count} objects (#{@namespace_count} namespaces and #{@method_count} methods)")
|
538
|
+
|
539
|
+
Logging.hooks.clear
|
540
|
+
|
541
|
+
unless warnings.empty?
|
542
|
+
Logging.warn("There were #{warnings.length} important warnings in the output file, listed below.")
|
543
|
+
if @replace_errors_with_untyped
|
544
|
+
Logging.warn("The types which caused them have been replaced with untyped.")
|
545
|
+
else
|
546
|
+
Logging.warn("The types which caused them have been replaced with SORD_ERROR_ constants.")
|
547
|
+
end
|
548
|
+
Logging.warn("Please edit the file to fix these errors.")
|
549
|
+
Logging.warn("Alternatively, edit your YARD documentation so that your types are valid and re-run Sord.")
|
550
|
+
warnings.each do |(msg, item, _)|
|
551
|
+
puts " (#{Rainbow(item&.path).bold}) #{msg}"
|
552
|
+
end
|
553
|
+
end
|
554
|
+
rescue
|
555
|
+
Logging.error($!)
|
556
|
+
$@.each do |line|
|
557
|
+
puts " #{line}"
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# Given two pairs of arrays representing method parameters, in the form
|
562
|
+
# of ["variable_name", "default_value"], sort the parameters so they're
|
563
|
+
# valid for Sorbet. Sorbet requires that, e.g. required kwargs go before
|
564
|
+
# optional kwargs.
|
565
|
+
#
|
566
|
+
# @param [Array] pair1
|
567
|
+
# @param [Array] pair2
|
568
|
+
# @return Integer
|
569
|
+
def sort_params(pair1, pair2)
|
570
|
+
pair1_type, pair2_type = [pair1, pair2].map do |pair|
|
571
|
+
if pair[0].start_with?('&')
|
572
|
+
:blk
|
573
|
+
elsif pair[0].start_with?('**')
|
574
|
+
:doublesplat
|
575
|
+
elsif pair[0].start_with?('*')
|
576
|
+
:splat
|
577
|
+
elsif !pair[0].end_with?(':') && pair[1].nil?
|
578
|
+
:required_ordered_param
|
579
|
+
elsif !pair[0].end_with?(':') && !pair[1].nil?
|
580
|
+
:optional_ordered_param
|
581
|
+
elsif pair[0].end_with?(':') && pair[1].nil?
|
582
|
+
:required_kwarg
|
583
|
+
elsif pair[0].end_with?(':') && !pair[1].nil?
|
584
|
+
:optional_kwarg
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
pair_type_order = {
|
589
|
+
required_ordered_param: 1,
|
590
|
+
optional_ordered_param: 2,
|
591
|
+
splat: 3,
|
592
|
+
required_kwarg: 4,
|
593
|
+
optional_kwarg: 5,
|
594
|
+
doublesplat: 6,
|
595
|
+
blk: 7
|
596
|
+
}
|
597
|
+
|
598
|
+
return pair_type_order[pair1_type] <=> pair_type_order[pair2_type]
|
599
|
+
end
|
600
|
+
end
|
601
|
+
end
|