docscribe 1.0.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.
@@ -0,0 +1,534 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require 'docscribe/infer'
5
+
6
+ module Docscribe
7
+ module InlineRewriter
8
+ # +Docscribe::InlineRewriter.insert_comments+ -> Object
9
+ #
10
+ # Method documentation.
11
+ #
12
+ # @param [Object] code Param documentation.
13
+ # @param [Boolean] rewrite Param documentation.
14
+ # @param [nil] config Param documentation.
15
+ # @return [Object]
16
+ def self.insert_comments(code, rewrite: false, config: nil)
17
+ buffer = Parser::Source::Buffer.new('(inline)')
18
+ buffer.source = code
19
+ parser = Parser::CurrentRuby.new
20
+ ast = parser.parse(buffer)
21
+ return code unless ast
22
+
23
+ config ||= Docscribe::Config.load
24
+
25
+ collector = Collector.new(buffer)
26
+ collector.process(ast)
27
+
28
+ rewriter = Parser::Source::TreeRewriter.new(buffer)
29
+
30
+ collector.insertions
31
+ .sort_by { |ins| ins.node.loc.expression.begin_pos }
32
+ .reverse_each do |ins|
33
+ bol_range = line_start_range(buffer, ins.node)
34
+
35
+ if rewrite
36
+ # If there is a comment block immediately above, remove it (and its trailing blank lines)
37
+ if (range = comment_block_removal_range(buffer, bol_range.begin_pos))
38
+ rewriter.remove(range)
39
+ end
40
+ elsif already_has_doc_immediately_above?(buffer, bol_range.begin_pos)
41
+ # Skip if a doc already exists immediately above
42
+ next
43
+ end
44
+
45
+ doc = build_doc_for_node(buffer, ins, config)
46
+ next unless doc && !doc.empty?
47
+
48
+ rewriter.insert_before(bol_range, doc)
49
+ end
50
+
51
+ rewriter.process
52
+ end
53
+
54
+ # +Docscribe::InlineRewriter.line_start_range+ -> Range
55
+ #
56
+ # Method documentation.
57
+ #
58
+ # @param [Object] buffer Param documentation.
59
+ # @param [Object] node Param documentation.
60
+ # @return [Range]
61
+ def self.line_start_range(buffer, node)
62
+ start_pos = node.loc.expression.begin_pos
63
+ src = buffer.source
64
+ bol = src.rindex("\n", start_pos - 1) || -1
65
+ Parser::Source::Range.new(buffer, bol + 1, bol + 1)
66
+ end
67
+
68
+ # +Docscribe::InlineRewriter.node_name+ -> Object
69
+ #
70
+ # Method documentation.
71
+ #
72
+ # @param [Object] node Param documentation.
73
+ # @return [Object]
74
+ def self.node_name(node)
75
+ case node.type
76
+ when :def
77
+ node.children[0]
78
+ when :defs
79
+ node.children[1] # method name symbol
80
+ end
81
+ end
82
+
83
+ # +Docscribe::InlineRewriter.comment_block_removal_range+ -> Range
84
+ #
85
+ # Method documentation.
86
+ #
87
+ # @param [Object] buffer Param documentation.
88
+ # @param [Object] def_bol_pos Param documentation.
89
+ # @return [Range]
90
+ def self.comment_block_removal_range(buffer, def_bol_pos)
91
+ src = buffer.source
92
+ lines = src.lines
93
+ # Find def line index
94
+ def_line_idx = src[0...def_bol_pos].count("\n")
95
+ i = def_line_idx - 1
96
+
97
+ # Walk up and skip blank lines directly above def
98
+ i -= 1 while i >= 0 && lines[i].strip.empty?
99
+ # Now if the nearest non-blank line isn't a comment, nothing to remove
100
+ return nil unless i >= 0 && lines[i] =~ /^\s*#/
101
+
102
+ # Find the start of the contiguous comment block
103
+ start_idx = i
104
+ start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
105
+ start_idx += 1
106
+
107
+ # End position is at def_bol_pos; start position is BOL of start_idx
108
+ # Compute absolute buffer positions
109
+ # Position of BOL for start_idx:
110
+ start_pos = 0
111
+ if start_idx.positive?
112
+ # Sum lengths of all preceding lines
113
+ start_pos = lines[0...start_idx].join.length
114
+ end
115
+
116
+ Parser::Source::Range.new(buffer, start_pos, def_bol_pos)
117
+ end
118
+
119
+ # +Docscribe::InlineRewriter.already_has_doc_immediately_above?+ -> Object
120
+ #
121
+ # Method documentation.
122
+ #
123
+ # @param [Object] buffer Param documentation.
124
+ # @param [Object] insert_pos Param documentation.
125
+ # @return [Object]
126
+ def self.already_has_doc_immediately_above?(buffer, insert_pos)
127
+ src = buffer.source
128
+ lines = src.lines
129
+ current_line_index = src[0...insert_pos].count("\n")
130
+ i = current_line_index - 1
131
+ i -= 1 while i >= 0 && lines[i].strip.empty?
132
+ return false if i.negative?
133
+
134
+ !!(lines[i] =~ /^\s*#/)
135
+ end
136
+
137
+ # +Docscribe::InlineRewriter.build_doc_for_node+ -> Object
138
+ #
139
+ # Method documentation.
140
+ #
141
+ # @param [Object] _buffer Param documentation.
142
+ # @param [Object] insertion Param documentation.
143
+ # @param [Object] config Param documentation.
144
+ # @raise [StandardError]
145
+ # @return [Object]
146
+ # @return [nil] if StandardError
147
+ def self.build_doc_for_node(_buffer, insertion, config)
148
+ node = insertion.node
149
+ indent = ' ' * node.loc.expression.column
150
+
151
+ name =
152
+ case node.type
153
+ when :def then node.children[0]
154
+ when :defs then node.children[1]
155
+ end
156
+
157
+ scope = insertion.scope
158
+ visibility = insertion.visibility
159
+
160
+ method_symbol = scope == :instance ? '#' : '.'
161
+ container = insertion.container
162
+
163
+ # Params
164
+ params_block = config.emit_param_tags? ? build_params_block(node, indent) : nil
165
+
166
+ # Raises (rescue and/or raise calls)
167
+ raise_types = config.emit_raise_tags? ? Docscribe::Infer.infer_raises_from_node(node) : []
168
+
169
+ # Returns: normal + conditional rescue returns
170
+ spec = Docscribe::Infer.returns_spec_from_node(node)
171
+ normal_type = spec[:normal]
172
+ rescue_specs = spec[:rescues]
173
+
174
+ lines = []
175
+ if config.emit_header?
176
+ lines << "#{indent}# +#{container}#{method_symbol}#{name}+ -> #{normal_type}"
177
+ lines << "#{indent}#"
178
+ end
179
+
180
+ # Default doc text (configurable per scope/vis)
181
+ lines << "#{indent}# #{config.default_message(scope, visibility)}"
182
+ lines << "#{indent}#"
183
+
184
+ if config.emit_visibility_tags?
185
+ case visibility
186
+ when :private then lines << "#{indent}# @private"
187
+ when :protected then lines << "#{indent}# @protected"
188
+ end
189
+ end
190
+
191
+ lines.concat(params_block) if params_block
192
+
193
+ raise_types.each { |rt| lines << "#{indent}# @raise [#{rt}]" } if config.emit_raise_tags?
194
+
195
+ lines << "#{indent}# @return [#{normal_type}]" if config.emit_return_tag?(scope, visibility)
196
+
197
+ if config.emit_rescue_conditional_returns?
198
+ rescue_specs.each do |(exceptions, rtype)|
199
+ ex_display = exceptions.join(', ')
200
+ lines << "#{indent}# @return [#{rtype}] if #{ex_display}"
201
+ end
202
+ end
203
+
204
+ lines.map { |l| "#{l}\n" }.join
205
+ rescue StandardError
206
+ nil
207
+ end
208
+
209
+ # +Docscribe::InlineRewriter.build_params_block+ -> Object?
210
+ #
211
+ # Method documentation.
212
+ #
213
+ # @param [Object] node Param documentation.
214
+ # @param [Object] indent Param documentation.
215
+ # @return [Object?]
216
+ def self.build_params_block(node, indent)
217
+ args =
218
+ case node.type
219
+ when :def then node.children[1]
220
+ when :defs then node.children[2] # FIX: args is children[2], not [3]
221
+ end
222
+ return nil unless args
223
+
224
+ params = []
225
+ (args.children || []).each do |a|
226
+ case a.type
227
+ when :arg
228
+ name = a.children.first.to_s
229
+ ty = Infer.infer_param_type(name, nil)
230
+ params << "#{indent}# @param [#{ty}] #{name} Param documentation."
231
+ when :optarg
232
+ name, default = *a
233
+ ty = Infer.infer_param_type(name.to_s, default&.loc&.expression&.source)
234
+ params << "#{indent}# @param [#{ty}] #{name} Param documentation."
235
+ when :kwarg
236
+ name = "#{a.children.first}:"
237
+ ty = Infer.infer_param_type(name, nil)
238
+ pname = name.sub(/:$/, '')
239
+ params << "#{indent}# @param [#{ty}] #{pname} Param documentation."
240
+ when :kwoptarg
241
+ name, default = *a
242
+ name = "#{name}:"
243
+ ty = Infer.infer_param_type(name, default&.loc&.expression&.source)
244
+ pname = name.sub(/:$/, '')
245
+ params << "#{indent}# @param [#{ty}] #{pname} Param documentation."
246
+ when :restarg
247
+ name = "*#{a.children.first}"
248
+ ty = Infer.infer_param_type(name, nil)
249
+ pname = a.children.first.to_s
250
+ params << "#{indent}# @param [#{ty}] #{pname} Param documentation."
251
+ when :kwrestarg
252
+ name = "**#{a.children.first || 'kwargs'}"
253
+ ty = Infer.infer_param_type(name, nil)
254
+ pname = (a.children.first || 'kwargs').to_s
255
+ params << "#{indent}# @param [#{ty}] #{pname} Param documentation."
256
+ when :blockarg
257
+ name = "&#{a.children.first}"
258
+ ty = Infer.infer_param_type(name, nil)
259
+ pname = a.children.first.to_s
260
+ params << "#{indent}# @param [#{ty}] #{pname} Param documentation."
261
+ when :forward_arg
262
+ # Ruby 3 '...' forwarding; skip
263
+ end
264
+ end
265
+ params.empty? ? nil : params
266
+ end
267
+
268
+ class VisibilityCtx
269
+ attr_accessor :default_instance_vis, :default_class_vis, :inside_sclass
270
+ attr_reader :explicit_instance, :explicit_class
271
+
272
+ # +Docscribe::InlineRewriter::VisibilityCtx#initialize+ -> Object
273
+ #
274
+ # Method documentation.
275
+ #
276
+ # @return [Object]
277
+ def initialize
278
+ @default_instance_vis = :public
279
+ @default_class_vis = :public
280
+ @explicit_instance = {} # { name_sym => :private|:protected|:public }
281
+ @explicit_class = {} # { name_sym => :private|:protected|:public }
282
+ @inside_sclass = false
283
+ end
284
+
285
+ # +Docscribe::InlineRewriter::VisibilityCtx#dup+ -> Object
286
+ #
287
+ # Method documentation.
288
+ #
289
+ # @return [Object]
290
+ def dup
291
+ c = VisibilityCtx.new
292
+ c.default_instance_vis = default_instance_vis
293
+ c.default_class_vis = default_class_vis
294
+ c.inside_sclass = inside_sclass
295
+ c.explicit_instance.merge!(explicit_instance)
296
+ c.explicit_class.merge!(explicit_class)
297
+ c
298
+ end
299
+ end
300
+
301
+ # Walks nodes and records where to insert docstrings
302
+ class Collector < Parser::AST::Processor
303
+ Insertion = Struct.new(:node, :scope, :visibility, :container)
304
+
305
+ attr_reader :insertions
306
+
307
+ # +Docscribe::InlineRewriter::Collector#initialize+ -> Object
308
+ #
309
+ # Method documentation.
310
+ #
311
+ # @param [Object] buffer Param documentation.
312
+ # @return [Object]
313
+ def initialize(buffer)
314
+ super()
315
+ @buffer = buffer
316
+ @insertions = []
317
+ @name_stack = [] # e.g., ['Demo']
318
+ end
319
+
320
+ # +Docscribe::InlineRewriter::Collector#on_class+ -> Object
321
+ #
322
+ # Method documentation.
323
+ #
324
+ # @param [Object] node Param documentation.
325
+ # @return [Object]
326
+ def on_class(node)
327
+ cname_node, _super_node, body = *node
328
+ @name_stack.push(const_name(cname_node))
329
+ ctx = VisibilityCtx.new
330
+ process_body(body, ctx)
331
+ @name_stack.pop
332
+ node
333
+ end
334
+
335
+ # +Docscribe::InlineRewriter::Collector#on_module+ -> Object
336
+ #
337
+ # Method documentation.
338
+ #
339
+ # @param [Object] node Param documentation.
340
+ # @return [Object]
341
+ def on_module(node)
342
+ cname_node, body = *node
343
+ @name_stack.push(const_name(cname_node))
344
+ ctx = VisibilityCtx.new
345
+ process_body(body, ctx)
346
+ @name_stack.pop
347
+ node
348
+ end
349
+
350
+ # +Docscribe::InlineRewriter::Collector#on_def+ -> Object
351
+ #
352
+ # Method documentation.
353
+ #
354
+ # @param [Object] node Param documentation.
355
+ # @return [Object]
356
+ def on_def(node)
357
+ @insertions << Insertion.new(node, :instance, :public, current_container)
358
+ node
359
+ end
360
+
361
+ # +Docscribe::InlineRewriter::Collector#on_defs+ -> Object
362
+ #
363
+ # Method documentation.
364
+ #
365
+ # @param [Object] node Param documentation.
366
+ # @return [Object]
367
+ def on_defs(node)
368
+ @insertions << Insertion.new(node, :class, :public, current_container)
369
+ node
370
+ end
371
+
372
+ private
373
+
374
+ # +Docscribe::InlineRewriter::Collector#process_stmt+ -> Object
375
+ #
376
+ # Method documentation.
377
+ #
378
+ # @private
379
+ # @param [Object] node Param documentation.
380
+ # @param [Object] ctx Param documentation.
381
+ # @return [Object]
382
+ def process_stmt(node, ctx)
383
+ return unless node
384
+
385
+ case node.type
386
+ when :def
387
+ name, _args, _body = *node
388
+ if ctx.inside_sclass
389
+ vis = ctx.explicit_class[name] || ctx.default_class_vis
390
+ scope = :class
391
+ else
392
+ vis = ctx.explicit_instance[name] || ctx.default_instance_vis
393
+ scope = :instance
394
+ end
395
+ @insertions << Insertion.new(node, scope, vis, current_container)
396
+
397
+ when :defs
398
+ _, name, _args, _body = *node
399
+ vis = ctx.explicit_class[name] || ctx.default_class_vis
400
+ @insertions << Insertion.new(node, :class, vis, current_container)
401
+
402
+ when :sclass
403
+ recv, body = *node
404
+ inner_ctx = ctx.dup
405
+ inner_ctx.inside_sclass = self_node?(recv)
406
+ inner_ctx.default_class_vis = :public
407
+ process_body(body, inner_ctx)
408
+
409
+ when :send
410
+ process_visibility_send(node, ctx)
411
+
412
+ else
413
+ process(node)
414
+ end
415
+ end
416
+
417
+ # +Docscribe::InlineRewriter::Collector#process_visibility_send+ -> Object
418
+ #
419
+ # Method documentation.
420
+ #
421
+ # @private
422
+ # @param [Object] node Param documentation.
423
+ # @param [Object] ctx Param documentation.
424
+ # @return [Object]
425
+ def process_visibility_send(node, ctx)
426
+ recv, meth, *args = *node
427
+ return unless recv.nil? && %i[private protected public].include?(meth)
428
+
429
+ if args.empty?
430
+ # bare keyword: affects current def-target
431
+ if ctx.inside_sclass
432
+ ctx.default_class_vis = meth
433
+ else
434
+ ctx.default_instance_vis = meth
435
+ end
436
+ else
437
+ # explicit list: affects current def-target
438
+ args.each do |arg|
439
+ sym = extract_name_sym(arg)
440
+ next unless sym
441
+
442
+ if ctx.inside_sclass
443
+ ctx.explicit_class[sym] = meth
444
+ else
445
+ ctx.explicit_instance[sym] = meth
446
+ end
447
+
448
+ target = ctx.inside_sclass ? 'class' : 'instance'
449
+ if args.empty?
450
+ puts "[vis] bare #{meth} -> default_#{target}_vis=#{meth}"
451
+ else
452
+ puts "[vis] explicit #{meth} -> #{target} names=#{args.map { |a| extract_name_sym(a) }.inspect}"
453
+ end
454
+ end
455
+ end
456
+ end
457
+
458
+ # +Docscribe::InlineRewriter::Collector#extract_name_sym+ -> Object
459
+ #
460
+ # Method documentation.
461
+ #
462
+ # @private
463
+ # @param [Object] arg Param documentation.
464
+ # @return [Object]
465
+ def extract_name_sym(arg)
466
+ case arg.type
467
+ when :sym then arg.children.first
468
+ when :str then arg.children.first.to_sym
469
+ end
470
+ end
471
+
472
+ # +Docscribe::InlineRewriter::Collector#self_node?+ -> Object
473
+ #
474
+ # Method documentation.
475
+ #
476
+ # @private
477
+ # @param [Object] node Param documentation.
478
+ # @return [Object]
479
+ def self_node?(node)
480
+ node && node.type == :self
481
+ end
482
+
483
+ # +Docscribe::InlineRewriter::Collector#current_container+ -> Object
484
+ #
485
+ # Method documentation.
486
+ #
487
+ # @private
488
+ # @return [Object]
489
+ def current_container
490
+ @name_stack.empty? ? 'Object' : @name_stack.join('::')
491
+ end
492
+
493
+ # +Docscribe::InlineRewriter::Collector#const_name+ -> Object
494
+ #
495
+ # Method documentation.
496
+ #
497
+ # @private
498
+ # @param [Object] node Param documentation.
499
+ # @return [Object]
500
+ def const_name(node)
501
+ return 'Object' unless node
502
+
503
+ case node.type
504
+ when :const
505
+ scope, name = *node
506
+ scope_name = scope ? const_name(scope) : nil
507
+ [scope_name, name].compact.join('::')
508
+ when :cbase
509
+ '' # leading ::
510
+ else
511
+ node.loc.expression.source # fallback
512
+ end
513
+ end
514
+
515
+ # +Docscribe::InlineRewriter::Collector#process_body+ -> Object
516
+ #
517
+ # Method documentation.
518
+ #
519
+ # @private
520
+ # @param [Object] body Param documentation.
521
+ # @param [Object] ctx Param documentation.
522
+ # @return [Object]
523
+ def process_body(body, ctx)
524
+ return unless body
525
+
526
+ if body.type == :begin
527
+ body.children.each { |child| process_stmt(child, ctx) }
528
+ else
529
+ process_stmt(body, ctx)
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ VERSION = '1.0.0'
5
+ end
data/lib/docscribe.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ class Error < StandardError; end
5
+ end
6
+
7
+ require_relative 'docscribe/version'
8
+ require_relative 'docscribe/config'
9
+ require_relative 'docscribe/infer'
10
+ require_relative 'docscribe/inline_rewriter'
data/rakelib/docs.rake ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'yard'
5
+ require 'fileutils'
6
+
7
+ GEM_NAME = Bundler.load_gemspec(Dir.glob('*.gemspec').first).name
8
+ DOCS_REPO_NAME = "#{GEM_NAME}_docs".freeze
9
+ DOCS_REPO_PATH = "../#{DOCS_REPO_NAME}".freeze
10
+
11
+ namespace :docs do # rubocop:disable Metrics/BlockLength
12
+ desc 'Generate new docs and push them to repo'
13
+ task generate: :clean do
14
+ puts 'Generating docs...'
15
+ YARD::CLI::Yardoc.run
16
+ puts 'OK!'
17
+ end
18
+
19
+ desc 'Clean existing docs'
20
+ task :clean do
21
+ if File.directory?('doc')
22
+ FileUtils.rm_rf('doc')
23
+ puts 'Cleaned existing docs directory'
24
+ end
25
+ end
26
+
27
+ desc 'Pushes docs to github'
28
+ task push: :generate do
29
+ unless File.directory?(DOCS_REPO_PATH)
30
+ puts "Error: Docs repo not found at #{DOCS_REPO_PATH}"
31
+ puts 'Please clone the docs repo first:'
32
+ puts " git clone git@github.com:unurgunite/#{DOCS_REPO_NAME}.git #{DOCS_REPO_PATH}"
33
+ exit 1
34
+ end
35
+
36
+ puts "Copying docs to #{DOCS_REPO_PATH}..."
37
+ FileUtils.mkdir_p('doc') unless File.directory?('doc')
38
+ FileUtils.cp_r('doc/.', DOCS_REPO_PATH)
39
+
40
+ puts 'Changing dir...'
41
+ Dir.chdir(DOCS_REPO_PATH) do
42
+ puts 'Checking git status...'
43
+ status_output = `git status --porcelain`
44
+
45
+ if status_output.strip.empty?
46
+ puts 'No changes to commit'
47
+ else
48
+ puts 'Committing git changes...'
49
+ puts `git add .`
50
+ commit_result = `git commit -m "Update docs for #{GEM_NAME} #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}"`
51
+ puts commit_result
52
+
53
+ if $CHILD_STATUS.success?
54
+ puts 'Pushing to GitHub...'
55
+ push_result = `git push origin master 2>&1`
56
+ puts push_result
57
+ if $CHILD_STATUS.success?
58
+ puts 'Docs successfully pushed!'
59
+ else
60
+ puts 'Push failed!'
61
+ exit 1
62
+ end
63
+ else
64
+ puts 'Commit failed!'
65
+ exit 1
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ desc 'Generate and push docs in one command'
72
+ task deploy: :push
73
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/docscribe/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'docscribe'
7
+ spec.version = Docscribe::VERSION
8
+ spec.authors = ['unurgunite']
9
+ spec.email = ['senpaiguru1488@gmail.com']
10
+
11
+ spec.summary = 'Autogenerate documentation for Ruby code with YARD syntax.'
12
+ spec.homepage = 'https://github.com/unurgunite/docscribe'
13
+ spec.required_ruby_version = '>= 3.0'
14
+
15
+ spec.metadata['homepage_uri'] = spec.homepage
16
+ spec.metadata['source_code_uri'] = 'https://github.com/unurgunite/docscribe'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/unurgunite/docscribe/blob/master/CHANGELOG.md'
18
+ spec.metadata['rubygems_mfa_required'] = 'true'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
25
+ end
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency 'parser', '>= 3.0'
33
+ spec.add_development_dependency 'rake'
34
+ spec.add_development_dependency 'rspec', '~> 3.0'
35
+ spec.add_development_dependency 'rubocop'
36
+ spec.add_development_dependency 'rubocop-sorted_methods_by_call'
37
+ spec.add_development_dependency 'yard', '>= 0.9.34'
38
+
39
+ # For more information and examples about making a new gem, check out our
40
+ # guide at: https://bundler.io/guides/creating_gem.html
41
+ end