Module: Docscribe::Validator::GenericCompatibility

Defined in:
lib/docscribe/validator/generic_compatibility.rb

Overview

Service object answering generic_compatible? without hardcoding alias names.

Dynamically decides if two type strings are compatible via generics/aliases. Uses hash dispatch to avoid long if/else chains and to allow easy extension.

Constant Summary collapse

CHECKS =

Check registry: name => checker method symbol

{
  generic_base: :generic_base_compatible?,
  short_name: :short_name_compatible?,
  alias_hash: :alias_hash_compatible?,
  tuple_array: :tuple_array_compatible?,
  optional_nil: :optional_nil_compatible?,
  union_containment: :union_containment?,
  optional_suffix: :optional_suffix_compatible?,
  generic_inner_alias: :generic_inner_alias_compatible?,
  bare_alias: :bare_alias_compatible?,
  union_optional: :union_vs_optional_compatible?,
  object_compatible: :object_compatible?,
  fallback_union: :fallback_union_check?
}.freeze

Class Method Summary collapse

Class Method Details

.alias_hash_compatible?(yard_type, expected_type) ⇒ Boolean

Whether alias (lowercase after ::) vs Hash/Array/Range is compatible.

Checks if one side is a namespaced alias starting with lowercase and the other is Hash, Array, or Range.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if alias vs Hash/Array/Range pair detected



248
249
250
251
252
253
254
# File 'lib/docscribe/validator/generic_compatibility.rb', line 248

def alias_hash_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  [[norm_yard, norm_expected], [norm_expected, norm_yard]].any? do |alias_type, hash_type|
    alias_hash_pair?(alias_type, hash_type)
  end
end

.alias_hash_pair?(alias_type, hash_type) ⇒ Boolean

Parameters:

  • alias_type (String)

    potential alias side

  • hash_type (String)

    potential Hash/Array side

Returns:

  • (Boolean)


259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/docscribe/validator/generic_compatibility.rb', line 259

def alias_hash_pair?(alias_type, hash_type)
  base = base_name(hash_type)
  return false unless %w[Hash Array Range].include?(base) ||
                      hash_type.start_with?('Hash') ||
                      hash_type.start_with?('Array') ||
                      hash_type == 'Range'

  short_alias = alias_type.split('::').last.to_s
  return false if %w[node Node].include?(short_alias)

  short_alias =~ /\A[a-z]/ && alias_type.include?('::')
end

.alias_token?(token) ⇒ Boolean

Whether token looks like an alias (dynamic, not hardcoded Elem/U).

Any token that is not a known Ruby/YARD primitive is considered alias. E.g., Elem, U, my_alias, Foo::Bar, change vs String, Integer, Hash.

Parameters:

  • token (String)

    single type token (trimmed inner part, may include ?)

Returns:

  • (Boolean)

    true if token is alias (unknown primitive or namespaced/lowercase)



424
425
426
427
428
429
430
431
# File 'lib/docscribe/validator/generic_compatibility.rb', line 424

def alias_token?(token) # rubocop:disable SortedMethodsByCall/Waterfall
  base = token.split('<').first.split('[').first.strip.delete_suffix('?').strip
  return false if Docscribe::Types::Primitive.primitive?(base)
  return true if base =~ /\A[a-z]/ || base.include?('::')
  return true if base =~ /\A[A-Z]\z/

  !!(base =~ /\A[A-Z][A-Za-z0-9_]*\z/)
end

.bare_alias?(normalized) ⇒ Boolean

Whether normalized string is a bare alias token (no generics, no union).

Parameters:

  • normalized (String)

    normalized type string

Returns:

  • (Boolean)

    true if bare alias



469
470
471
472
473
# File 'lib/docscribe/validator/generic_compatibility.rb', line 469

def bare_alias?(normalized)
  return false if normalized.include?('<') || normalized.include?('[') || normalized.include?(',')

  alias_token?(normalized.strip)
end

.bare_alias_compatible?(yard_type, expected_type) ⇒ Boolean

Whether one side is a bare alias (e.g., V, U, T, Elem) vs concrete type.

Bare alias means single token without generic brackets or commas that satisfies #alias_token?. E.g., "V" vs "Array" => true via alias. Handles single capital letter A-Z explicitly for RBS type params.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if either side is bare alias



442
443
444
445
446
447
448
# File 'lib/docscribe/validator/generic_compatibility.rb', line 442

def bare_alias_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  return false if norm_yard.empty? || norm_expected.empty?

  bare_single_cap?(norm_yard) || bare_single_cap?(norm_expected)
end

.bare_single_cap?(normalized) ⇒ Boolean

Whether normalized string is a bare single-capital alias (V, U, T, K, Elem? no, single A-Z only).

Single capital letters are RBS type parameters (generic placeholders) that should be compatible with concrete types like Array or String. Multi-letter aliases like Config are not considered bare aliases (handled via other checks).

Parameters:

  • normalized (String)

    normalized type string

Returns:

  • (Boolean)

    true if bare single capital



458
459
460
461
462
463
# File 'lib/docscribe/validator/generic_compatibility.rb', line 458

def bare_single_cap?(normalized)
  return false if normalized.include?('<') || normalized.include?('[') || normalized.include?(',')

  stripped = normalized.strip.delete_suffix('?').strip
  stripped =~ /\A[A-Z]\z/ && alias_token?(stripped)
end

.base_name(type_str) ⇒ String

Base name before generic or paren.

Strips "<", "[", "(" suffixes. E.g., "Hash<Symbol,String>" => "Hash".

Parameters:

  • type_str (String)

    raw type string, may be nil

Returns:

  • (String)

    base type name without generic arguments



278
279
280
# File 'lib/docscribe/validator/generic_compatibility.rb', line 278

def base_name(type_str)
  normalize(type_str).split('<').first.split('[').first.split('(').first.strip
end

.canonical_without_nil(canonical) ⇒ Array<String>

Parameters:

  • canonical (Array<String>)

Returns:

  • (Array<String>)


555
556
557
# File 'lib/docscribe/validator/generic_compatibility.rb', line 555

def canonical_without_nil(canonical)
  canonical.reject { |p| p == 'nil' }
end

.canonicalize_parts(parts) ⇒ Array<String>

Parameters:

  • parts (Array<String>)

Returns:

  • (Array<String>)


592
593
594
# File 'lib/docscribe/validator/generic_compatibility.rb', line 592

def canonicalize_parts(parts)
  expand_optional_parts(parts).map { |part| normalize(part) }.reject(&:empty?).uniq.sort
end

.comma_nil_pair?(norm_yard, norm_expected) ⇒ Boolean

Whether one side is "nil" and the other contains ", nil" union.

E.g., "nil" vs "String, nil" is considered compatible.

Parameters:

  • norm_yard (String)

    normalized YARD type

  • norm_expected (String)

    normalized expected type

Returns:

  • (Boolean)

    true if nil vs comma-nil union pair



329
330
331
# File 'lib/docscribe/validator/generic_compatibility.rb', line 329

def comma_nil_pair?(norm_yard, norm_expected)
  (norm_yard.include?(', nil') && norm_expected == 'nil') || (norm_expected.include?(', nil') && norm_yard == 'nil')
end

.compatible?(yard_type, expected_type, fallback_type: 'Object', method_name: nil) ⇒ Boolean

Whether two type strings are generic compatible (any checker true).

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

  • fallback_type (String) (defaults to: 'Object')

    fallback type for union checks (default "Object")

  • method_name (String, Symbol, nil) (defaults to: nil)

    method name for void compatibility (e.g., "initialize")

Returns:

  • (Boolean)

    true if any compatibility checker matches



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/docscribe/validator/generic_compatibility.rb', line 37

def compatible?(yard_type, expected_type, fallback_type: 'Object', method_name: nil)
  return true if void_compatible?(yard_type, expected_type, fallback_type, method_name: method_name)

  return true if union_parts_compatible?(yard_type, expected_type, fallback_type, method_name)

  CHECKS.any? do |name, checker|
    if name == :fallback_union
      send(checker, yard_type, expected_type, fallback_type)
    else
      send(checker, yard_type, expected_type)
    end
  end
end

.empty_str?(str) ⇒ Boolean

Parameters:

  • str (String, nil)

Returns:

  • (Boolean)


574
575
576
# File 'lib/docscribe/validator/generic_compatibility.rb', line 574

def empty_str?(str)
  str.to_s.strip.empty?
end

.expand_optional_parts(parts) ⇒ Array<String>

Parameters:

  • parts (Array<String>)

Returns:

  • (Array<String>)


605
606
607
608
609
610
611
612
613
614
# File 'lib/docscribe/validator/generic_compatibility.rb', line 605

def expand_optional_parts(parts)
  parts.flat_map do |part|
    stripped = part.strip
    if stripped.end_with?('?')
      [normalize(stripped.delete_suffix('?').strip), 'nil']
    else
      [normalize(stripped)]
    end
  end
end

.extract_inner(normalized) ⇒ String?

Extracts inner generic arguments from normalized Array/Hash type.

E.g., "Array" => "String", "Hash<Symbol, String>" => "Symbol, String".

Parameters:

  • normalized (String)

    normalized generic type string

Returns:

  • (String, nil)

    inner content or nil if not generic



405
406
407
# File 'lib/docscribe/validator/generic_compatibility.rb', line 405

def extract_inner(normalized)
  normalized[/\A(?:Array|Hash)[<\[](.*)[>\]]\z/, 1]
end

.fallback_union?(type_str, fallback) ⇒ Boolean

Whether a type string contains only the fallback type (comma-separated, ignoring trailing ?).

Parameters:

  • type_str (String, nil)

    type string to test, may be comma-separated union

  • fallback (String)

    fallback type name (e.g., "Object")

Returns:

  • (Boolean)

    true if every comma-separated part equals the normalized fallback



164
165
166
167
168
169
170
# File 'lib/docscribe/validator/generic_compatibility.rb', line 164

def fallback_union?(type_str, fallback)
  return false if type_str.nil? || type_str.strip.empty?

  fallback_norm = normalize(fallback)
  parts = type_str.to_s.split(',').map { |part| normalize(part.strip.delete_suffix('?').strip) }
  parts.all? { |part| part == fallback_norm || part.empty? }
end

.fallback_union_check?(yard_type, expected_type, fallback_type) ⇒ Boolean

Whether either type is a fallback-only union for the given fallback type.

Delegates to #fallback_union? for both yard_type and expected_type.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

  • fallback_type (String)

    fallback type to match (e.g., "Object")

Returns:

  • (Boolean)

    true if either type contains only fallback parts



155
156
157
# File 'lib/docscribe/validator/generic_compatibility.rb', line 155

def fallback_union_check?(yard_type, expected_type, fallback_type)
  fallback_union?(yard_type, fallback_type) || fallback_union?(expected_type, fallback_type)
end

.generic_base_compatible?(yard_type, expected_type) ⇒ Boolean

Whether generic base matches: Hash vs Hash<Symbol,String> or Array vs Array.

True when normalized types equal or one is bare base of the other's generic.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if generic base matches (bare vs generic or identical)



179
180
181
182
183
184
185
186
187
# File 'lib/docscribe/validator/generic_compatibility.rb', line 179

def generic_base_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  return true if norm_yard == norm_expected

  yard_generic = norm_expected !~ /[<\[]/ && (norm_yard.start_with?("#{norm_expected}<") || norm_yard.start_with?("#{norm_expected}["))
  expected_generic = norm_yard !~ /[<\[]/ && (norm_expected.start_with?("#{norm_yard}<") || norm_expected.start_with?("#{norm_yard}["))
  yard_generic || expected_generic
end

.generic_inner_alias_compatible?(yard_type, expected_type) ⇒ Boolean

Whether Array/Hash generic inners contain alias tokens.

True when both are Array/Hash generics and either inner contains an alias (lowercase or "::"). E.g., "Array<my_alias>" vs "Array".

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if generic inners alias-compatible



378
379
380
381
382
383
384
385
386
387
388
# File 'lib/docscribe/validator/generic_compatibility.rb', line 378

def generic_inner_alias_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  return false unless generic_pair?(norm_yard, norm_expected)

  inner_yard = extract_inner(norm_yard)
  inner_expected = extract_inner(norm_expected)
  return false unless inner_yard && inner_expected

  inner_has_alias?(inner_yard) || inner_has_alias?(inner_expected)
end

.generic_pair?(norm_yard, norm_expected) ⇒ Boolean

Whether both normalized types are Array or Hash generics.

Parameters:

  • norm_yard (String)

    normalized YARD type

  • norm_expected (String)

    normalized expected type

Returns:

  • (Boolean)

    true if both match Array/Hash generic pattern



395
396
397
# File 'lib/docscribe/validator/generic_compatibility.rb', line 395

def generic_pair?(norm_yard, norm_expected)
  norm_yard =~ /\A(?:Array|Hash)[<\[]/ && norm_expected =~ /\A(?:Array|Hash)[<\[]/
end

.generic_string?(normalized) ⇒ Boolean

Whether a normalized type string contains generic brackets.

Parameters:

  • normalized (String)

    normalized type string (after #normalize)

Returns:

  • (Boolean)

    true if string includes "<" or "[" indicating generic



220
221
222
# File 'lib/docscribe/validator/generic_compatibility.rb', line 220

def generic_string?(normalized)
  normalized.include?('<') || normalized.include?('[')
end

.handle_split_char(chr, state) ⇒ void

This method returns an undefined value.

Parameters:

  • chr (String)
  • state (Hash<Symbol, Object>)


632
633
634
635
636
637
638
639
640
641
# File 'lib/docscribe/validator/generic_compatibility.rb', line 632

def handle_split_char(chr, state)
  case chr
  when '<', '>', '[', ']', '(', ')'
    update_split_depth(chr, state)
  when ','
    handle_split_comma(state)
  else
    state[:cur] << chr
  end
end

.handle_split_comma(state) ⇒ void

This method returns an undefined value.

Parameters:

  • state (Hash<Symbol, Object>)


655
656
657
658
659
660
661
662
# File 'lib/docscribe/validator/generic_compatibility.rb', line 655

def handle_split_comma(state)
  if state[:da].zero? && state[:db].zero? && state[:dp].zero?
    state[:parts] << state[:cur]
    state[:cur] = +''
  else
    state[:cur] << ','
  end
end

.inner_has_alias?(inner) ⇒ Boolean

Whether any comma-separated part of generic inner is an alias token.

Parameters:

  • inner (String)

    inner generic string (comma-separated)

Returns:

  • (Boolean)

    true if any part satisfies #alias_token?



413
414
415
# File 'lib/docscribe/validator/generic_compatibility.rb', line 413

def inner_has_alias?(inner)
  inner.split(',').any? { |part| alias_token?(part.strip) }
end

.node_nil_pair?(norm_yard, norm_expected) ⇒ Boolean

Whether Parser::AST::Node vs nil is considered compatible.

Special-case for AST nodes where nil represents absent node.

Parameters:

  • norm_yard (String)

    normalized YARD type

  • norm_expected (String)

    normalized expected type

Returns:

  • (Boolean)

    true if Parser::AST::Node vs nil pair



340
341
342
# File 'lib/docscribe/validator/generic_compatibility.rb', line 340

def node_nil_pair?(norm_yard, norm_expected)
  (norm_yard == 'Parser::AST::Node' && norm_expected == 'nil') || (norm_expected == 'Parser::AST::Node' && norm_yard == 'nil')
end

.normalize(type_str) ⇒ String

Normalizes type string for comparison.

Strips, squeezes spaces, converts "["/"]" to "<"/">", replaces "untyped"/"FALLBACK_TYPE" with "Object".

Parameters:

  • type_str (String, nil)

    raw type string, may be nil

Returns:

  • (String)

    normalized type string



690
691
692
693
694
# File 'lib/docscribe/validator/generic_compatibility.rb', line 690

def normalize(type_str)
  s = type_str.to_s
  s = s.sub(/#.*\z/m, '').strip unless s.lstrip.start_with?('#')
  s.strip.squeeze(' ').gsub('[', '<').gsub(']', '>').gsub(/\buntyped\b/, 'Object').gsub(/\bFALLBACK_TYPE\b/, 'Object')
end

.normalized_union_str(str) ⇒ String

Parameters:

  • str (String, nil)

Returns:

  • (String)


580
581
582
# File 'lib/docscribe/validator/generic_compatibility.rb', line 580

def normalized_union_str(str)
  str.to_s.gsub('|', ',').strip
end

.object_compatible?(yard_type, expected_type) ⇒ Boolean

Whether Object supertype compatibility holds: e.g., String vs Object, String, nil vs Object, nil.

If expected is Object (or Object, nil, Object? etc) and yard is a concrete type (String, Array, etc) with same nil presence, then yard is considered compatible with expected since Object is supertype of all. Handles receiver_or_and_type: String, nil vs Object, nil dynamically.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if Object supertype compatibility holds



533
534
535
536
537
538
539
540
541
542
543
# File 'lib/docscribe/validator/generic_compatibility.rb', line 533

def object_compatible?(yard_type, expected_type)
  yard_canonical = optional_canonical_parts(yard_type)
  exp_canonical = optional_canonical_parts(expected_type)
  return false if yard_canonical.empty? || exp_canonical.empty?

  yard_without_nil = canonical_without_nil(yard_canonical)
  exp_without_nil = canonical_without_nil(exp_canonical)
  return false unless object_supertype_pair?(yard_without_nil, exp_without_nil)

  yard_canonical.include?('nil') == exp_canonical.include?('nil')
end

.object_supertype_pair?(yard_without_nil, exp_without_nil) ⇒ Boolean

Parameters:

  • yard_without_nil (Array<String>)
  • exp_without_nil (Array<String>)

Returns:

  • (Boolean)


548
549
550
551
# File 'lib/docscribe/validator/generic_compatibility.rb', line 548

def object_supertype_pair?(yard_without_nil, exp_without_nil)
  (exp_without_nil == ['Object'] && yard_without_nil != ['Object'] && !yard_without_nil.empty?) ||
    (yard_without_nil == ['Object'] && exp_without_nil != ['Object'] && !exp_without_nil.empty?)
end

.optional_canonical_parts(str) ⇒ Array<String>

Canonical optional parts for T? / T, nil / T|nil / T forms.

Parameters:

  • str (String, nil)

    raw type string

Returns:

  • (Array<String>)

    sorted unique normalized parts



563
564
565
566
567
568
569
570
# File 'lib/docscribe/validator/generic_compatibility.rb', line 563

def optional_canonical_parts(str)
  return [] if str.nil? || empty_str?(str)

  s = normalized_union_str(str)
  return single_optional_parts(s) if single_optional_form?(s)

  canonicalize_parts(split_top_level_commas_local(s))
end

.optional_forms_equal?(first, second) ⇒ Boolean

Whether optional forms T?, T, nil, T|nil are equivalent (same canonical parts).

Canonicalizes T? => [T, nil], T, nil / T|nil => [T, nil], T => [T]. Generic-aware split keeps Hash<String, Integer>, nil intact.

Parameters:

  • first (String, nil)

    first type string

  • second (String, nil)

    second type string

Returns:

  • (Boolean)

    true if canonical optional parts equal and non-empty



517
518
519
520
521
# File 'lib/docscribe/validator/generic_compatibility.rb', line 517

def optional_forms_equal?(first, second)
  pa = optional_canonical_parts(first)
  pb = optional_canonical_parts(second)
  !pa.empty? && pa == pb
end

.optional_nil_compatible?(yard_type, expected_type) ⇒ Boolean

Whether optional vs nil pair is compatible.

Delegates to #question_nil_pair?, #comma_nil_pair?, and #node_nil_pair?.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if any nil-optional pairing matches



302
303
304
305
306
307
308
309
# File 'lib/docscribe/validator/generic_compatibility.rb', line 302

def optional_nil_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  return true if question_nil_pair?(norm_yard, norm_expected)
  return true if comma_nil_pair?(norm_yard, norm_expected)

  node_nil_pair?(norm_yard, norm_expected)
end

.optional_suffix_compatible?(yard_type, expected_type) ⇒ Boolean

Whether types match after stripping trailing "?".

E.g., "String" vs "String?" considered compatible.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if types equal ignoring optional "?" suffix



366
367
368
# File 'lib/docscribe/validator/generic_compatibility.rb', line 366

def optional_suffix_compatible?(yard_type, expected_type)
  normalize(yard_type).delete_suffix('?') == normalize(expected_type).delete_suffix('?')
end

.part_compatible_with_single?(single, part, fallback_type) ⇒ Boolean

Parameters:

  • single (String)
  • part (String)
  • fallback_type (String)

Returns:

  • (Boolean)


91
92
93
94
95
96
97
98
99
# File 'lib/docscribe/validator/generic_compatibility.rb', line 91

def part_compatible_with_single?(single, part, fallback_type)
  CHECKS.any? do |name, checker|
    next if %i[union_containment fallback_union].include?(name)

    compatible = name == :fallback_union ? send(checker, single, part, fallback_type) : send(checker, single, part)
    return true if compatible || single == part
  end
  false
end

.pipe_aware_suffix_vs_union_first?(yard_type, expected_type) ⇒ Boolean

Pipe-aware variant of #suffix_vs_union_first? normalizing | to ,.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if suffix vs union matches after pipe normalization



498
499
500
501
502
503
504
505
506
507
# File 'lib/docscribe/validator/generic_compatibility.rb', line 498

def pipe_aware_suffix_vs_union_first?(yard_type, expected_type)
  # Normalize both by converting pipe to comma before comparison
  y = yard_type.to_s.gsub('|', ',')
  e = expected_type.to_s.gsub('|', ',')
  left_match = normalize(y).delete_suffix('?') ==
               normalize(split_top_level_commas_local(e).first || '')
  right_match = normalize(e).delete_suffix('?') ==
                normalize(split_top_level_commas_local(y).first || '')
  left_match || right_match
end

.question_nil_pair?(norm_yard, norm_expected) ⇒ Boolean

Whether one side is "nil" and the other uses trailing "?" optional syntax.

E.g., "nil" vs "String?" is considered compatible.

Parameters:

  • norm_yard (String)

    normalized YARD type

  • norm_expected (String)

    normalized expected type

Returns:

  • (Boolean)

    true if nil vs "?" optional pair



318
319
320
# File 'lib/docscribe/validator/generic_compatibility.rb', line 318

def question_nil_pair?(norm_yard, norm_expected)
  (norm_yard == 'nil' && norm_expected.end_with?('?')) || (norm_expected == 'nil' && norm_yard.end_with?('?'))
end

.question_vs_comma_nil?(norm_yard, norm_expected) ⇒ Boolean

Method documentation.

Parameters:

  • norm_yard (String)

    Param documentation.

  • norm_expected (String)

    Param documentation.

Returns:

  • (Boolean)


669
670
671
672
# File 'lib/docscribe/validator/generic_compatibility.rb', line 669

def question_vs_comma_nil?(norm_yard, norm_expected)
  norm_yard.delete(' ') == "#{norm_expected.delete(' ').delete_suffix('?')},nil" ||
    norm_expected.delete(' ') == "#{norm_yard.delete(' ').delete_suffix('?')},nil"
end

.short_compatible?(short_yard, short_expected, norm_yard, norm_expected) ⇒ Boolean

Whether short names are compatible given full normalized forms.

Allows Docscribe::Config vs Config and cross-checks short vs full forms.

Parameters:

  • short_yard (String)

    short name derived from YARD type

  • short_expected (String)

    short name derived from expected type

  • norm_yard (String)

    full normalized YARD type

  • norm_expected (String)

    full normalized expected type

Returns:

  • (Boolean)

    true if short names align via namespace elision



233
234
235
236
237
238
239
# File 'lib/docscribe/validator/generic_compatibility.rb', line 233

def short_compatible?(short_yard, short_expected, norm_yard, norm_expected)
  return true if short_yard == short_expected && short_yard != norm_yard && short_expected != norm_expected
  return true if short_yard == norm_expected
  return true if short_expected == norm_yard

  false
end

.short_name(type_str) ⇒ String

Short name without namespace or generic args.

Strips module prefix and generic suffix. E.g., "Docscribe::Config" => "Config".

Parameters:

  • type_str (String)

    raw type string, may be nil

Returns:

  • (String)

    short name (last namespace segment without < or [)



212
213
214
# File 'lib/docscribe/validator/generic_compatibility.rb', line 212

def short_name(type_str)
  normalize(type_str).split('::').last.to_s.split('<').first.split('[').first.strip
end

.short_name_compatible?(yard_type, expected_type) ⇒ Boolean

Whether short names equal: Docscribe::Config vs Config, Parser::Source::Range vs Range.

Ignores generic brackets and checks namespace-elided compatibility via #short_compatible?.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if short names match with namespace variation



196
197
198
199
200
201
202
203
204
# File 'lib/docscribe/validator/generic_compatibility.rb', line 196

def short_name_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  return false if generic_string?(norm_yard) || generic_string?(norm_expected)

  short_yard = short_name(norm_yard)
  short_expected = short_name(norm_expected)
  short_compatible?(short_yard, short_expected, norm_yard, norm_expected)
end

.single_optional_form?(str) ⇒ Boolean

Parameters:

  • str (String)

Returns:

  • (Boolean)


586
587
588
# File 'lib/docscribe/validator/generic_compatibility.rb', line 586

def single_optional_form?(str)
  str.end_with?('?') && !str.include?(',')
end

.single_optional_parts(str) ⇒ Array<String>

Parameters:

  • str (String)

Returns:

  • (Array<String>)


598
599
600
601
# File 'lib/docscribe/validator/generic_compatibility.rb', line 598

def single_optional_parts(str)
  base = normalize(str.delete_suffix('?').strip)
  [base, 'nil'].sort
end

.single_vs_union?(single_parts, union_parts, fallback_type) ⇒ Boolean

Parameters:

  • single_parts (Array<String>)
  • union_parts (Array<String>)
  • fallback_type (String)

Returns:

  • (Boolean)


80
81
82
83
84
85
# File 'lib/docscribe/validator/generic_compatibility.rb', line 80

def single_vs_union?(single_parts, union_parts, fallback_type)
  return false unless single_parts.size == 1 && union_parts.size > 1

  single = single_parts.first
  union_parts.any? { |part| part_compatible_with_single?(single, part, fallback_type) }
end

.split_top_level_commas_local(str) ⇒ Array<String>

Split by top-level commas outside < > [ ] ( ) (generic-aware).

Mirrors Docscribe::Infer::Returns.split_top_level_commas but local to avoid cross-dep.

Parameters:

  • str (String)

    type string to split

Returns:

  • (Array<String>)

    parts split on top-level commas



622
623
624
625
626
627
# File 'lib/docscribe/validator/generic_compatibility.rb', line 622

def split_top_level_commas_local(str)
  state = { parts: [], cur: +'', da: 0, db: 0, dp: 0 } #: Hash[Symbol, untyped]
  str.each_char { |chr| handle_split_char(chr, state) }
  state[:parts] << state[:cur] unless state[:cur].empty?
  state[:parts]
end

.suffix_vs_union_first?(yard_type, expected_type) ⇒ Boolean

Method documentation.

Parameters:

  • yard_type (String, nil)

    Param documentation.

  • expected_type (String, nil)

    Param documentation.

Returns:

  • (Boolean)


679
680
681
682
# File 'lib/docscribe/validator/generic_compatibility.rb', line 679

def suffix_vs_union_first?(yard_type, expected_type)
  normalize(yard_type).delete_suffix('?') == normalize(expected_type).split(',').first&.strip ||
    normalize(expected_type).delete_suffix('?') == normalize(yard_type).split(',').first&.strip
end

.tuple_array_compatible?(yard_type, expected_type) ⇒ Boolean

Whether tuple "(String, Integer)" vs Array is compatible.

True when one side is bare "Array" and the other is parenthesized tuple.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if tuple vs Array pair



289
290
291
292
293
# File 'lib/docscribe/validator/generic_compatibility.rb', line 289

def tuple_array_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type)
  norm_expected = normalize(expected_type)
  (norm_expected == 'Array' && norm_yard =~ /\A\(.*\)\z/) || (norm_yard == 'Array' && norm_expected =~ /\A\(.*\)\z/)
end

.union_containment?(yard_type, expected_type) ⇒ Boolean

Whether one type is contained in the other's comma-separated union.

Checks both directions after normalization (e.g., "String" in "String, Integer").

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if one type appears in the other's union parts



351
352
353
354
355
356
357
# File 'lib/docscribe/validator/generic_compatibility.rb', line 351

def union_containment?(yard_type, expected_type)
  return false if yard_type.nil? || expected_type.nil?

  norm_yard = normalize(yard_type)
  expected_type.split(',').any? { |part| normalize(part) == norm_yard } ||
    yard_type.split(',').any? { |part| normalize(part) == normalize(expected_type) }
end

.union_parts(str) ⇒ Array<String>

Parameters:

  • str (String)

Returns:

  • (Array<String>)


72
73
74
# File 'lib/docscribe/validator/generic_compatibility.rb', line 72

def union_parts(str)
  str.include?(',') ? split_top_level_commas_local(str).map { |p| normalize(p) } : [normalize(str)]
end

.union_parts_compatible?(yard_type, expected_type, fallback_type, _method_name) ⇒ Boolean

Parameters:

  • yard_type (String, nil)
  • expected_type (String, nil)
  • fallback_type (String)
  • _method_name (String, Symbol, nil)

Returns:

  • (Boolean)


56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/docscribe/validator/generic_compatibility.rb', line 56

def union_parts_compatible?(yard_type, expected_type, fallback_type, _method_name)
  yt = yard_type.to_s
  et = expected_type.to_s
  return false unless yt.include?(',') || et.include?(',')

  parts_yard = union_parts(yt)
  parts_expected = union_parts(et)

  return true if single_vs_union?(parts_yard, parts_expected, fallback_type)
  return true if single_vs_union?(parts_expected, parts_yard, fallback_type)

  false
end

.union_vs_optional_compatible?(yard_type, expected_type) ⇒ Boolean

Whether union ?, nil forms are compatible: Object? vs Object, nil vs Object|nil.

Handles all three forms SomeType? == SomeType|nil == SomeType, nil plus bare SomeType vs optional equivalence via pipe/comma normalization.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

Returns:

  • (Boolean)

    true if one is Type? and other is Type, nil / Type|nil



483
484
485
486
487
488
489
490
491
# File 'lib/docscribe/validator/generic_compatibility.rb', line 483

def union_vs_optional_compatible?(yard_type, expected_type)
  norm_yard = normalize(yard_type).gsub('|', ',')
  norm_expected = normalize(expected_type).gsub('|', ',')
  return true if question_vs_comma_nil?(norm_yard, norm_expected)
  return true if suffix_vs_union_first?(yard_type, expected_type)
  return true if pipe_aware_suffix_vs_union_first?(yard_type, expected_type)

  optional_forms_equal?(yard_type, expected_type)
end

.update_split_depth(chr, state) ⇒ void

This method returns an undefined value.

Parameters:

  • chr (String)
  • state (Hash<Symbol, Object>)


646
647
648
649
650
651
# File 'lib/docscribe/validator/generic_compatibility.rb', line 646

def update_split_depth(chr, state)
  deltas = { '<' => [:da, 1], '>' => [:da, -1], '[' => [:db, 1], ']' => [:db, -1], '(' => [:dp, 1], ')' => [:dp, -1] }
  key, delta = deltas[chr]
  state[key] += delta if key
  state[:cur] << chr
end

.void_compatible?(yard_type, expected_type, fallback_type = 'Object', method_name: nil) ⇒ Boolean

Whether void YARD type is compatible with expected type.

Handles fallback unions and dynamic initialize/setup compatibility where void is treated as compatible with Hash, self, or Boolean for initializers per Ruby idiom.

Parameters:

  • yard_type (String, nil)

    YARD type string

  • expected_type (String, nil)

    inferred/RBS type string

  • fallback_type (String) (defaults to: 'Object')

    fallback type for union checks

  • method_name (String, Symbol, nil) (defaults to: nil)

    method name for dynamic check

Returns:

  • (Boolean)

    true if void compatibility holds



112
113
114
115
116
117
118
119
# File 'lib/docscribe/validator/generic_compatibility.rb', line 112

def void_compatible?(yard_type, expected_type, fallback_type = 'Object', method_name: nil)
  return false unless normalize(yard_type) == 'void'
  return true if void_fallback_or_nil?(expected_type, fallback_type)
  return true if void_initialize_compatible?(expected_type, method_name)
  return true if void_predicate_compatible?(expected_type, method_name)

  false
end

.void_fallback_or_nil?(expected_type, fallback_type) ⇒ Boolean

Parameters:

  • expected_type (String, nil)
  • fallback_type (String)

Returns:

  • (Boolean)


124
125
126
# File 'lib/docscribe/validator/generic_compatibility.rb', line 124

def void_fallback_or_nil?(expected_type, fallback_type)
  fallback_union?(expected_type, fallback_type) || %w[nil void].include?(normalize(expected_type))
end

.void_initialize_compatible?(expected_type, method_name) ⇒ Boolean

Parameters:

  • expected_type (String, nil)
  • method_name (String, Symbol, nil)

Returns:

  • (Boolean)


131
132
133
134
135
136
# File 'lib/docscribe/validator/generic_compatibility.rb', line 131

def void_initialize_compatible?(expected_type, method_name)
  return false unless method_name.to_s =~ /initialize|setup/

  norm = normalize(expected_type).delete_suffix('?').strip
  norm == 'Hash' || norm.start_with?('Hash<') || norm.start_with?('Hash[') || %w[self Boolean].include?(norm)
end

.void_predicate_compatible?(expected_type, method_name) ⇒ Boolean

Parameters:

  • expected_type (String, nil)
  • method_name (String, Symbol, nil)

Returns:

  • (Boolean)


141
142
143
144
145
# File 'lib/docscribe/validator/generic_compatibility.rb', line 141

def void_predicate_compatible?(expected_type, method_name)
  return false unless method_name.to_s.end_with?('?')

  normalize(expected_type).delete_suffix('?').strip == 'Boolean'
end