docscribe 1.0.0 → 1.2.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.
- checksums.yaml +4 -4
- data/README.md +692 -180
- data/exe/docscribe +2 -74
- data/lib/docscribe/cli/config_builder.rb +62 -0
- data/lib/docscribe/cli/init.rb +58 -0
- data/lib/docscribe/cli/options.rb +204 -0
- data/lib/docscribe/cli/run.rb +415 -0
- data/lib/docscribe/cli.rb +31 -0
- data/lib/docscribe/config/defaults.rb +71 -0
- data/lib/docscribe/config/emit.rb +126 -0
- data/lib/docscribe/config/filtering.rb +160 -0
- data/lib/docscribe/config/loader.rb +59 -0
- data/lib/docscribe/config/rbs.rb +51 -0
- data/lib/docscribe/config/sorbet.rb +87 -0
- data/lib/docscribe/config/sorting.rb +23 -0
- data/lib/docscribe/config/template.rb +176 -0
- data/lib/docscribe/config/utils.rb +102 -0
- data/lib/docscribe/config.rb +20 -230
- data/lib/docscribe/infer/ast_walk.rb +28 -0
- data/lib/docscribe/infer/constants.rb +11 -0
- data/lib/docscribe/infer/literals.rb +55 -0
- data/lib/docscribe/infer/names.rb +43 -0
- data/lib/docscribe/infer/params.rb +62 -0
- data/lib/docscribe/infer/raises.rb +68 -0
- data/lib/docscribe/infer/returns.rb +171 -0
- data/lib/docscribe/infer.rb +110 -259
- data/lib/docscribe/inline_rewriter/collector.rb +845 -0
- data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
- data/lib/docscribe/inline_rewriter/doc_builder.rb +605 -0
- data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
- data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
- data/lib/docscribe/inline_rewriter.rb +604 -425
- data/lib/docscribe/parsing.rb +120 -0
- data/lib/docscribe/types/provider_chain.rb +37 -0
- data/lib/docscribe/types/rbs/provider.rb +213 -0
- data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
- data/lib/docscribe/types/signature.rb +65 -0
- data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
- data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
- data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
- data/lib/docscribe/version.rb +1 -1
- data/lib/docscribe.rb +1 -0
- metadata +85 -17
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -73
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -73
- data/Rakefile +0 -12
- data/rakelib/docs.rake +0 -73
- data/stingray_docs_internal.gemspec +0 -41
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'docscribe/infer'
|
|
4
|
+
require 'docscribe/inline_rewriter/source_helpers'
|
|
5
|
+
|
|
6
|
+
module Docscribe
|
|
7
|
+
module InlineRewriter
|
|
8
|
+
# Build generated YARD-style doc lines for methods and attribute helpers.
|
|
9
|
+
#
|
|
10
|
+
# DocBuilder combines:
|
|
11
|
+
# - Ruby visibility/container metadata from Collector
|
|
12
|
+
# - optional external signatures from Sorbet/RBS providers
|
|
13
|
+
# - fallback AST inference from Docscribe::Infer
|
|
14
|
+
#
|
|
15
|
+
# It is responsible for producing complete doc blocks for aggressive mode
|
|
16
|
+
# and "missing lines only" payloads for safe merge mode.
|
|
17
|
+
module DocBuilder
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Build a complete doc block for one collected method insertion.
|
|
21
|
+
#
|
|
22
|
+
# External signatures, when available, override inferred param and return
|
|
23
|
+
# types.
|
|
24
|
+
#
|
|
25
|
+
# @note module_function: when included, also defines #build (instance visibility: private)
|
|
26
|
+
# @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
|
|
27
|
+
# @param [Docscribe::Config] config
|
|
28
|
+
# @param [Object, nil] signature_provider provider responding to
|
|
29
|
+
# `signature_for(container:, scope:, name:)`
|
|
30
|
+
# @raise [StandardError]
|
|
31
|
+
# @return [String, nil]
|
|
32
|
+
def build(insertion, config:, signature_provider: nil)
|
|
33
|
+
node = insertion.node
|
|
34
|
+
name = SourceHelpers.node_name(node)
|
|
35
|
+
return nil unless name
|
|
36
|
+
|
|
37
|
+
indent = SourceHelpers.line_indent(node)
|
|
38
|
+
scope = insertion.scope
|
|
39
|
+
visibility = insertion.visibility
|
|
40
|
+
container = insertion.container
|
|
41
|
+
method_symbol = scope == :instance ? '#' : '.'
|
|
42
|
+
|
|
43
|
+
external_sig = signature_provider&.signature_for(
|
|
44
|
+
container: container,
|
|
45
|
+
scope: scope,
|
|
46
|
+
name: name
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if config.emit_param_tags?
|
|
50
|
+
params_lines = build_params_lines(node, indent, external_sig: external_sig,
|
|
51
|
+
config: config)
|
|
52
|
+
end
|
|
53
|
+
raise_types = config.emit_raise_tags? ? Docscribe::Infer.infer_raises_from_node(node) : []
|
|
54
|
+
|
|
55
|
+
returns_spec = Docscribe::Infer.returns_spec_from_node(
|
|
56
|
+
node,
|
|
57
|
+
fallback_type: config.fallback_type,
|
|
58
|
+
nil_as_optional: config.nil_as_optional?
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
normal_type = external_sig&.return_type || returns_spec[:normal]
|
|
62
|
+
rescue_specs = returns_spec[:rescues] || []
|
|
63
|
+
|
|
64
|
+
lines = []
|
|
65
|
+
|
|
66
|
+
if config.emit_header?
|
|
67
|
+
lines << "#{indent}# +#{container}#{method_symbol}#{name}+ -> #{normal_type}"
|
|
68
|
+
lines << "#{indent}#"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
lines << "#{indent}# #{config.default_message(scope, visibility)}"
|
|
72
|
+
lines << "#{indent}#"
|
|
73
|
+
|
|
74
|
+
if config.emit_visibility_tags?
|
|
75
|
+
case visibility
|
|
76
|
+
when :private
|
|
77
|
+
lines << "#{indent}# @private"
|
|
78
|
+
when :protected
|
|
79
|
+
lines << "#{indent}# @protected"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if insertion.respond_to?(:module_function) && insertion.module_function
|
|
84
|
+
included_vis =
|
|
85
|
+
if insertion.respond_to?(:included_instance_visibility) && insertion.included_instance_visibility
|
|
86
|
+
insertion.included_instance_visibility
|
|
87
|
+
else
|
|
88
|
+
:private
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
|
|
92
|
+
"(instance visibility: #{included_vis})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
lines.concat(params_lines) if params_lines
|
|
96
|
+
raise_types.each { |rt| lines << "#{indent}# @raise [#{rt}]" } if config.emit_raise_tags?
|
|
97
|
+
lines << "#{indent}# @return [#{normal_type}]" if config.emit_return_tag?(scope, visibility)
|
|
98
|
+
|
|
99
|
+
if config.emit_rescue_conditional_returns?
|
|
100
|
+
rescue_specs.each do |exceptions, rtype|
|
|
101
|
+
lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
lines.map { |l| "#{l}\n" }.join
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build')
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build only the missing doc lines that should be merged into an existing
|
|
112
|
+
# doc-like block.
|
|
113
|
+
#
|
|
114
|
+
# This is used by safe mode for non-destructive updates.
|
|
115
|
+
#
|
|
116
|
+
# @note module_function: when included, also defines #build_merge_additions (instance visibility: private)
|
|
117
|
+
# @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
|
|
118
|
+
# @param [Array<String>] existing_lines
|
|
119
|
+
# @param [Docscribe::Config] config
|
|
120
|
+
# @param [Object, nil] signature_provider
|
|
121
|
+
# @raise [StandardError]
|
|
122
|
+
# @return [String, nil]
|
|
123
|
+
def build_merge_additions(insertion, existing_lines:, config:, signature_provider: nil)
|
|
124
|
+
node = insertion.node
|
|
125
|
+
name = SourceHelpers.node_name(node)
|
|
126
|
+
return '' unless name
|
|
127
|
+
|
|
128
|
+
indent = SourceHelpers.line_indent(node)
|
|
129
|
+
info = parse_existing_doc_tags(existing_lines)
|
|
130
|
+
scope = insertion.scope
|
|
131
|
+
visibility = insertion.visibility
|
|
132
|
+
|
|
133
|
+
external_sig = signature_provider&.signature_for(
|
|
134
|
+
container: insertion.container,
|
|
135
|
+
scope: scope,
|
|
136
|
+
name: name
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
returns_spec = Docscribe::Infer.returns_spec_from_node(
|
|
140
|
+
node,
|
|
141
|
+
fallback_type: config.fallback_type,
|
|
142
|
+
nil_as_optional: config.nil_as_optional?
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
normal_type = external_sig&.return_type || returns_spec[:normal]
|
|
146
|
+
rescue_specs = returns_spec[:rescues] || []
|
|
147
|
+
|
|
148
|
+
lines = []
|
|
149
|
+
lines << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
|
|
150
|
+
|
|
151
|
+
if config.emit_visibility_tags?
|
|
152
|
+
if visibility == :private && !info[:has_private]
|
|
153
|
+
lines << "#{indent}# @private"
|
|
154
|
+
elsif visibility == :protected && !info[:has_protected]
|
|
155
|
+
lines << "#{indent}# @protected"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
|
|
160
|
+
included_vis = insertion.included_instance_visibility || :private
|
|
161
|
+
lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
|
|
162
|
+
"(instance visibility: #{included_vis})"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if config.emit_param_tags?
|
|
166
|
+
all_params = build_params_lines(node, indent, external_sig: external_sig, config: config)
|
|
167
|
+
all_params&.each do |pl|
|
|
168
|
+
pname = extract_param_name_from_param_line(pl)
|
|
169
|
+
next if pname.nil? || info[:param_names].include?(pname)
|
|
170
|
+
|
|
171
|
+
lines << pl
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if config.emit_raise_tags?
|
|
176
|
+
inferred = Docscribe::Infer.infer_raises_from_node(node)
|
|
177
|
+
existing = info[:raise_types] || {}
|
|
178
|
+
missing = inferred.reject { |rt| existing[rt] }
|
|
179
|
+
missing.each { |rt| lines << "#{indent}# @raise [#{rt}]" }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if config.emit_return_tag?(scope, visibility) && !info[:has_return]
|
|
183
|
+
lines << "#{indent}# @return [#{normal_type}]"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if config.emit_rescue_conditional_returns? && !info[:has_return]
|
|
187
|
+
rescue_specs.each do |exceptions, rtype|
|
|
188
|
+
lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
useful = lines.reject { |l| l.strip == '#' }
|
|
193
|
+
return '' if useful.empty?
|
|
194
|
+
|
|
195
|
+
lines.map { |l| "#{l}\n" }.join
|
|
196
|
+
rescue StandardError => e
|
|
197
|
+
debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_merge_additions')
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Build structured missing-line information for safe merge mode.
|
|
202
|
+
#
|
|
203
|
+
# Returns both:
|
|
204
|
+
# - generated missing lines
|
|
205
|
+
# - structured reasons used by `--explain`
|
|
206
|
+
#
|
|
207
|
+
# @note module_function: when included, also defines #build_missing_merge_result (instance visibility: private)
|
|
208
|
+
# @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
|
|
209
|
+
# @param [Array<String>] existing_lines
|
|
210
|
+
# @param [Docscribe::Config] config
|
|
211
|
+
# @param [Object, nil] signature_provider
|
|
212
|
+
# @raise [StandardError]
|
|
213
|
+
# @return [Hash]
|
|
214
|
+
def build_missing_merge_result(insertion, existing_lines:, config:, signature_provider: nil)
|
|
215
|
+
node = insertion.node
|
|
216
|
+
name = SourceHelpers.node_name(node)
|
|
217
|
+
return { lines: [], reasons: [] } unless name
|
|
218
|
+
|
|
219
|
+
indent = SourceHelpers.line_indent(node)
|
|
220
|
+
info = parse_existing_doc_tags(existing_lines)
|
|
221
|
+
scope = insertion.scope
|
|
222
|
+
visibility = insertion.visibility
|
|
223
|
+
|
|
224
|
+
external_sig = signature_provider&.signature_for(
|
|
225
|
+
container: insertion.container,
|
|
226
|
+
scope: scope,
|
|
227
|
+
name: name
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
returns_spec = Docscribe::Infer.returns_spec_from_node(
|
|
231
|
+
node,
|
|
232
|
+
fallback_type: config.fallback_type,
|
|
233
|
+
nil_as_optional: config.nil_as_optional?
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
normal_type = external_sig&.return_type || returns_spec[:normal]
|
|
237
|
+
rescue_specs = returns_spec[:rescues] || []
|
|
238
|
+
|
|
239
|
+
lines = []
|
|
240
|
+
reasons = []
|
|
241
|
+
|
|
242
|
+
if config.emit_visibility_tags?
|
|
243
|
+
if visibility == :private && !info[:has_private]
|
|
244
|
+
lines << "#{indent}# @private\n"
|
|
245
|
+
reasons << { type: :missing_visibility, message: 'missing @private' }
|
|
246
|
+
elsif visibility == :protected && !info[:has_protected]
|
|
247
|
+
lines << "#{indent}# @protected\n"
|
|
248
|
+
reasons << { type: :missing_visibility, message: 'missing @protected' }
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
|
|
253
|
+
included_vis = insertion.included_instance_visibility || :private
|
|
254
|
+
lines << "#{indent}# @note module_function: when included, also defines ##{name} " \
|
|
255
|
+
"(instance visibility: #{included_vis})\n"
|
|
256
|
+
reasons << { type: :missing_module_function_note, message: 'missing module_function note' }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
if config.emit_param_tags?
|
|
260
|
+
all_params = build_params_lines(node, indent, external_sig: external_sig, config: config)
|
|
261
|
+
|
|
262
|
+
all_params&.each do |pl|
|
|
263
|
+
pname = extract_param_name_from_param_line(pl)
|
|
264
|
+
next if pname.nil? || info[:param_names].include?(pname)
|
|
265
|
+
|
|
266
|
+
lines << "#{pl}\n"
|
|
267
|
+
reasons << { type: :missing_param, message: "missing @param #{pname}", extra: { param: pname } }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if config.emit_raise_tags?
|
|
272
|
+
inferred = Docscribe::Infer.infer_raises_from_node(node)
|
|
273
|
+
existing = info[:raise_types] || {}
|
|
274
|
+
missing = inferred.reject { |rt| existing[rt] }
|
|
275
|
+
|
|
276
|
+
missing.each do |rt|
|
|
277
|
+
lines << "#{indent}# @raise [#{rt}]\n"
|
|
278
|
+
reasons << { type: :missing_raise, message: "missing @raise [#{rt}]", extra: { raise_type: rt } }
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if config.emit_return_tag?(scope, visibility) && !info[:has_return]
|
|
283
|
+
lines << "#{indent}# @return [#{normal_type}]\n"
|
|
284
|
+
reasons << { type: :missing_return, message: 'missing @return' }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
if config.emit_rescue_conditional_returns? && !info[:has_return]
|
|
288
|
+
rescue_specs.each do |exceptions, rtype|
|
|
289
|
+
lines << "#{indent}# @return [#{rtype}] if #{exceptions.join(', ')}\n"
|
|
290
|
+
reasons << {
|
|
291
|
+
type: :missing_return,
|
|
292
|
+
message: "missing conditional @return for #{exceptions.join(', ')}"
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
{ lines: lines, reasons: reasons }
|
|
298
|
+
rescue StandardError => e
|
|
299
|
+
debug_warn(e, insertion: insertion, name: name || '(unknown)', phase: 'DocBuilder.build_missing_merge_result')
|
|
300
|
+
{ lines: [], reasons: [] }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Method documentation.
|
|
304
|
+
#
|
|
305
|
+
# @note module_function: when included, also defines #parse_existing_doc_tags (instance visibility: private)
|
|
306
|
+
# @param [Object] lines Param documentation.
|
|
307
|
+
# @return [Hash]
|
|
308
|
+
def parse_existing_doc_tags(lines)
|
|
309
|
+
param_names = {}
|
|
310
|
+
has_return = false
|
|
311
|
+
has_private = false
|
|
312
|
+
has_protected = false
|
|
313
|
+
has_module_function_note = false
|
|
314
|
+
raise_types = {}
|
|
315
|
+
|
|
316
|
+
Array(lines).each do |line|
|
|
317
|
+
if (pname = extract_param_name_from_param_line(line))
|
|
318
|
+
param_names[pname] = true
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
has_return ||= line.match?(/^\s*#\s*@return\b/)
|
|
322
|
+
has_private ||= line.match?(/^\s*#\s*@private\b/)
|
|
323
|
+
has_protected ||= line.match?(/^\s*#\s*@protected\b/)
|
|
324
|
+
has_module_function_note ||= line.match?(/^\s*#\s*@note\s+module_function:/)
|
|
325
|
+
|
|
326
|
+
extract_raise_types_from_line(line).each { |t| raise_types[t] = true }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
param_names: param_names,
|
|
331
|
+
has_return: has_return,
|
|
332
|
+
raise_types: raise_types,
|
|
333
|
+
has_private: has_private,
|
|
334
|
+
has_protected: has_protected,
|
|
335
|
+
has_module_function_note: has_module_function_note
|
|
336
|
+
}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Method documentation.
|
|
340
|
+
#
|
|
341
|
+
# @note module_function: when included, also defines #extract_raise_types_from_line (instance visibility: private)
|
|
342
|
+
# @param [Object] line Param documentation.
|
|
343
|
+
# @raise [StandardError]
|
|
344
|
+
# @return [Object]
|
|
345
|
+
# @return [Array] if StandardError
|
|
346
|
+
def extract_raise_types_from_line(line)
|
|
347
|
+
return [] unless line.match?(/^\s*#\s*@raise\b/)
|
|
348
|
+
|
|
349
|
+
if (m = line.match(/^\s*#\s*@raise\s*\[([^\]]+)\]/))
|
|
350
|
+
parse_raise_bracket_list(m[1])
|
|
351
|
+
elsif (m = line.match(/^\s*#\s*@raise\s+([A-Z]\w*(?:::[A-Z]\w*)*)/))
|
|
352
|
+
[m[1]]
|
|
353
|
+
else
|
|
354
|
+
[]
|
|
355
|
+
end
|
|
356
|
+
rescue StandardError
|
|
357
|
+
[]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Method documentation.
|
|
361
|
+
#
|
|
362
|
+
# @note module_function: when included, also defines #parse_raise_bracket_list (instance visibility: private)
|
|
363
|
+
# @param [Object] s Param documentation.
|
|
364
|
+
# @return [Object]
|
|
365
|
+
def parse_raise_bracket_list(s)
|
|
366
|
+
s.to_s.split(',').map(&:strip).reject(&:empty?)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Build generated `@param` / `@option` lines for a method node.
|
|
370
|
+
#
|
|
371
|
+
# External signatures take precedence over inferred parameter types.
|
|
372
|
+
#
|
|
373
|
+
# @note module_function: when included, also defines #build_params_lines (instance visibility: private)
|
|
374
|
+
# @param [Parser::AST::Node] node
|
|
375
|
+
# @param [String] indent
|
|
376
|
+
# @param [Docscribe::Types::MethodSignature, nil] external_sig
|
|
377
|
+
# @param [Docscribe::Config] config
|
|
378
|
+
# @return [Array<String>, nil]
|
|
379
|
+
def build_params_lines(node, indent, external_sig:, config:)
|
|
380
|
+
fallback_type = config.fallback_type
|
|
381
|
+
treat_options_keyword_as_hash = config.treat_options_keyword_as_hash?
|
|
382
|
+
param_tag_style = config.param_tag_style
|
|
383
|
+
param_documentation = config.param_documentation
|
|
384
|
+
|
|
385
|
+
args =
|
|
386
|
+
case node.type
|
|
387
|
+
when :def then node.children[1]
|
|
388
|
+
when :defs then node.children[2]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
return nil unless args
|
|
392
|
+
|
|
393
|
+
params = []
|
|
394
|
+
|
|
395
|
+
(args.children || []).each do |a|
|
|
396
|
+
case a.type
|
|
397
|
+
when :arg
|
|
398
|
+
pname = a.children.first.to_s
|
|
399
|
+
ty = external_sig&.param_types&.[](pname) ||
|
|
400
|
+
Infer.infer_param_type(
|
|
401
|
+
pname,
|
|
402
|
+
nil,
|
|
403
|
+
fallback_type: fallback_type,
|
|
404
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
405
|
+
)
|
|
406
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
407
|
+
|
|
408
|
+
when :optarg
|
|
409
|
+
pname, default = *a
|
|
410
|
+
pname = pname.to_s
|
|
411
|
+
default_src = default&.loc&.expression&.source
|
|
412
|
+
ty = external_sig&.param_types&.[](pname) ||
|
|
413
|
+
Infer.infer_param_type(
|
|
414
|
+
pname,
|
|
415
|
+
default_src,
|
|
416
|
+
fallback_type: fallback_type,
|
|
417
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
418
|
+
)
|
|
419
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
420
|
+
|
|
421
|
+
hash_option_pairs(default).each do |pair|
|
|
422
|
+
key_node, value_node = pair.children
|
|
423
|
+
option_key = option_key_name(key_node)
|
|
424
|
+
option_type = Infer::Literals.type_from_literal(value_node, fallback_type: fallback_type)
|
|
425
|
+
option_default = node_default_literal(value_node)
|
|
426
|
+
|
|
427
|
+
line = "#{indent}# @option #{pname} [#{option_type}] :#{option_key}"
|
|
428
|
+
line += " (#{option_default})" if option_default
|
|
429
|
+
line += ' Option documentation.'
|
|
430
|
+
params << line
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
when :kwarg
|
|
434
|
+
pname = a.children.first.to_s
|
|
435
|
+
ty = external_sig&.param_types&.[](pname) ||
|
|
436
|
+
Infer.infer_param_type(
|
|
437
|
+
"#{pname}:",
|
|
438
|
+
nil,
|
|
439
|
+
fallback_type: fallback_type,
|
|
440
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
441
|
+
)
|
|
442
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
443
|
+
|
|
444
|
+
when :kwoptarg
|
|
445
|
+
pname, default = *a
|
|
446
|
+
pname = pname.to_s
|
|
447
|
+
default_src = default&.loc&.expression&.source
|
|
448
|
+
ty = external_sig&.param_types&.[](pname) ||
|
|
449
|
+
Infer.infer_param_type(
|
|
450
|
+
"#{pname}:",
|
|
451
|
+
default_src,
|
|
452
|
+
fallback_type: fallback_type,
|
|
453
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
454
|
+
)
|
|
455
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
456
|
+
|
|
457
|
+
when :restarg
|
|
458
|
+
pname = (a.children.first || 'args').to_s
|
|
459
|
+
ty =
|
|
460
|
+
if external_sig&.rest_positional&.element_type
|
|
461
|
+
"Array<#{external_sig.rest_positional.element_type}>"
|
|
462
|
+
else
|
|
463
|
+
Infer.infer_param_type(
|
|
464
|
+
"*#{pname}",
|
|
465
|
+
nil,
|
|
466
|
+
fallback_type: fallback_type,
|
|
467
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
471
|
+
|
|
472
|
+
when :kwrestarg
|
|
473
|
+
pname = (a.children.first || 'kwargs').to_s
|
|
474
|
+
ty = external_sig&.rest_keywords&.type ||
|
|
475
|
+
Infer.infer_param_type(
|
|
476
|
+
"**#{pname}",
|
|
477
|
+
nil,
|
|
478
|
+
fallback_type: fallback_type,
|
|
479
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
480
|
+
)
|
|
481
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
482
|
+
|
|
483
|
+
when :blockarg
|
|
484
|
+
pname = (a.children.first || 'block').to_s
|
|
485
|
+
ty = external_sig&.param_types&.[](pname) ||
|
|
486
|
+
Infer.infer_param_type(
|
|
487
|
+
"&#{pname}",
|
|
488
|
+
nil,
|
|
489
|
+
fallback_type: fallback_type,
|
|
490
|
+
treat_options_keyword_as_hash: treat_options_keyword_as_hash
|
|
491
|
+
)
|
|
492
|
+
params << format_param_tag(indent, pname, ty, param_documentation, style: param_tag_style)
|
|
493
|
+
|
|
494
|
+
when :forward_arg
|
|
495
|
+
# skip
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
params.empty? ? nil : params
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Method documentation.
|
|
503
|
+
#
|
|
504
|
+
# @note module_function: when included, also defines #format_param_tag (instance visibility: private)
|
|
505
|
+
# @param [Object] indent Param documentation.
|
|
506
|
+
# @param [Object] name Param documentation.
|
|
507
|
+
# @param [Object] type Param documentation.
|
|
508
|
+
# @param [Object] documentation Param documentation.
|
|
509
|
+
# @param [Object] style Param documentation.
|
|
510
|
+
# @return [Object]
|
|
511
|
+
def format_param_tag(indent, name, type, documentation, style:)
|
|
512
|
+
doc = documentation.to_s.strip
|
|
513
|
+
type = type.to_s
|
|
514
|
+
|
|
515
|
+
line = case style.to_s
|
|
516
|
+
when 'name_type'
|
|
517
|
+
"#{indent}# @param #{name} [#{type}]"
|
|
518
|
+
else
|
|
519
|
+
"#{indent}# @param [#{type}] #{name}"
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
doc.empty? ? line : "#{line} #{doc}"
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Method documentation.
|
|
526
|
+
#
|
|
527
|
+
# @note module_function: when included, also defines #hash_option_pairs (instance visibility: private)
|
|
528
|
+
# @param [Object] node Param documentation.
|
|
529
|
+
# @return [Object]
|
|
530
|
+
def hash_option_pairs(node)
|
|
531
|
+
return [] unless node&.type == :hash
|
|
532
|
+
|
|
533
|
+
node.children.select { |child| child.is_a?(Parser::AST::Node) && child.type == :pair }
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Method documentation.
|
|
537
|
+
#
|
|
538
|
+
# @note module_function: when included, also defines #option_key_name (instance visibility: private)
|
|
539
|
+
# @param [Object] key_node Param documentation.
|
|
540
|
+
# @return [Object]
|
|
541
|
+
def option_key_name(key_node)
|
|
542
|
+
case key_node&.type
|
|
543
|
+
when :sym, :str
|
|
544
|
+
key_node.children.first.to_s
|
|
545
|
+
else
|
|
546
|
+
key_node&.loc&.expression&.source.to_s.sub(/\A:/, '')
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Method documentation.
|
|
551
|
+
#
|
|
552
|
+
# @note module_function: when included, also defines #node_default_literal (instance visibility: private)
|
|
553
|
+
# @param [Object] node Param documentation.
|
|
554
|
+
# @return [Object]
|
|
555
|
+
def node_default_literal(node)
|
|
556
|
+
node&.loc&.expression&.source
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Method documentation.
|
|
560
|
+
#
|
|
561
|
+
# @note module_function: when included, also defines #extract_param_name_from_param_line (instance visibility: private)
|
|
562
|
+
# @param [Object] line Param documentation.
|
|
563
|
+
# @return [nil]
|
|
564
|
+
def extract_param_name_from_param_line(line)
|
|
565
|
+
return Regexp.last_match(1) if line =~ /@param\b\s+\[[^\]]+\]\s+(\S+)/
|
|
566
|
+
return Regexp.last_match(1) if line =~ /@param\b\s+(\S+)\s+\[[^\]]+\]/
|
|
567
|
+
|
|
568
|
+
nil
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Method documentation.
|
|
572
|
+
#
|
|
573
|
+
# @note module_function: when included, also defines #debug_warn (instance visibility: private)
|
|
574
|
+
# @param [Object] e Param documentation.
|
|
575
|
+
# @param [Object] insertion Param documentation.
|
|
576
|
+
# @param [Object] name Param documentation.
|
|
577
|
+
# @param [Object] phase Param documentation.
|
|
578
|
+
# @return [Object]
|
|
579
|
+
def debug_warn(e, insertion:, name:, phase:)
|
|
580
|
+
return unless debug?
|
|
581
|
+
|
|
582
|
+
node = insertion&.node
|
|
583
|
+
buf_name = node&.loc&.expression&.source_buffer&.name || '(unknown)'
|
|
584
|
+
line = node&.loc&.expression&.line
|
|
585
|
+
scope = insertion&.scope
|
|
586
|
+
method_symbol = scope == :class ? '.' : '#'
|
|
587
|
+
container = insertion&.container || 'Object'
|
|
588
|
+
|
|
589
|
+
where = +buf_name.to_s
|
|
590
|
+
where << ":#{line}" if line
|
|
591
|
+
where << " #{container}#{method_symbol}#{name}"
|
|
592
|
+
|
|
593
|
+
warn "Docscribe DEBUG: #{phase} failed at #{where}: #{e.class}: #{e.message}"
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Method documentation.
|
|
597
|
+
#
|
|
598
|
+
# @note module_function: when included, also defines #debug? (instance visibility: private)
|
|
599
|
+
# @return [Object]
|
|
600
|
+
def debug?
|
|
601
|
+
ENV['DOCSCRIBE_DEBUG'] == '1'
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|