rubycli 0.1.2 → 0.1.5
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/CHANGELOG.md +44 -0
- data/README.ja.md +82 -14
- data/README.md +82 -14
- data/lib/rubycli/argument_mode_controller.rb +69 -0
- data/lib/rubycli/argument_parser.rb +287 -102
- data/lib/rubycli/arguments/token_stream.rb +41 -0
- data/lib/rubycli/arguments/value_converter.rb +85 -0
- data/lib/rubycli/cli.rb +14 -7
- data/lib/rubycli/command_line.rb +58 -6
- data/lib/rubycli/constant_capture.rb +50 -0
- data/lib/rubycli/documentation/comment_extractor.rb +52 -0
- data/lib/rubycli/documentation/metadata_parser.rb +973 -0
- data/lib/rubycli/documentation_registry.rb +11 -853
- data/lib/rubycli/environment.rb +50 -8
- data/lib/rubycli/eval_coercer.rb +16 -1
- data/lib/rubycli/help_renderer.rb +64 -9
- data/lib/rubycli/types.rb +2 -2
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +265 -121
- metadata +8 -2
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'types'
|
|
2
4
|
require_relative 'type_utils'
|
|
5
|
+
require_relative 'documentation/comment_extractor'
|
|
6
|
+
require_relative 'documentation/metadata_parser'
|
|
3
7
|
|
|
4
8
|
module Rubycli
|
|
5
9
|
class DocumentationRegistry
|
|
6
|
-
include TypeUtils
|
|
7
|
-
|
|
8
10
|
def initialize(environment:)
|
|
9
11
|
@environment = environment
|
|
10
12
|
@metadata_cache = {}
|
|
11
|
-
@
|
|
13
|
+
@comment_extractor = Documentation::CommentExtractor.new
|
|
14
|
+
@parser = Documentation::MetadataParser.new(environment: environment)
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def metadata_for(method_obj)
|
|
@@ -17,11 +20,11 @@ module Rubycli
|
|
|
17
20
|
location = method_obj.source_location
|
|
18
21
|
return empty_metadata unless location
|
|
19
22
|
|
|
20
|
-
cache_key = [location[0], location[1], @environment.
|
|
23
|
+
cache_key = [location[0], location[1], @environment.doc_check_mode?, @environment.allow_param_comments?]
|
|
21
24
|
return deep_dup(@metadata_cache[cache_key]) if @metadata_cache.key?(cache_key)
|
|
22
25
|
|
|
23
|
-
comment_lines =
|
|
24
|
-
metadata =
|
|
26
|
+
comment_lines = @comment_extractor.extract(location[0], location[1])
|
|
27
|
+
metadata = @parser.parse(comment_lines, method_obj)
|
|
25
28
|
@metadata_cache[cache_key] = metadata
|
|
26
29
|
deep_dup(metadata)
|
|
27
30
|
rescue Errno::ENOENT
|
|
@@ -30,862 +33,17 @@ module Rubycli
|
|
|
30
33
|
|
|
31
34
|
def reset!
|
|
32
35
|
@metadata_cache.clear
|
|
33
|
-
@
|
|
36
|
+
@comment_extractor.reset!
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
private
|
|
37
40
|
|
|
38
41
|
def empty_metadata
|
|
39
|
-
|
|
40
|
-
options: [],
|
|
41
|
-
returns: [],
|
|
42
|
-
summary: nil,
|
|
43
|
-
summary_lines: [],
|
|
44
|
-
detail_lines: [],
|
|
45
|
-
positionals: [],
|
|
46
|
-
positionals_map: {}
|
|
47
|
-
}
|
|
42
|
+
@parser.empty_metadata
|
|
48
43
|
end
|
|
49
44
|
|
|
50
45
|
def deep_dup(metadata)
|
|
51
46
|
Marshal.load(Marshal.dump(metadata))
|
|
52
47
|
end
|
|
53
|
-
|
|
54
|
-
def extract_comment_block(file, line_number)
|
|
55
|
-
lines = (@file_cache[file] ||= File.readlines(file, chomp: true))
|
|
56
|
-
index = line_number - 2
|
|
57
|
-
block = []
|
|
58
|
-
|
|
59
|
-
while index >= 0
|
|
60
|
-
line = lines[index]
|
|
61
|
-
break unless comment_line?(line)
|
|
62
|
-
|
|
63
|
-
block << line
|
|
64
|
-
index -= 1
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
block.reverse.map { |line| strip_comment_prefix(line) }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def comment_line?(line)
|
|
71
|
-
return false unless line
|
|
72
|
-
|
|
73
|
-
stripped = line.lstrip
|
|
74
|
-
stripped.start_with?('#')
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def strip_comment_prefix(line)
|
|
78
|
-
line.lstrip.sub(/^#/, '').lstrip
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def parse_comment_metadata(comment_lines, method_obj)
|
|
82
|
-
metadata = empty_metadata
|
|
83
|
-
return metadata if comment_lines.empty?
|
|
84
|
-
|
|
85
|
-
summary_compact_lines = []
|
|
86
|
-
summary_display_lines = []
|
|
87
|
-
detail_lines = []
|
|
88
|
-
summary_phase = true
|
|
89
|
-
|
|
90
|
-
comment_lines.each do |content|
|
|
91
|
-
stripped = content.strip
|
|
92
|
-
if summary_phase && stripped.empty?
|
|
93
|
-
summary_display_lines << ""
|
|
94
|
-
next
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
if (option = parse_tagged_param_line(stripped, method_obj))
|
|
98
|
-
if option.is_a?(OptionDefinition)
|
|
99
|
-
if method_accepts_keyword?(method_obj, option.keyword)
|
|
100
|
-
metadata[:options].reject! { |existing| existing.keyword == option.keyword }
|
|
101
|
-
metadata[:options] << option
|
|
102
|
-
else
|
|
103
|
-
metadata[:positionals] << option_to_positional_definition(option)
|
|
104
|
-
end
|
|
105
|
-
elsif option.is_a?(PositionalDefinition)
|
|
106
|
-
metadata[:positionals] << option
|
|
107
|
-
end
|
|
108
|
-
summary_phase = false
|
|
109
|
-
next
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
if (return_meta = parse_return_metadata(stripped))
|
|
113
|
-
metadata[:returns] << return_meta
|
|
114
|
-
summary_phase = false
|
|
115
|
-
next
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
if (option = parse_tagless_option_line(stripped, method_obj))
|
|
119
|
-
metadata[:options].reject! { |existing| existing.keyword == option.keyword }
|
|
120
|
-
metadata[:options] << option
|
|
121
|
-
summary_phase = false
|
|
122
|
-
next
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
if (positional = parse_positional_line(stripped))
|
|
126
|
-
metadata[:positionals] << positional
|
|
127
|
-
summary_phase = false
|
|
128
|
-
next
|
|
129
|
-
end
|
|
130
|
-
if summary_phase
|
|
131
|
-
summary_display_lines << content.rstrip
|
|
132
|
-
summary_compact_lines << stripped unless stripped.empty?
|
|
133
|
-
else
|
|
134
|
-
detail_lines << content.rstrip
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
summary_text = summary_compact_lines.join(' ')
|
|
139
|
-
summary_text = nil if summary_text.empty?
|
|
140
|
-
metadata[:summary] = summary_text
|
|
141
|
-
metadata[:summary_lines] = trim_blank_edges(summary_display_lines)
|
|
142
|
-
metadata[:detail_lines] = trim_blank_edges(detail_lines)
|
|
143
|
-
|
|
144
|
-
defaults = extract_parameter_defaults(method_obj)
|
|
145
|
-
align_and_validate_parameter_docs(method_obj, metadata, defaults)
|
|
146
|
-
|
|
147
|
-
metadata
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def trim_blank_edges(lines)
|
|
151
|
-
return [] if lines.nil? || lines.empty?
|
|
152
|
-
|
|
153
|
-
first = lines.index { |line| line && !line.strip.empty? }
|
|
154
|
-
return [] unless first
|
|
155
|
-
|
|
156
|
-
last = lines.rindex { |line| line && !line.strip.empty? }
|
|
157
|
-
return [] unless last
|
|
158
|
-
|
|
159
|
-
lines[first..last]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def parse_tagged_param_line(line, method_obj)
|
|
163
|
-
return nil unless line.start_with?('@param')
|
|
164
|
-
|
|
165
|
-
source_file = nil
|
|
166
|
-
source_line = nil
|
|
167
|
-
if method_obj.respond_to?(:source_location)
|
|
168
|
-
source_file, source_line = method_obj.source_location
|
|
169
|
-
end
|
|
170
|
-
line_number = source_line ? [source_line - 1, 1].max : nil
|
|
171
|
-
|
|
172
|
-
unless @environment.allow_param_comments?
|
|
173
|
-
source_file, source_line = method_obj.source_location
|
|
174
|
-
@environment.handle_documentation_issue(
|
|
175
|
-
'@param notation is disabled. Enable it via ENV RUBYCLI_ALLOW_PARAM_COMMENT=ON.',
|
|
176
|
-
file: source_file,
|
|
177
|
-
line: line_number
|
|
178
|
-
)
|
|
179
|
-
return nil if @environment.strict_mode?
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
pattern = /\A@param\s+([a-zA-Z0-9_]+)(?:\s+\[([^\]]+)\])?(?:\s+\(([^)]+)\))?(?:\s+(.*))?\z/
|
|
183
|
-
match = pattern.match(line)
|
|
184
|
-
return nil unless match
|
|
185
|
-
|
|
186
|
-
param_name = match[1]
|
|
187
|
-
type_str = match[2]
|
|
188
|
-
option_tokens = combine_bracketed_tokens(match[3]&.split(/\s+/) || [])
|
|
189
|
-
description = match[4]&.strip
|
|
190
|
-
description = nil if description&.empty?
|
|
191
|
-
|
|
192
|
-
types = parse_type_annotation(type_str)
|
|
193
|
-
|
|
194
|
-
long_option = nil
|
|
195
|
-
short_option = nil
|
|
196
|
-
value_name = nil
|
|
197
|
-
type_token = nil
|
|
198
|
-
|
|
199
|
-
unless option_tokens.empty?
|
|
200
|
-
normalized = option_tokens.flat_map { |token| token.split('/') }
|
|
201
|
-
normalized.each do |token|
|
|
202
|
-
token_without_at = token.start_with?('@') ? token[1..] : token
|
|
203
|
-
if token.start_with?('--')
|
|
204
|
-
if (eq_index = token.index('='))
|
|
205
|
-
long_option = token[0...eq_index]
|
|
206
|
-
inline_value = token[(eq_index + 1)..]
|
|
207
|
-
if value_name.nil? && inline_value && !inline_value.strip.empty?
|
|
208
|
-
value_name = inline_value.strip
|
|
209
|
-
next
|
|
210
|
-
end
|
|
211
|
-
else
|
|
212
|
-
long_option = token
|
|
213
|
-
end
|
|
214
|
-
elsif token.start_with?('-')
|
|
215
|
-
if (eq_index = token.index('='))
|
|
216
|
-
short_option = token[0...eq_index]
|
|
217
|
-
inline_value = token[(eq_index + 1)..]
|
|
218
|
-
if value_name.nil? && inline_value && !inline_value.strip.empty?
|
|
219
|
-
value_name = inline_value.strip
|
|
220
|
-
next
|
|
221
|
-
end
|
|
222
|
-
else
|
|
223
|
-
short_option = token
|
|
224
|
-
end
|
|
225
|
-
elsif value_name.nil? && placeholder_token?(token_without_at)
|
|
226
|
-
value_name = token_without_at
|
|
227
|
-
elsif type_token.nil? && type_token_candidate?(token)
|
|
228
|
-
type_token = token
|
|
229
|
-
elsif value_name.nil?
|
|
230
|
-
value_name = token_without_at
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
long_option ||= "--#{param_name.tr('_', '-')}"
|
|
236
|
-
|
|
237
|
-
types = parse_type_annotation(type_token) if (types.nil? || types.empty?) && type_token
|
|
238
|
-
|
|
239
|
-
option_def = build_option_definition(
|
|
240
|
-
param_name.to_sym,
|
|
241
|
-
long_option,
|
|
242
|
-
short_option,
|
|
243
|
-
value_name,
|
|
244
|
-
types,
|
|
245
|
-
description,
|
|
246
|
-
inline_type_annotation: !type_token.nil?,
|
|
247
|
-
doc_format: :tagged_param
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
param_symbol = param_name.to_sym
|
|
251
|
-
role = parameter_role(method_obj, param_symbol)
|
|
252
|
-
|
|
253
|
-
if role == :positional
|
|
254
|
-
placeholder = option_def.value_name || default_placeholder_for(option_def.keyword)
|
|
255
|
-
return PositionalDefinition.new(
|
|
256
|
-
placeholder: placeholder,
|
|
257
|
-
label: placeholder,
|
|
258
|
-
types: option_def.types,
|
|
259
|
-
description: option_def.description,
|
|
260
|
-
param_name: param_symbol,
|
|
261
|
-
doc_format: option_def.doc_format
|
|
262
|
-
)
|
|
263
|
-
elsif role == :keyword
|
|
264
|
-
return option_def
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
unless method_accepts_keyword?(method_obj, param_symbol)
|
|
268
|
-
placeholder = option_def.value_name || default_placeholder_for(option_def.keyword)
|
|
269
|
-
return PositionalDefinition.new(
|
|
270
|
-
placeholder: placeholder,
|
|
271
|
-
label: placeholder,
|
|
272
|
-
types: option_def.types,
|
|
273
|
-
description: option_def.description,
|
|
274
|
-
param_name: param_symbol,
|
|
275
|
-
doc_format: option_def.doc_format
|
|
276
|
-
)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
option_def
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def parse_tagless_option_line(line, method_obj)
|
|
283
|
-
return nil unless line.start_with?('--') || line.start_with?('-')
|
|
284
|
-
|
|
285
|
-
raw_tokens = combine_bracketed_tokens(line.split(/\s+/))
|
|
286
|
-
tokens = raw_tokens.flat_map { |token|
|
|
287
|
-
if token.include?('/') && !token.start_with?('[')
|
|
288
|
-
token.split('/')
|
|
289
|
-
else
|
|
290
|
-
[token]
|
|
291
|
-
end
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
long_option = nil
|
|
295
|
-
short_option = nil
|
|
296
|
-
inline_value_from_long = nil
|
|
297
|
-
inline_value_from_short = nil
|
|
298
|
-
remaining = []
|
|
299
|
-
|
|
300
|
-
tokens.each do |token|
|
|
301
|
-
if long_option.nil? && token.start_with?('--')
|
|
302
|
-
if (eq_index = token.index('='))
|
|
303
|
-
long_option = token[0...eq_index]
|
|
304
|
-
inline_value_from_long = token[(eq_index + 1)..]
|
|
305
|
-
else
|
|
306
|
-
long_option = token
|
|
307
|
-
end
|
|
308
|
-
next
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
if short_option.nil? && token.start_with?('-') && !token.start_with?('--')
|
|
312
|
-
if (eq_index = token.index('='))
|
|
313
|
-
short_option = token[0...eq_index]
|
|
314
|
-
inline_value_from_short = token[(eq_index + 1)..]
|
|
315
|
-
else
|
|
316
|
-
short_option = token
|
|
317
|
-
end
|
|
318
|
-
next
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
remaining << token
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
return nil unless long_option
|
|
325
|
-
|
|
326
|
-
type_token = nil
|
|
327
|
-
value_name = [inline_value_from_long, inline_value_from_short].compact.map(&:strip).find { |val| !val.empty? }
|
|
328
|
-
description_tokens = []
|
|
329
|
-
|
|
330
|
-
remaining.each do |token|
|
|
331
|
-
token_without_at = token.start_with?('@') ? token[1..] : token
|
|
332
|
-
|
|
333
|
-
if value_name.nil? && placeholder_token?(token_without_at)
|
|
334
|
-
value_name = token_without_at
|
|
335
|
-
next
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
if type_token.nil? && type_token_candidate?(token)
|
|
339
|
-
type_token = token
|
|
340
|
-
next
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
description_tokens << token
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
description = description_tokens.join(' ').strip
|
|
347
|
-
description = nil if description.empty?
|
|
348
|
-
types = parse_type_annotation(type_token)
|
|
349
|
-
|
|
350
|
-
keyword = long_option.delete_prefix('--').tr('-', '_').to_sym
|
|
351
|
-
return nil unless method_accepts_keyword?(method_obj, keyword)
|
|
352
|
-
|
|
353
|
-
build_option_definition(
|
|
354
|
-
keyword,
|
|
355
|
-
long_option,
|
|
356
|
-
short_option,
|
|
357
|
-
value_name,
|
|
358
|
-
types,
|
|
359
|
-
description,
|
|
360
|
-
inline_type_annotation: !type_token.nil?,
|
|
361
|
-
doc_format: :rubycli
|
|
362
|
-
)
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def parse_positional_line(line)
|
|
366
|
-
return nil if line.start_with?('--') || line.start_with?('-')
|
|
367
|
-
|
|
368
|
-
tokens = combine_bracketed_tokens(line.split(/\s+/))
|
|
369
|
-
placeholder = tokens.shift
|
|
370
|
-
return nil unless placeholder
|
|
371
|
-
|
|
372
|
-
clean_placeholder = placeholder.delete('[]')
|
|
373
|
-
return nil unless placeholder_token?(clean_placeholder)
|
|
374
|
-
|
|
375
|
-
type_token = nil
|
|
376
|
-
if tokens.first && type_token_candidate?(tokens.first)
|
|
377
|
-
type_token = tokens.shift
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
description = tokens.join(' ').strip
|
|
381
|
-
description = nil if description.empty?
|
|
382
|
-
|
|
383
|
-
types = parse_type_annotation(type_token)
|
|
384
|
-
placeholder_info = analyze_placeholder(placeholder)
|
|
385
|
-
inferred_types = infer_types_from_placeholder(
|
|
386
|
-
normalize_type_list(types),
|
|
387
|
-
placeholder_info,
|
|
388
|
-
include_optional_boolean: false
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
label = clean_placeholder
|
|
392
|
-
|
|
393
|
-
inline_annotation = !type_token.nil?
|
|
394
|
-
inline_text = inline_annotation ? format_inline_type_label(inferred_types) : nil
|
|
395
|
-
|
|
396
|
-
PositionalDefinition.new(
|
|
397
|
-
placeholder: placeholder,
|
|
398
|
-
label: label.empty? ? placeholder : label,
|
|
399
|
-
types: inferred_types,
|
|
400
|
-
description: description,
|
|
401
|
-
inline_type_annotation: inline_annotation,
|
|
402
|
-
inline_type_text: inline_text,
|
|
403
|
-
doc_format: :rubycli
|
|
404
|
-
)
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def parse_return_metadata(line)
|
|
408
|
-
yard_match = /\A@return\s+\[([^\]]+)\](?:\s+(.*))?\z/.match(line)
|
|
409
|
-
if yard_match
|
|
410
|
-
types = parse_type_annotation(yard_match[1])
|
|
411
|
-
description = yard_match[2]&.strip
|
|
412
|
-
return ReturnDefinition.new(types: types, description: description)
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
shorthand_match = /\A=>\s+(\[[^\]]+\]|[^\s]+)(?:\s+(.*))?\z/.match(line)
|
|
416
|
-
if shorthand_match
|
|
417
|
-
types = parse_type_annotation(shorthand_match[1])
|
|
418
|
-
description = shorthand_match[2]&.strip
|
|
419
|
-
return ReturnDefinition.new(types: types, description: description)
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
if line.start_with?('return ')
|
|
423
|
-
stripped = line.sub(/\Areturn\s+/, '')
|
|
424
|
-
type_token, description = stripped.split(/\s+/, 2)
|
|
425
|
-
types = parse_type_annotation(type_token)
|
|
426
|
-
description = description&.strip
|
|
427
|
-
return ReturnDefinition.new(types: types, description: description)
|
|
428
|
-
end
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
def extract_parameter_defaults(method_obj)
|
|
432
|
-
location = method_obj.source_location
|
|
433
|
-
return {} unless location
|
|
434
|
-
|
|
435
|
-
file, line_no = location
|
|
436
|
-
return {} unless file && line_no
|
|
437
|
-
|
|
438
|
-
lines = File.readlines(file)
|
|
439
|
-
signature = String.new
|
|
440
|
-
index = line_no - 1
|
|
441
|
-
while index < lines.length
|
|
442
|
-
line = lines[index]
|
|
443
|
-
signature << line
|
|
444
|
-
break if balanced_signature?(signature)
|
|
445
|
-
index += 1
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
params_source = extract_params_from_signature(signature)
|
|
449
|
-
return {} unless params_source
|
|
450
|
-
|
|
451
|
-
split_parameters(params_source).each_with_object({}) do |param_token, memo|
|
|
452
|
-
case param_token
|
|
453
|
-
when /^\*\*/
|
|
454
|
-
next
|
|
455
|
-
when /^\*/
|
|
456
|
-
next
|
|
457
|
-
when /^&/
|
|
458
|
-
next
|
|
459
|
-
else
|
|
460
|
-
if (match = param_token.match(/\A([a-zA-Z0-9_]+)\s*=\s*(.+)\z/))
|
|
461
|
-
memo[match[1].to_sym] = match[2].strip
|
|
462
|
-
elsif (match = param_token.match(/\A([a-zA-Z0-9_]+):\s*(.+)\z/))
|
|
463
|
-
memo[match[1].to_sym] = match[2].strip
|
|
464
|
-
end
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
rescue Errno::ENOENT
|
|
468
|
-
{}
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
def balanced_signature?(signature)
|
|
472
|
-
def_index = signature.index(/\bdef\b/)
|
|
473
|
-
return false unless def_index
|
|
474
|
-
|
|
475
|
-
open_parens = signature.count('(')
|
|
476
|
-
close_parens = signature.count(')')
|
|
477
|
-
|
|
478
|
-
if open_parens.zero?
|
|
479
|
-
!signature.strip.end_with?(',')
|
|
480
|
-
else
|
|
481
|
-
open_parens == close_parens && signature.rindex(')') > signature.index('(')
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def extract_params_from_signature(signature)
|
|
486
|
-
return nil unless (def_match = signature.match(/\bdef\b\s+[^(\s]+\s*(\((.*)\))?/m))
|
|
487
|
-
if def_match[1]
|
|
488
|
-
inner = def_match[1][1..-2]
|
|
489
|
-
inner
|
|
490
|
-
else
|
|
491
|
-
signature_after_def = signature.sub(/.*\bdef\b\s+[^(\s]+\s*/m, '')
|
|
492
|
-
signature_after_def.split(/\n/).first&.strip
|
|
493
|
-
end
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
def split_parameters(param_string)
|
|
497
|
-
return [] unless param_string
|
|
498
|
-
|
|
499
|
-
tokens = []
|
|
500
|
-
current = String.new
|
|
501
|
-
depth = 0
|
|
502
|
-
param_string.each_char do |char|
|
|
503
|
-
case char
|
|
504
|
-
when '(', '[', '{'
|
|
505
|
-
depth += 1
|
|
506
|
-
when ')', ']', '}'
|
|
507
|
-
depth -= 1 if depth > 0
|
|
508
|
-
when ','
|
|
509
|
-
if depth.zero?
|
|
510
|
-
tokens << current.strip unless current.strip.empty?
|
|
511
|
-
current = String.new
|
|
512
|
-
next
|
|
513
|
-
end
|
|
514
|
-
end
|
|
515
|
-
current << char
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
tokens << current.strip unless current.strip.empty?
|
|
519
|
-
tokens
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
def align_and_validate_parameter_docs(method_obj, metadata, defaults)
|
|
523
|
-
positional_defs = metadata[:positionals].dup
|
|
524
|
-
positional_map = {}
|
|
525
|
-
existing_options = metadata[:options].dup
|
|
526
|
-
options_by_keyword = existing_options.each_with_object({}) { |opt, memo| memo[opt.keyword] = opt }
|
|
527
|
-
ordered_options = []
|
|
528
|
-
|
|
529
|
-
source_file = nil
|
|
530
|
-
source_line = nil
|
|
531
|
-
if method_obj.respond_to?(:source_location)
|
|
532
|
-
source_file, source_line = method_obj.source_location
|
|
533
|
-
end
|
|
534
|
-
line_for_comment = source_line ? [source_line - 1, 1].max : nil
|
|
535
|
-
|
|
536
|
-
method_obj.parameters.each do |type, name|
|
|
537
|
-
case type
|
|
538
|
-
when :req, :opt
|
|
539
|
-
doc = positional_defs.shift
|
|
540
|
-
if doc
|
|
541
|
-
doc.param_name = name
|
|
542
|
-
doc.default_value = defaults[name]
|
|
543
|
-
positional_map[name] = doc
|
|
544
|
-
else
|
|
545
|
-
@environment.handle_documentation_issue(
|
|
546
|
-
"Documentation is missing for positional argument '#{name}'",
|
|
547
|
-
file: source_file,
|
|
548
|
-
line: line_for_comment
|
|
549
|
-
)
|
|
550
|
-
unless @environment.strict_mode?
|
|
551
|
-
fallback = PositionalDefinition.new(
|
|
552
|
-
placeholder: name.to_s,
|
|
553
|
-
label: name.to_s.upcase,
|
|
554
|
-
types: [],
|
|
555
|
-
description: nil,
|
|
556
|
-
param_name: name,
|
|
557
|
-
default_value: defaults[name],
|
|
558
|
-
inline_type_annotation: false,
|
|
559
|
-
inline_type_text: nil
|
|
560
|
-
)
|
|
561
|
-
metadata[:positionals] << fallback
|
|
562
|
-
positional_map[name] = fallback
|
|
563
|
-
end
|
|
564
|
-
end
|
|
565
|
-
when :keyreq, :key
|
|
566
|
-
if (option = options_by_keyword[name])
|
|
567
|
-
ordered_options << option unless ordered_options.include?(option)
|
|
568
|
-
else
|
|
569
|
-
@environment.handle_documentation_issue(
|
|
570
|
-
"Documentation is missing for keyword argument ':#{name}'",
|
|
571
|
-
file: source_file,
|
|
572
|
-
line: line_for_comment
|
|
573
|
-
)
|
|
574
|
-
unless @environment.strict_mode?
|
|
575
|
-
fallback_option = build_auto_option_definition(name)
|
|
576
|
-
ordered_options << fallback_option if fallback_option
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
end
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
metadata[:options] = ordered_options + (existing_options - ordered_options)
|
|
583
|
-
|
|
584
|
-
unless positional_defs.empty?
|
|
585
|
-
extra = positional_defs.map(&:placeholder).join(', ')
|
|
586
|
-
@environment.handle_documentation_issue(
|
|
587
|
-
"Extra positional argument comments were found: #{extra}",
|
|
588
|
-
file: source_file,
|
|
589
|
-
line: line_for_comment
|
|
590
|
-
)
|
|
591
|
-
|
|
592
|
-
metadata[:positionals] -= positional_defs
|
|
593
|
-
|
|
594
|
-
positional_defs.each do |doc|
|
|
595
|
-
detail_line = detail_line_for_extra_positional(doc)
|
|
596
|
-
next unless detail_line
|
|
597
|
-
|
|
598
|
-
metadata[:detail_lines] ||= []
|
|
599
|
-
metadata[:detail_lines] << detail_line
|
|
600
|
-
end
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
metadata[:positionals_map] = positional_map
|
|
604
|
-
|
|
605
|
-
metadata[:options].each do |opt|
|
|
606
|
-
if defaults.key?(opt.keyword)
|
|
607
|
-
opt.default_value = defaults[opt.keyword]
|
|
608
|
-
if TypeUtils.boolean_string?(opt.default_value)
|
|
609
|
-
opt.boolean_flag = true
|
|
610
|
-
opt.requires_value = false
|
|
611
|
-
if opt.doc_format == :auto_generated
|
|
612
|
-
opt.value_name = nil
|
|
613
|
-
opt.types = ['Boolean']
|
|
614
|
-
end
|
|
615
|
-
end
|
|
616
|
-
end
|
|
617
|
-
end
|
|
618
|
-
end
|
|
619
|
-
|
|
620
|
-
def detail_line_for_extra_positional(doc)
|
|
621
|
-
return nil unless doc
|
|
622
|
-
|
|
623
|
-
parts = []
|
|
624
|
-
placeholder = doc.placeholder || doc.label
|
|
625
|
-
placeholder = placeholder.to_s.strip
|
|
626
|
-
parts << placeholder unless placeholder.empty?
|
|
627
|
-
|
|
628
|
-
type_text = doc.inline_type_text
|
|
629
|
-
if (!type_text || type_text.empty?) && doc.types && !doc.types.empty?
|
|
630
|
-
type_text = "[#{doc.types.join(', ')}]"
|
|
631
|
-
end
|
|
632
|
-
parts << type_text if type_text && !type_text.empty?
|
|
633
|
-
|
|
634
|
-
description = doc.description.to_s.strip
|
|
635
|
-
parts << description unless description.empty?
|
|
636
|
-
|
|
637
|
-
text = parts.join(' ').strip
|
|
638
|
-
text.empty? ? nil : text
|
|
639
|
-
end
|
|
640
|
-
|
|
641
|
-
INLINE_TYPE_HINTS = %w[
|
|
642
|
-
String
|
|
643
|
-
Integer
|
|
644
|
-
Float
|
|
645
|
-
Numeric
|
|
646
|
-
Boolean
|
|
647
|
-
TrueClass
|
|
648
|
-
FalseClass
|
|
649
|
-
Symbol
|
|
650
|
-
Array
|
|
651
|
-
Hash
|
|
652
|
-
JSON
|
|
653
|
-
Time
|
|
654
|
-
Date
|
|
655
|
-
DateTime
|
|
656
|
-
BigDecimal
|
|
657
|
-
File
|
|
658
|
-
Pathname
|
|
659
|
-
nil
|
|
660
|
-
].freeze
|
|
661
|
-
|
|
662
|
-
def parse_type_annotation(type_str)
|
|
663
|
-
return [] unless type_str
|
|
664
|
-
|
|
665
|
-
cleaned = type_str.strip
|
|
666
|
-
cleaned = cleaned.delete_prefix('@')
|
|
667
|
-
cleaned = cleaned[1..-2].strip if cleaned.start_with?('(') && cleaned.end_with?(')')
|
|
668
|
-
cleaned = cleaned[1..-2] if cleaned.start_with?('[') && cleaned.end_with?(']')
|
|
669
|
-
cleaned = cleaned.sub(/\Atype\s*:\s*/i, '')
|
|
670
|
-
cleaned.split(/[,|]/).map { |token| normalize_type_token(token) }.reject(&:empty?)
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
def placeholder_token?(token)
|
|
674
|
-
return false unless token
|
|
675
|
-
|
|
676
|
-
candidate = token.strip.delete_prefix('@')
|
|
677
|
-
return false if candidate.empty?
|
|
678
|
-
|
|
679
|
-
optional = candidate.start_with?('[') && candidate.end_with?(']')
|
|
680
|
-
candidate = candidate[1..-2].strip if optional
|
|
681
|
-
return false if candidate.empty?
|
|
682
|
-
|
|
683
|
-
candidate = candidate.gsub(/[,\|]/, '')
|
|
684
|
-
return false if candidate.empty?
|
|
685
|
-
|
|
686
|
-
ellipsis = candidate.end_with?('...')
|
|
687
|
-
candidate = candidate[0..-4] if ellipsis
|
|
688
|
-
candidate = candidate.strip
|
|
689
|
-
return false if candidate.empty?
|
|
690
|
-
|
|
691
|
-
if candidate.start_with?('<') && candidate.end_with?('>')
|
|
692
|
-
inner = candidate[1..-2]
|
|
693
|
-
inner.match?(/\A[0-9A-Za-z][0-9A-Za-z._-]*\z/)
|
|
694
|
-
else
|
|
695
|
-
cleaned = candidate.gsub(/[^A-Za-z0-9_]/, '')
|
|
696
|
-
return false if cleaned.empty?
|
|
697
|
-
|
|
698
|
-
cleaned == cleaned.upcase && cleaned.match?(/[A-Z]/)
|
|
699
|
-
end
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
def type_token_candidate?(token)
|
|
703
|
-
return false unless token
|
|
704
|
-
|
|
705
|
-
stripped = token.strip
|
|
706
|
-
return false if stripped.empty?
|
|
707
|
-
|
|
708
|
-
return true if stripped.start_with?('@')
|
|
709
|
-
|
|
710
|
-
parsed = parse_type_annotation(stripped)
|
|
711
|
-
return false if parsed.empty?
|
|
712
|
-
|
|
713
|
-
parsed.all? { |entry| inline_type_hint?(entry) }
|
|
714
|
-
end
|
|
715
|
-
|
|
716
|
-
def known_type_token?(token)
|
|
717
|
-
return false unless token
|
|
718
|
-
|
|
719
|
-
candidate = token.start_with?('@') ? token[1..] : token
|
|
720
|
-
candidate =~ /\A[A-Z][A-Za-z0-9_:<>\[\]]*\z/
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
def inline_type_hint?(token)
|
|
724
|
-
normalized = normalize_type_token(token)
|
|
725
|
-
return false if normalized.empty?
|
|
726
|
-
|
|
727
|
-
base = if normalized.include?('<') && normalized.end_with?('>')
|
|
728
|
-
normalized.split('<').first
|
|
729
|
-
elsif normalized.end_with?('[]')
|
|
730
|
-
normalized[0..-3]
|
|
731
|
-
else
|
|
732
|
-
normalized
|
|
733
|
-
end
|
|
734
|
-
|
|
735
|
-
INLINE_TYPE_HINTS.include?(base)
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
def parameter_role(method_obj, keyword)
|
|
739
|
-
return nil unless method_obj.respond_to?(:parameters)
|
|
740
|
-
|
|
741
|
-
symbol = keyword.to_sym
|
|
742
|
-
method_obj.parameters.each do |type, name|
|
|
743
|
-
next unless name == symbol
|
|
744
|
-
|
|
745
|
-
case type
|
|
746
|
-
when :req, :opt, :rest
|
|
747
|
-
return :positional
|
|
748
|
-
when :keyreq, :key
|
|
749
|
-
return :keyword
|
|
750
|
-
else
|
|
751
|
-
return nil
|
|
752
|
-
end
|
|
753
|
-
end
|
|
754
|
-
|
|
755
|
-
nil
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
def combine_bracketed_tokens(tokens)
|
|
759
|
-
combined = []
|
|
760
|
-
buffer = nil
|
|
761
|
-
closing = nil
|
|
762
|
-
|
|
763
|
-
tokens.each do |token|
|
|
764
|
-
next if token.nil?
|
|
765
|
-
|
|
766
|
-
if buffer
|
|
767
|
-
buffer << ' ' unless token.empty?
|
|
768
|
-
buffer << token
|
|
769
|
-
if closing && token.include?(closing)
|
|
770
|
-
combined << buffer
|
|
771
|
-
buffer = nil
|
|
772
|
-
closing = nil
|
|
773
|
-
end
|
|
774
|
-
elsif token.start_with?('[') && !token.include?(']')
|
|
775
|
-
buffer = token.dup
|
|
776
|
-
closing = ']'
|
|
777
|
-
elsif token.start_with?('(') && !token.include?(')')
|
|
778
|
-
buffer = token.dup
|
|
779
|
-
closing = ')'
|
|
780
|
-
else
|
|
781
|
-
combined << token
|
|
782
|
-
end
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
combined << buffer if buffer
|
|
786
|
-
combined
|
|
787
|
-
end
|
|
788
|
-
|
|
789
|
-
def format_inline_type_label(types)
|
|
790
|
-
return nil if types.nil? || types.empty?
|
|
791
|
-
|
|
792
|
-
unique_types = types.reject(&:empty?).uniq
|
|
793
|
-
return nil if unique_types.empty?
|
|
794
|
-
|
|
795
|
-
"[#{unique_types.join(', ')}]"
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
def method_accepts_keyword?(method_obj, keyword)
|
|
799
|
-
params = method_obj.parameters
|
|
800
|
-
keyword_names = params.select { |type, _| %i[key keyreq keyrest].include?(type) }.map { |_, name| name }
|
|
801
|
-
keyword_names.include?(keyword) || params.any? { |type, _| type == :keyrest }
|
|
802
|
-
end
|
|
803
|
-
|
|
804
|
-
def build_option_definition(
|
|
805
|
-
keyword,
|
|
806
|
-
long_option,
|
|
807
|
-
short_option,
|
|
808
|
-
value_name,
|
|
809
|
-
types,
|
|
810
|
-
description,
|
|
811
|
-
inline_type_annotation: false,
|
|
812
|
-
doc_format: nil
|
|
813
|
-
)
|
|
814
|
-
normalized_long = normalize_long_option(long_option)
|
|
815
|
-
normalized_short = normalize_short_option(short_option)
|
|
816
|
-
value_placeholder = value_name&.strip
|
|
817
|
-
value_placeholder = nil if value_placeholder&.empty?
|
|
818
|
-
description_text = description&.strip
|
|
819
|
-
description_text = nil if description_text&.empty?
|
|
820
|
-
|
|
821
|
-
placeholder_info = analyze_placeholder(value_placeholder)
|
|
822
|
-
normalized_types = normalize_type_list(types)
|
|
823
|
-
inferred_types = infer_types_from_placeholder(normalized_types, placeholder_info)
|
|
824
|
-
if inferred_types.empty? && value_placeholder.nil?
|
|
825
|
-
inferred_types = ['Boolean']
|
|
826
|
-
end
|
|
827
|
-
|
|
828
|
-
optional_value = placeholder_info[:optional]
|
|
829
|
-
boolean_flag = !optional_value && inferred_types.any? { |type| boolean_type?(type) }
|
|
830
|
-
requires_value = determine_requires_value(
|
|
831
|
-
value_placeholder: value_placeholder,
|
|
832
|
-
types: inferred_types,
|
|
833
|
-
boolean_flag: boolean_flag,
|
|
834
|
-
optional_value: optional_value
|
|
835
|
-
)
|
|
836
|
-
|
|
837
|
-
if value_placeholder.nil? && !boolean_flag && requires_value
|
|
838
|
-
value_placeholder = default_placeholder_for(keyword)
|
|
839
|
-
placeholder_info = analyze_placeholder(value_placeholder)
|
|
840
|
-
optional_value = placeholder_info[:optional]
|
|
841
|
-
end
|
|
842
|
-
|
|
843
|
-
inline_type_text = inline_type_annotation ? format_inline_type_label(inferred_types) : nil
|
|
844
|
-
|
|
845
|
-
OptionDefinition.new(
|
|
846
|
-
keyword: keyword,
|
|
847
|
-
long: normalized_long,
|
|
848
|
-
short: normalized_short,
|
|
849
|
-
value_name: value_placeholder,
|
|
850
|
-
types: inferred_types,
|
|
851
|
-
description: description_text,
|
|
852
|
-
requires_value: requires_value,
|
|
853
|
-
boolean_flag: boolean_flag,
|
|
854
|
-
optional_value: optional_value,
|
|
855
|
-
inline_type_annotation: inline_type_annotation,
|
|
856
|
-
inline_type_text: inline_type_text,
|
|
857
|
-
doc_format: doc_format
|
|
858
|
-
)
|
|
859
|
-
end
|
|
860
|
-
|
|
861
|
-
def build_auto_option_definition(keyword)
|
|
862
|
-
long_option = "--#{keyword.to_s.tr('_', '-')}"
|
|
863
|
-
placeholder = default_placeholder_for(keyword)
|
|
864
|
-
build_option_definition(
|
|
865
|
-
keyword,
|
|
866
|
-
long_option,
|
|
867
|
-
nil,
|
|
868
|
-
placeholder,
|
|
869
|
-
[],
|
|
870
|
-
nil,
|
|
871
|
-
inline_type_annotation: false,
|
|
872
|
-
doc_format: :auto_generated
|
|
873
|
-
)
|
|
874
|
-
end
|
|
875
|
-
|
|
876
|
-
def option_to_positional_definition(option)
|
|
877
|
-
placeholder = option.value_name || default_placeholder_for(option.keyword)
|
|
878
|
-
PositionalDefinition.new(
|
|
879
|
-
placeholder: placeholder,
|
|
880
|
-
label: placeholder,
|
|
881
|
-
types: option.types,
|
|
882
|
-
description: option.description,
|
|
883
|
-
param_name: option.keyword,
|
|
884
|
-
default_value: option.default_value,
|
|
885
|
-
inline_type_annotation: option.inline_type_annotation,
|
|
886
|
-
inline_type_text: option.inline_type_text,
|
|
887
|
-
doc_format: option.doc_format
|
|
888
|
-
)
|
|
889
|
-
end
|
|
890
48
|
end
|
|
891
49
|
end
|