rubycli 0.1.4 → 0.1.6
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 +24 -0
- data/README.ja.md +67 -12
- data/README.md +67 -12
- data/lib/rubycli/argument_parser.rb +255 -1
- data/lib/rubycli/arguments/value_converter.rb +11 -0
- data/lib/rubycli/cli.rb +5 -1
- data/lib/rubycli/command_line.rb +42 -6
- data/lib/rubycli/documentation/metadata_parser.rb +377 -31
- data/lib/rubycli/documentation_registry.rb +1 -1
- data/lib/rubycli/environment.rb +42 -7
- data/lib/rubycli/eval_coercer.rb +1 -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 +103 -32
- metadata +2 -2
|
@@ -55,7 +55,7 @@ module Rubycli
|
|
|
55
55
|
next
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
if (return_meta = parse_return_metadata(stripped))
|
|
58
|
+
if (return_meta = parse_return_metadata(stripped, method_obj))
|
|
59
59
|
metadata[:returns] << return_meta
|
|
60
60
|
summary_phase = false
|
|
61
61
|
next
|
|
@@ -68,7 +68,7 @@ module Rubycli
|
|
|
68
68
|
next
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
if (positional = parse_positional_line(stripped))
|
|
71
|
+
if (positional = parse_positional_line(stripped, method_obj))
|
|
72
72
|
metadata[:positionals] << positional
|
|
73
73
|
summary_phase = false
|
|
74
74
|
next
|
|
@@ -122,7 +122,7 @@ module Rubycli
|
|
|
122
122
|
file: source_file,
|
|
123
123
|
line: line_number
|
|
124
124
|
)
|
|
125
|
-
return nil if @environment.
|
|
125
|
+
return nil if @environment.doc_check_mode?
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
pattern = /\A@param\s+([a-zA-Z0-9_]+)(?:\s+\[([^\]]+)\])?(?:\s+\(([^)]+)\))?(?:\s+(.*))?\z/
|
|
@@ -130,12 +130,15 @@ module Rubycli
|
|
|
130
130
|
return nil unless match
|
|
131
131
|
|
|
132
132
|
param_name = match[1]
|
|
133
|
+
param_symbol = param_name.to_sym
|
|
133
134
|
type_str = match[2]
|
|
134
135
|
option_tokens = combine_bracketed_tokens(match[3]&.split(/\s+/) || [])
|
|
135
136
|
description = match[4]&.strip
|
|
136
137
|
description = nil if description&.empty?
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
raw_types = parse_type_annotation(type_str)
|
|
140
|
+
audit_type_annotation_tokens(raw_types, method_obj)
|
|
141
|
+
types, allowed_values = partition_type_tokens(raw_types)
|
|
139
142
|
|
|
140
143
|
long_option = nil
|
|
141
144
|
short_option = nil
|
|
@@ -179,23 +182,37 @@ module Rubycli
|
|
|
179
182
|
end
|
|
180
183
|
|
|
181
184
|
long_option ||= "--#{param_name.tr('_', '-')}"
|
|
185
|
+
role = parameter_role(method_obj, param_symbol)
|
|
186
|
+
if value_name.nil?
|
|
187
|
+
if role == :positional
|
|
188
|
+
value_name = default_placeholder_for(param_symbol)
|
|
189
|
+
elsif !types&.any? { |entry| boolean_type?(entry) }
|
|
190
|
+
# Most keywords expect a value; boolean flags should be documented with [Boolean].
|
|
191
|
+
value_name = default_placeholder_for(param_symbol)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
182
194
|
|
|
183
|
-
|
|
195
|
+
if (types.nil? || types.empty?) && type_token
|
|
196
|
+
inline_raw_types = parse_type_annotation(type_token)
|
|
197
|
+
audit_type_annotation_tokens(inline_raw_types, method_obj)
|
|
198
|
+
inline_types, inline_allowed = partition_type_tokens(inline_raw_types)
|
|
199
|
+
types = inline_types
|
|
200
|
+
allowed_values = merge_allowed_values(allowed_values, inline_allowed)
|
|
201
|
+
end
|
|
184
202
|
|
|
203
|
+
# TODO: Derive primitive types from Ruby default values when explicit hints are absent.
|
|
185
204
|
option_def = build_option_definition(
|
|
186
|
-
|
|
205
|
+
param_symbol,
|
|
187
206
|
long_option,
|
|
188
207
|
short_option,
|
|
189
208
|
value_name,
|
|
190
209
|
types,
|
|
191
210
|
description,
|
|
192
211
|
inline_type_annotation: !type_token.nil?,
|
|
193
|
-
doc_format: :tagged_param
|
|
212
|
+
doc_format: :tagged_param,
|
|
213
|
+
allowed_values: allowed_values
|
|
194
214
|
)
|
|
195
215
|
|
|
196
|
-
param_symbol = param_name.to_sym
|
|
197
|
-
role = parameter_role(method_obj, param_symbol)
|
|
198
|
-
|
|
199
216
|
if role == :positional
|
|
200
217
|
placeholder = option_def.value_name || default_placeholder_for(option_def.keyword)
|
|
201
218
|
return PositionalDefinition.new(
|
|
@@ -204,7 +221,8 @@ module Rubycli
|
|
|
204
221
|
types: option_def.types,
|
|
205
222
|
description: option_def.description,
|
|
206
223
|
param_name: param_symbol,
|
|
207
|
-
doc_format: option_def.doc_format
|
|
224
|
+
doc_format: option_def.doc_format,
|
|
225
|
+
allowed_values: option_def.allowed_values
|
|
208
226
|
)
|
|
209
227
|
elsif role == :keyword
|
|
210
228
|
return option_def
|
|
@@ -218,7 +236,8 @@ module Rubycli
|
|
|
218
236
|
types: option_def.types,
|
|
219
237
|
description: option_def.description,
|
|
220
238
|
param_name: param_symbol,
|
|
221
|
-
doc_format: option_def.doc_format
|
|
239
|
+
doc_format: option_def.doc_format,
|
|
240
|
+
allowed_values: option_def.allowed_values
|
|
222
241
|
)
|
|
223
242
|
end
|
|
224
243
|
|
|
@@ -291,7 +310,9 @@ module Rubycli
|
|
|
291
310
|
|
|
292
311
|
description = description_tokens.join(' ').strip
|
|
293
312
|
description = nil if description.empty?
|
|
294
|
-
|
|
313
|
+
raw_types = parse_type_annotation(type_token)
|
|
314
|
+
audit_type_annotation_tokens(raw_types, method_obj)
|
|
315
|
+
types, allowed_values = partition_type_tokens(raw_types)
|
|
295
316
|
|
|
296
317
|
keyword = long_option.delete_prefix('--').tr('-', '_').to_sym
|
|
297
318
|
return nil unless method_accepts_keyword?(method_obj, keyword)
|
|
@@ -304,11 +325,12 @@ module Rubycli
|
|
|
304
325
|
types,
|
|
305
326
|
description,
|
|
306
327
|
inline_type_annotation: !type_token.nil?,
|
|
307
|
-
doc_format: :rubycli
|
|
328
|
+
doc_format: :rubycli,
|
|
329
|
+
allowed_values: allowed_values
|
|
308
330
|
)
|
|
309
331
|
end
|
|
310
332
|
|
|
311
|
-
def parse_positional_line(line)
|
|
333
|
+
def parse_positional_line(line, method_obj)
|
|
312
334
|
return nil if line.start_with?('--') || line.start_with?('-')
|
|
313
335
|
|
|
314
336
|
tokens = combine_bracketed_tokens(line.split(/\s+/))
|
|
@@ -326,7 +348,9 @@ module Rubycli
|
|
|
326
348
|
description = tokens.join(' ').strip
|
|
327
349
|
description = nil if description.empty?
|
|
328
350
|
|
|
329
|
-
|
|
351
|
+
raw_types = parse_type_annotation(type_token)
|
|
352
|
+
audit_type_annotation_tokens(raw_types, method_obj)
|
|
353
|
+
types, allowed_values = partition_type_tokens(raw_types)
|
|
330
354
|
placeholder_info = analyze_placeholder(placeholder)
|
|
331
355
|
inferred_types = infer_types_from_placeholder(
|
|
332
356
|
normalize_type_list(types),
|
|
@@ -346,14 +370,16 @@ module Rubycli
|
|
|
346
370
|
description: description,
|
|
347
371
|
inline_type_annotation: inline_annotation,
|
|
348
372
|
inline_type_text: inline_text,
|
|
349
|
-
doc_format: :rubycli
|
|
373
|
+
doc_format: :rubycli,
|
|
374
|
+
allowed_values: allowed_values
|
|
350
375
|
)
|
|
351
376
|
end
|
|
352
377
|
|
|
353
|
-
def parse_return_metadata(line)
|
|
378
|
+
def parse_return_metadata(line, method_obj)
|
|
354
379
|
yard_match = /\A@return\s+\[([^\]]+)\](?:\s+(.*))?\z/.match(line)
|
|
355
380
|
if yard_match
|
|
356
381
|
types = parse_type_annotation(yard_match[1])
|
|
382
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
357
383
|
description = yard_match[2]&.strip
|
|
358
384
|
return ReturnDefinition.new(types: types, description: description)
|
|
359
385
|
end
|
|
@@ -361,6 +387,7 @@ module Rubycli
|
|
|
361
387
|
shorthand_match = /\A=>\s+(\[[^\]]+\]|[^\s]+)(?:\s+(.*))?\z/.match(line)
|
|
362
388
|
if shorthand_match
|
|
363
389
|
types = parse_type_annotation(shorthand_match[1])
|
|
390
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
364
391
|
description = shorthand_match[2]&.strip
|
|
365
392
|
return ReturnDefinition.new(types: types, description: description)
|
|
366
393
|
end
|
|
@@ -369,11 +396,74 @@ module Rubycli
|
|
|
369
396
|
stripped = line.sub(/\Areturn\s+/, '')
|
|
370
397
|
type_token, description = stripped.split(/\s+/, 2)
|
|
371
398
|
types = parse_type_annotation(type_token)
|
|
399
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
372
400
|
description = description&.strip
|
|
373
401
|
return ReturnDefinition.new(types: types, description: description)
|
|
374
402
|
end
|
|
375
403
|
end
|
|
376
404
|
|
|
405
|
+
def doc_issue_location(method_obj)
|
|
406
|
+
return [nil, nil] unless method_obj.respond_to?(:source_location)
|
|
407
|
+
|
|
408
|
+
source_file, source_line = method_obj.source_location
|
|
409
|
+
line_number = source_line ? [source_line - 1, 1].max : nil
|
|
410
|
+
[source_file, line_number]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def audit_type_annotation_tokens(tokens, method_obj)
|
|
414
|
+
return if tokens.nil? || tokens.empty?
|
|
415
|
+
return unless @environment.doc_check_mode?
|
|
416
|
+
|
|
417
|
+
source_file, line_number = doc_issue_location(method_obj)
|
|
418
|
+
literal_tokens_present = Array(tokens).any? do |token|
|
|
419
|
+
literal_entry = literal_entry_from_token(token)
|
|
420
|
+
literal_entry || literal_hint_token?(token)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
Array(tokens).each do |token|
|
|
424
|
+
audit_single_token(token, source_file, line_number, literal_tokens_present)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def audit_single_token(token, source_file, line_number, literal_context)
|
|
429
|
+
normalized = token.to_s.strip
|
|
430
|
+
return if normalized.empty?
|
|
431
|
+
|
|
432
|
+
if literal_hint_token?(normalized)
|
|
433
|
+
expand_annotation_token(normalized).each do |entry|
|
|
434
|
+
audit_literal_entry(entry, source_file, line_number)
|
|
435
|
+
end
|
|
436
|
+
return
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
literal_entry = literal_entry_from_token(normalized)
|
|
440
|
+
if literal_entry
|
|
441
|
+
return
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
if literal_context && literal_token_candidate?(normalized, include_uppercase: true) && !known_type_token?(normalized)
|
|
445
|
+
warn_unknown_allowed_value(normalized, source_file, line_number)
|
|
446
|
+
return
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if literal_token_candidate?(normalized, include_uppercase: false)
|
|
450
|
+
warn_unknown_allowed_value(normalized, source_file, line_number)
|
|
451
|
+
return
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
return if inline_type_hint?(normalized)
|
|
455
|
+
return if known_type_token?(normalized)
|
|
456
|
+
|
|
457
|
+
warn_unknown_type_token(normalized, source_file, line_number)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def audit_literal_entry(token, source_file, line_number)
|
|
461
|
+
literal_entry = literal_entry_from_token(token)
|
|
462
|
+
return if literal_entry
|
|
463
|
+
|
|
464
|
+
warn_unknown_allowed_value(token, source_file, line_number)
|
|
465
|
+
end
|
|
466
|
+
|
|
377
467
|
def extract_parameter_defaults(method_obj)
|
|
378
468
|
location = method_obj.source_location
|
|
379
469
|
return {} unless location
|
|
@@ -493,16 +583,18 @@ module Rubycli
|
|
|
493
583
|
file: source_file,
|
|
494
584
|
line: line_for_comment
|
|
495
585
|
)
|
|
496
|
-
unless @environment.
|
|
586
|
+
unless @environment.doc_check_mode?
|
|
497
587
|
fallback = PositionalDefinition.new(
|
|
498
588
|
placeholder: name.to_s,
|
|
499
589
|
label: name.to_s.upcase,
|
|
500
|
-
types: [],
|
|
590
|
+
types: ['String'],
|
|
501
591
|
description: nil,
|
|
502
592
|
param_name: name,
|
|
503
593
|
default_value: defaults[name],
|
|
504
594
|
inline_type_annotation: false,
|
|
505
|
-
inline_type_text: nil
|
|
595
|
+
inline_type_text: nil,
|
|
596
|
+
doc_format: :auto_generated,
|
|
597
|
+
allowed_values: []
|
|
506
598
|
)
|
|
507
599
|
metadata[:positionals] << fallback
|
|
508
600
|
positional_map[name] = fallback
|
|
@@ -517,7 +609,7 @@ module Rubycli
|
|
|
517
609
|
file: source_file,
|
|
518
610
|
line: line_for_comment
|
|
519
611
|
)
|
|
520
|
-
unless @environment.
|
|
612
|
+
unless @environment.doc_check_mode?
|
|
521
613
|
fallback_option = build_auto_option_definition(name)
|
|
522
614
|
ordered_options << fallback_option if fallback_option
|
|
523
615
|
end
|
|
@@ -558,6 +650,11 @@ module Rubycli
|
|
|
558
650
|
opt.value_name = nil
|
|
559
651
|
opt.types = ['Boolean']
|
|
560
652
|
end
|
|
653
|
+
elsif opt.boolean_flag
|
|
654
|
+
opt.boolean_flag = false
|
|
655
|
+
opt.requires_value = true
|
|
656
|
+
opt.value_name ||= default_placeholder_for(opt.keyword)
|
|
657
|
+
opt.types = ['String'] if opt.types.nil? || opt.types.empty?
|
|
561
658
|
end
|
|
562
659
|
end
|
|
563
660
|
end
|
|
@@ -616,6 +713,185 @@ module Rubycli
|
|
|
616
713
|
cleaned.split(/[,|]/).map { |token| normalize_type_token(token) }.reject(&:empty?)
|
|
617
714
|
end
|
|
618
715
|
|
|
716
|
+
def partition_type_tokens(tokens)
|
|
717
|
+
normalized = Array(tokens).dup
|
|
718
|
+
allowed = []
|
|
719
|
+
|
|
720
|
+
normalized.each do |token|
|
|
721
|
+
expand_annotation_token(token).each do |expanded|
|
|
722
|
+
literal_entry = literal_entry_from_token(expanded)
|
|
723
|
+
allowed << literal_entry if literal_entry
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
[normalized, allowed.compact.uniq]
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def merge_allowed_values(primary, additional)
|
|
731
|
+
return Array(additional) if primary.nil? || primary.empty?
|
|
732
|
+
return Array(primary) if additional.nil? || additional.empty?
|
|
733
|
+
|
|
734
|
+
(primary + additional).uniq
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def expand_annotation_token(token)
|
|
738
|
+
return [] unless token
|
|
739
|
+
|
|
740
|
+
stripped = token.strip
|
|
741
|
+
return [] if stripped.empty?
|
|
742
|
+
|
|
743
|
+
if stripped.start_with?('%i[') && stripped.end_with?(']')
|
|
744
|
+
inner = stripped[3..-2]
|
|
745
|
+
inner.split(/\s+/).map { |entry| ":#{entry}" }
|
|
746
|
+
elsif stripped.start_with?('%I[') && stripped.end_with?(']')
|
|
747
|
+
inner = stripped[3..-2]
|
|
748
|
+
inner.split(/\s+/).map { |entry| ":#{entry}" }
|
|
749
|
+
elsif stripped.start_with?('%w[') && stripped.end_with?(']')
|
|
750
|
+
inner = stripped[3..-2]
|
|
751
|
+
inner.split(/\s+/)
|
|
752
|
+
elsif stripped.start_with?('%W[') && stripped.end_with?(']')
|
|
753
|
+
inner = stripped[3..-2]
|
|
754
|
+
inner.split(/\s+/)
|
|
755
|
+
else
|
|
756
|
+
[stripped]
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def literal_entry_from_token(token)
|
|
761
|
+
return nil unless token
|
|
762
|
+
|
|
763
|
+
stripped = token.strip
|
|
764
|
+
return nil if stripped.empty?
|
|
765
|
+
stripped = stripped[1..] if stripped.start_with?('[') && !stripped.end_with?(']')
|
|
766
|
+
if stripped.end_with?(']') && !stripped.include?('[')
|
|
767
|
+
stripped = stripped[0...-1]
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
lowered = stripped.downcase
|
|
771
|
+
return { kind: :literal, value: nil } if %w[nil null ~].include?(lowered)
|
|
772
|
+
return { kind: :literal, value: true } if lowered == 'true'
|
|
773
|
+
return { kind: :literal, value: false } if lowered == 'false'
|
|
774
|
+
|
|
775
|
+
if stripped.start_with?(':')
|
|
776
|
+
sym_name = stripped[1..]
|
|
777
|
+
return nil if sym_name.nil? || sym_name.empty?
|
|
778
|
+
|
|
779
|
+
return { kind: :literal, value: sym_name.to_sym }
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
if stripped.start_with?('"') && stripped.end_with?('"') && stripped.length >= 2
|
|
783
|
+
return { kind: :literal, value: stripped[1..-2] }
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
if stripped.start_with?("'") && stripped.end_with?("'") && stripped.length >= 2
|
|
787
|
+
return { kind: :literal, value: stripped[1..-2] }
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
if stripped.match?(/\A-?\d+\z/)
|
|
791
|
+
return { kind: :literal, value: Integer(stripped) }
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
if stripped.match?(/\A-?\d+\.\d+\z/)
|
|
795
|
+
return { kind: :literal, value: Float(stripped) }
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
if stripped.match?(/\A[a-z0-9._-]+\z/)
|
|
799
|
+
return { kind: :literal, value: stripped }
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
nil
|
|
803
|
+
rescue ArgumentError
|
|
804
|
+
nil
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def literal_token_candidate?(token, include_uppercase: false)
|
|
808
|
+
return false unless token
|
|
809
|
+
|
|
810
|
+
stripped = token.strip
|
|
811
|
+
return false if stripped.empty?
|
|
812
|
+
|
|
813
|
+
lowered = stripped.downcase
|
|
814
|
+
return true if %w[true false nil null ~].include?(lowered)
|
|
815
|
+
return true if stripped.start_with?(':', '"', "'")
|
|
816
|
+
return true if stripped.match?(/\A-?\d/)
|
|
817
|
+
|
|
818
|
+
pattern = include_uppercase ? /\A[a-z0-9._-]+\z/i : /\A[a-z0-9._-]+\z/
|
|
819
|
+
stripped.match?(pattern)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def warn_unknown_type_token(token, source_file, line_number)
|
|
823
|
+
return if token.nil? || token.empty?
|
|
824
|
+
|
|
825
|
+
suggestions = type_token_suggestions(token)
|
|
826
|
+
message = "Unknown type token '#{token}'"
|
|
827
|
+
if suggestions.any?
|
|
828
|
+
hint = suggestions.first(2).join(' or ')
|
|
829
|
+
message = "#{message} (did you mean #{hint}?)"
|
|
830
|
+
end
|
|
831
|
+
@environment.handle_documentation_issue(message, file: source_file, line: line_number)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def warn_unknown_allowed_value(token, source_file, line_number)
|
|
835
|
+
return if token.nil? || token.empty?
|
|
836
|
+
|
|
837
|
+
suggestions = allowed_value_suggestions(token)
|
|
838
|
+
message = "Unknown allowed value token '#{token}'"
|
|
839
|
+
if suggestions.any?
|
|
840
|
+
hint = suggestions.first(2).join(' or ')
|
|
841
|
+
message = "#{message} (did you mean #{hint}?)"
|
|
842
|
+
end
|
|
843
|
+
@environment.handle_documentation_issue(message, file: source_file, line: line_number)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def type_token_suggestions(token)
|
|
847
|
+
dictionary = type_dictionary
|
|
848
|
+
return [] if dictionary.empty?
|
|
849
|
+
|
|
850
|
+
require 'did_you_mean'
|
|
851
|
+
checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
|
|
852
|
+
checker.correct(token.to_s).take(3)
|
|
853
|
+
rescue LoadError, NameError
|
|
854
|
+
[]
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def allowed_value_suggestions(token)
|
|
858
|
+
stripped = token.to_s.strip
|
|
859
|
+
return [] if stripped.empty?
|
|
860
|
+
|
|
861
|
+
candidates = []
|
|
862
|
+
if stripped.match?(/\A[a-z0-9._-]+\z/i)
|
|
863
|
+
candidates << ":#{stripped.downcase}"
|
|
864
|
+
candidates << stripped.downcase.inspect
|
|
865
|
+
end
|
|
866
|
+
return [] if candidates.empty?
|
|
867
|
+
|
|
868
|
+
require 'did_you_mean'
|
|
869
|
+
checker = DidYouMean::SpellChecker.new(dictionary: candidates)
|
|
870
|
+
checker.correct(stripped).take(2)
|
|
871
|
+
rescue LoadError, NameError
|
|
872
|
+
candidates.first(1)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def type_dictionary
|
|
876
|
+
builtins = (INLINE_TYPE_HINTS + %w[NilClass Fixnum Decimal Struct]).uniq
|
|
877
|
+
constant_names = []
|
|
878
|
+
begin
|
|
879
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
880
|
+
name = mod.name
|
|
881
|
+
next unless name && !name.empty?
|
|
882
|
+
|
|
883
|
+
constant_names << name
|
|
884
|
+
parts = name.split('::')
|
|
885
|
+
constant_names << parts.last if parts.size > 1
|
|
886
|
+
end
|
|
887
|
+
rescue StandardError
|
|
888
|
+
constant_names = Object.constants.map(&:to_s)
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
(builtins + constant_names).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
|
|
619
895
|
def placeholder_token?(token)
|
|
620
896
|
return false unless token
|
|
621
897
|
|
|
@@ -652,18 +928,30 @@ module Rubycli
|
|
|
652
928
|
return false if stripped.empty?
|
|
653
929
|
|
|
654
930
|
return true if stripped.start_with?('@')
|
|
931
|
+
return true if stripped.start_with?('%')
|
|
932
|
+
return true if stripped.include?('::')
|
|
933
|
+
return true if stripped.start_with?('(') && stripped.end_with?(')')
|
|
934
|
+
return true if stripped.include?('[') && stripped.include?(']')
|
|
655
935
|
|
|
656
|
-
|
|
657
|
-
return false if
|
|
936
|
+
normalized = normalize_type_token(stripped)
|
|
937
|
+
return false if normalized.empty?
|
|
658
938
|
|
|
659
|
-
|
|
939
|
+
INLINE_TYPE_HINTS.include?(normalized)
|
|
660
940
|
end
|
|
661
941
|
|
|
662
942
|
def known_type_token?(token)
|
|
663
943
|
return false unless token
|
|
664
944
|
|
|
665
|
-
|
|
666
|
-
|
|
945
|
+
normalized = normalize_type_token(token)
|
|
946
|
+
return false if normalized.empty?
|
|
947
|
+
|
|
948
|
+
return true if primitive_type_token?(normalized)
|
|
949
|
+
|
|
950
|
+
if (inner = array_inner_type_token(normalized))
|
|
951
|
+
return known_type_token?(inner)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
!safe_constant_lookup(normalized).nil?
|
|
667
955
|
end
|
|
668
956
|
|
|
669
957
|
def inline_type_hint?(token)
|
|
@@ -681,6 +969,53 @@ module Rubycli
|
|
|
681
969
|
INLINE_TYPE_HINTS.include?(base)
|
|
682
970
|
end
|
|
683
971
|
|
|
972
|
+
def primitive_type_token?(token)
|
|
973
|
+
return false if token.nil? || token.empty?
|
|
974
|
+
|
|
975
|
+
base = token.to_s
|
|
976
|
+
INLINE_TYPE_HINTS.include?(base) || %w[NilClass Fixnum Decimal Struct].include?(base)
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def array_inner_type_token(token)
|
|
980
|
+
return nil unless token
|
|
981
|
+
|
|
982
|
+
stripped = token.to_s.strip
|
|
983
|
+
if stripped.end_with?('[]')
|
|
984
|
+
stripped[0..-3]
|
|
985
|
+
elsif stripped.start_with?('Array<') && stripped.end_with?('>')
|
|
986
|
+
stripped[6..-2].strip
|
|
987
|
+
else
|
|
988
|
+
nil
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
def safe_constant_lookup(name)
|
|
993
|
+
parts = name.to_s.split('::').reject(&:empty?)
|
|
994
|
+
return nil if parts.empty?
|
|
995
|
+
|
|
996
|
+
context = Object
|
|
997
|
+
parts.each do |const_name|
|
|
998
|
+
return nil unless context.const_defined?(const_name, false)
|
|
999
|
+
|
|
1000
|
+
context = context.const_get(const_name)
|
|
1001
|
+
end
|
|
1002
|
+
context
|
|
1003
|
+
rescue NameError
|
|
1004
|
+
nil
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def literal_hint_token?(token)
|
|
1008
|
+
return false unless token
|
|
1009
|
+
|
|
1010
|
+
stripped = token.to_s.strip
|
|
1011
|
+
return false if stripped.empty?
|
|
1012
|
+
|
|
1013
|
+
stripped.start_with?('%i[') ||
|
|
1014
|
+
stripped.start_with?('%I[') ||
|
|
1015
|
+
stripped.start_with?('%w[') ||
|
|
1016
|
+
stripped.start_with?('%W[')
|
|
1017
|
+
end
|
|
1018
|
+
|
|
684
1019
|
def parameter_role(method_obj, keyword)
|
|
685
1020
|
return nil unless method_obj.respond_to?(:parameters)
|
|
686
1021
|
|
|
@@ -723,6 +1058,9 @@ module Rubycli
|
|
|
723
1058
|
elsif token.start_with?('(') && !token.include?(')')
|
|
724
1059
|
buffer = token.dup
|
|
725
1060
|
closing = ')'
|
|
1061
|
+
elsif token.start_with?('%') && token.include?('[') && !token.include?(']')
|
|
1062
|
+
buffer = token.dup
|
|
1063
|
+
closing = ']'
|
|
726
1064
|
else
|
|
727
1065
|
combined << token
|
|
728
1066
|
end
|
|
@@ -755,7 +1093,8 @@ module Rubycli
|
|
|
755
1093
|
types,
|
|
756
1094
|
description,
|
|
757
1095
|
inline_type_annotation: false,
|
|
758
|
-
doc_format: nil
|
|
1096
|
+
doc_format: nil,
|
|
1097
|
+
allowed_values: nil
|
|
759
1098
|
)
|
|
760
1099
|
normalized_long = normalize_long_option(long_option)
|
|
761
1100
|
normalized_short = normalize_short_option(short_option)
|
|
@@ -800,10 +1139,15 @@ module Rubycli
|
|
|
800
1139
|
optional_value: optional_value,
|
|
801
1140
|
inline_type_annotation: inline_type_annotation,
|
|
802
1141
|
inline_type_text: inline_type_text,
|
|
803
|
-
doc_format: doc_format
|
|
1142
|
+
doc_format: doc_format,
|
|
1143
|
+
allowed_values: normalize_allowed_values(allowed_values)
|
|
804
1144
|
)
|
|
805
1145
|
end
|
|
806
1146
|
|
|
1147
|
+
def normalize_allowed_values(values)
|
|
1148
|
+
Array(values).compact.uniq
|
|
1149
|
+
end
|
|
1150
|
+
|
|
807
1151
|
def build_auto_option_definition(keyword)
|
|
808
1152
|
long_option = "--#{keyword.to_s.tr('_', '-')}"
|
|
809
1153
|
placeholder = default_placeholder_for(keyword)
|
|
@@ -815,7 +1159,8 @@ module Rubycli
|
|
|
815
1159
|
[],
|
|
816
1160
|
nil,
|
|
817
1161
|
inline_type_annotation: false,
|
|
818
|
-
doc_format: :auto_generated
|
|
1162
|
+
doc_format: :auto_generated,
|
|
1163
|
+
allowed_values: []
|
|
819
1164
|
)
|
|
820
1165
|
end
|
|
821
1166
|
|
|
@@ -830,7 +1175,8 @@ module Rubycli
|
|
|
830
1175
|
default_value: option.default_value,
|
|
831
1176
|
inline_type_annotation: option.inline_type_annotation,
|
|
832
1177
|
inline_type_text: option.inline_type_text,
|
|
833
|
-
doc_format: option.doc_format
|
|
1178
|
+
doc_format: option.doc_format,
|
|
1179
|
+
allowed_values: option.allowed_values
|
|
834
1180
|
)
|
|
835
1181
|
end
|
|
836
1182
|
end
|
|
@@ -20,7 +20,7 @@ module Rubycli
|
|
|
20
20
|
location = method_obj.source_location
|
|
21
21
|
return empty_metadata unless location
|
|
22
22
|
|
|
23
|
-
cache_key = [location[0], location[1], @environment.
|
|
23
|
+
cache_key = [location[0], location[1], @environment.doc_check_mode?, @environment.allow_param_comments?]
|
|
24
24
|
return deep_dup(@metadata_cache[cache_key]) if @metadata_cache.key?(cache_key)
|
|
25
25
|
|
|
26
26
|
comment_lines = @comment_extractor.extract(location[0], location[1])
|
data/lib/rubycli/environment.rb
CHANGED
|
@@ -6,6 +6,9 @@ module Rubycli
|
|
|
6
6
|
@argv = argv
|
|
7
7
|
@debug = env['RUBYCLI_DEBUG'] == 'true'
|
|
8
8
|
@print_result = env['RUBYCLI_PRINT_RESULT'] == 'true'
|
|
9
|
+
@doc_check_mode = false
|
|
10
|
+
@strict_input = false
|
|
11
|
+
@documentation_issues = []
|
|
9
12
|
scrub_argv_flags!
|
|
10
13
|
end
|
|
11
14
|
|
|
@@ -17,16 +20,39 @@ module Rubycli
|
|
|
17
20
|
@print_result
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
def strict_mode?
|
|
21
|
-
value = fetch_env_value('RUBYCLI_STRICT', 'OFF')
|
|
22
|
-
!%w[off 0 false].include?(value.downcase)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
23
|
def allow_param_comments?
|
|
26
24
|
value = fetch_env_value('RUBYCLI_ALLOW_PARAM_COMMENT', 'ON')
|
|
27
25
|
%w[on 1 true].include?(value)
|
|
28
26
|
end
|
|
29
27
|
|
|
28
|
+
def enable_doc_check!
|
|
29
|
+
@doc_check_mode = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def doc_check_mode?
|
|
33
|
+
@doc_check_mode
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def disable_doc_check!
|
|
37
|
+
@doc_check_mode = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def enable_strict_input!
|
|
41
|
+
@strict_input = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def strict_input?
|
|
45
|
+
@strict_input
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def documentation_issues
|
|
49
|
+
@documentation_issues.dup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def clear_documentation_issues!
|
|
53
|
+
@documentation_issues.clear
|
|
54
|
+
end
|
|
55
|
+
|
|
30
56
|
def constant_resolution_mode
|
|
31
57
|
value = fetch_env_value('RUBYCLI_AUTO_TARGET', 'strict')
|
|
32
58
|
return :auto if %w[auto on true yes 1].include?(value)
|
|
@@ -49,7 +75,17 @@ module Rubycli
|
|
|
49
75
|
message
|
|
50
76
|
end
|
|
51
77
|
|
|
52
|
-
|
|
78
|
+
entry = { message: formatted_message, location: location }
|
|
79
|
+
@documentation_issues << entry
|
|
80
|
+
warn "[WARN] Rubycli documentation mismatch: #{formatted_message}" if doc_check_mode?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def handle_input_violation(message)
|
|
84
|
+
if strict_input?
|
|
85
|
+
raise Rubycli::ArgumentError, message
|
|
86
|
+
else
|
|
87
|
+
warn "[WARN] #{message} (use --strict to abort on invalid input)"
|
|
88
|
+
end
|
|
53
89
|
end
|
|
54
90
|
|
|
55
91
|
def enable_print_result!
|
|
@@ -65,7 +101,6 @@ module Rubycli
|
|
|
65
101
|
def scrub_argv_flags!
|
|
66
102
|
return unless @argv
|
|
67
103
|
|
|
68
|
-
remove_all_flags!(@argv, '--debug') { @debug = true }
|
|
69
104
|
remove_all_flags!(@argv, '--print-result') { @print_result = true }
|
|
70
105
|
end
|
|
71
106
|
|