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.
@@ -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.strict_mode?
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
- types = parse_type_annotation(type_str)
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
- types = parse_type_annotation(type_token) if (types.nil? || types.empty?) && type_token
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
- param_name.to_sym,
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
- types = parse_type_annotation(type_token)
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
- types = parse_type_annotation(type_token)
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.strict_mode?
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.strict_mode?
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
- parsed = parse_type_annotation(stripped)
657
- return false if parsed.empty?
936
+ normalized = normalize_type_token(stripped)
937
+ return false if normalized.empty?
658
938
 
659
- parsed.all? { |entry| inline_type_hint?(entry) }
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
- candidate = token.start_with?('@') ? token[1..] : token
666
- candidate =~ /\A[A-Z][A-Za-z0-9_:<>\[\]]*\z/
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.strict_mode?, @environment.allow_param_comments?]
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])
@@ -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
- warn "[WARN] Rubycli documentation mismatch: #{formatted_message}" if strict_mode?
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