Class: Puppet::Pops::Types::TypeMismatchDescriber

Inherits:
Object
  • Object
show all
Defined in:
lib/puppet/pops/types/type_mismatch_describer.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.describe_signatures(closure, signatures, args_tuple) ⇒ Object



318
319
320
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 318

def self.describe_signatures(closure, signatures, args_tuple)
  singleton.describe_signatures(closure, signatures, args_tuple)
end

.singletonObject



322
323
324
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 322

def self.singleton
  @singleton ||= new
end

.validate_default_parameter(subject, param_name, param_type, value) ⇒ Object



314
315
316
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 314

def self.validate_default_parameter(subject, param_name, param_type, value)
  singleton.validate_default_parameter(subject, param_name, param_type, value)
end

.validate_parameters(subject, params_struct, given_hash, missing_ok = false) ⇒ Object



310
311
312
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 310

def self.validate_parameters(subject, params_struct, given_hash, missing_ok = false)
  singleton.validate_parameters(subject, params_struct, given_hash, missing_ok)
end

Instance Method Details

#describe(expected, actual, path) ⇒ Object



654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 654

def describe(expected, actual, path)
  case expected
  when PVariantType
    describe_PVariantType(expected, actual, path)
  when PStructType
    describe_PStructType(expected, actual, path)
  when PTupleType
    describe_PTupleType(expected, actual, path)
  when PCallableType
    describe_PCallableType(expected, actual, path)
  when POptionalType
    describe_POptionalType(expected, actual, path)
  when PPatternType
    describe_PPatternType(expected, actual, path)
  when PEnumType
    describe_PEnumType(expected, actual, path)
  else
    describe_PAnyType(expected, actual, path)
  end
end

#describe_argument_tuple(expected, actual, path) ⇒ Object



572
573
574
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 572

def describe_argument_tuple(expected, actual, path)
  describe_tuple(expected, actual, path, CountMismatch)
end

#describe_no_block_arguments(signature, atypes, path, expected_size, actual_size, arg_count) ⇒ Object



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 474

def describe_no_block_arguments(signature, atypes, path, expected_size, actual_size, arg_count)
  # not assignable if the number of types in actual is outside number of types in expected
  if expected_size.assignable?(actual_size)
    etypes = signature.type.param_types.types
    enames = signature.parameter_names
    arg_count.times do |index|
      adx = index >= etypes.size ? etypes.size - 1 : index
      etype = etypes[adx]
      descriptions = describe(etype, atypes[index], path + [ParameterPathElement.new(enames[adx])])
      return descriptions unless descriptions.empty?
    end
    EMPTY_ARRAY
  else
    [CountMismatch.new(path, expected_size, actual_size)]
  end
end

#describe_PAnyType(expected, actual, path) ⇒ Object



650
651
652
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 650

def describe_PAnyType(expected, actual, path)
  expected.assignable?(actual) ? EMPTY_ARRAY : [TypeMismatch.new(path, expected, actual)]
end

#describe_PCallableType(expected, actual, path) ⇒ Object



622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 622

def describe_PCallableType(expected, actual, path)
  if actual.is_a?(PCallableType)
    # nil param_types means, any other Callable is assignable
    if expected.param_types.nil?
      EMPTY_ARRAY
    else
      # NOTE: these tests are made in reverse as it is calling the callable that is constrained
      # (it's lower bound), not its upper bound
      param_errors = describe_argument_tuple(expected.param_types, actual.param_types, path)
      if param_errors.empty?
        # names are ignored, they are just information
        # Blocks must be compatible
        this_block_t = expected.block_type || PUndefType::DEFAULT
        that_block_t = actual.block_type || PUndefType::DEFAULT
        if that_block_t.assignable?(this_block_t)
          EMPTY_ARRAY
        else
          [TypeMismatch.new(path + BlockPathElement.new, this_block_t, that_block_t)]
        end
      else
        param_errors
      end
    end
  else
    [TypeMismatch.new(path, expected, actual)]
  end
end

#describe_PEnumType(expected, actual, path) ⇒ Object



526
527
528
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 526

def describe_PEnumType(expected, actual, path)
  [PatternMismatch.new(path, expected, actual)]
end

#describe_POptionalType(expected, actual, path) ⇒ Object



522
523
524
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 522

def describe_POptionalType(expected, actual, path)
  actual.is_a?(PUndefType) ? [] : describe(expected.optional_type, actual, path)
end

#describe_PPatternType(expected, actual, path) ⇒ Object



530
531
532
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 530

def describe_PPatternType(expected, actual, path)
  [PatternMismatch.new(path, expected, actual)]
end

#describe_PStructType(expected, actual, path) ⇒ Object



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 534

def describe_PStructType(expected, actual, path)
  elements = expected.elements
  descriptions = []
  if actual.is_a?(PStructType)
    h2 = actual.hashed_elements.clone
    elements.each do |e1|
      key = e1.name
      e2 = h2.delete(key)
      if e2.nil?
        descriptions << MissingKey.new(path, key) unless e1.key_type.assignable?(PUndefType::DEFAULT)
      else
        descriptions.concat(describe(e1.key_type, e2.key_type, path + [MemberKeyPathElement.new(key)])) unless e1.key_type.assignable?(e2.key_type)
        descriptions.concat(describe(e1.value_type, e2.value_type, path + [MemberPathElement.new(key)])) unless e1.value_type.assignable?(e2.value_type)
      end
    end
    h2.each_key { |key| descriptions << ExtraneousKey.new(path, key) }
  elsif actual.is_a?(PHashType)
    actual_size = actual.size_type || PCollectionType::DEFAULT_SIZE
    expected_size = PIntegerType.new(elements.count { |e| !e.type.assignable?(PUndefType::DEFAULT) }, elements.size)
    if expected_size.assignable?(actual_size)
      if actual_size.to == 0 || PStringType::NON_EMPTY.assignable?(actual.key_type)
        descriptions.concat(describe(e.type, actual.element_type, path + [MemberPathElement.new(e.key)]))
      else
        descriptions << TypeMismatch(path, @non_empty_string_, actual.key_type)
      end
    else
      descriptions << SizeMismatch(path, expected_size, actual_size)
    end
  else
    descriptions << TypeMismatch.new(path, expected, actual)
  end
  descriptions
end

#describe_PTupleType(expected, actual, path) ⇒ Object



568
569
570
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 568

def describe_PTupleType(expected, actual, path)
  describe_tuple(expected, actual, path, SizeMismatch)
end

#describe_PVariantType(expected, actual, path) ⇒ Object



491
492
493
494
495
496
497
498
499
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 491

def describe_PVariantType(expected, actual, path)
  variant_descriptions = []
  expected.types.each_with_index do |vt, index|
    d = describe(vt, actual, path + [VariantPathElement.new(index)])
    return EMPTY_ARRAY if d.empty?
    variant_descriptions << d
  end
  merge_descriptions(path.length, SizeMismatch, variant_descriptions)
end

#describe_signature_arguments(signature, args_tuple, path) ⇒ Object



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 431

def describe_signature_arguments(signature, args_tuple, path)
  params_tuple = signature.type.param_types
  params_size_t = params_tuple.size_type || TypeFactory.range(*params_tuple.size_range)

  if args_tuple.is_a?(PTupleType)
    arg_types = args_tuple.types
  elsif args_tuple.is_a?(PArrayType)
    arg_types = Array.new(params_tuple.types.size, args_tuple.element_type || PUndefType::DEFAULT)
  else
    return [TypeMismatch.new(path, params_tuple, args_tuple)]
  end

  if arg_types.last.kind_of_callable?
    # Check other arguments
    arg_count = arg_types.size - 1
    describe_no_block_arguments(signature, arg_types, path, params_size_t, TypeFactory.range(arg_count, arg_count), arg_count)
  else
    args_size_t = TypeFactory.range(*args_tuple.size_range)
    describe_no_block_arguments(signature, arg_types, path, params_size_t, args_size_t, arg_types.size)
  end
end

#describe_signature_block(signature, args_tuple, path) ⇒ Object



453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 453

def describe_signature_block(signature, args_tuple, path)
  param_block_t = signature.block_type
  arg_block_t = args_tuple.is_a?(PTupleType) ? args_tuple.types.last : nil
  if TypeCalculator.is_kind_of_callable?(arg_block_t)
    # Can't pass a block to a callable that doesn't accept one
    if param_block_t.nil?
      [UnexpectedBlock.new(path)]
    else
      # Check that the block is of the right type
      describe(param_block_t, arg_block_t, path + [BlockPathElement.new])
    end
  else
    # Check that the block is optional
    if param_block_t.nil? || param_block_t.assignable?(PUndefType::DEFAULT)
      EMPTY_ARRAY
    else
      [MissingRequiredBlock.new(path)]
    end
  end
end

#describe_signatures(closure, signatures, args_tuple) ⇒ Object



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 388

def describe_signatures(closure, signatures, args_tuple)
  error_arrays = []
  signatures.each_with_index do |signature, index|
    error_arrays << describe_signature_arguments(signature, args_tuple, [SignaturePathElement.new(index)])
  end

  # Skip block checks if all signatures have argument errors
  unless error_arrays.all? { |a| !a.empty? }
    block_arrays = []
    signatures.each_with_index do |signature, index|
      block_arrays << describe_signature_block(signature, args_tuple, [SignaturePathElement.new(index)])
    end
    bc_count = block_arrays.count { |a| !a.empty? }
    if bc_count == block_arrays.size
      # Skip argument errors when all alternatives have block errors
      error_arrays = block_arrays
    elsif bc_count > 0
      # Merge errors giving argument errors precedence over block errors
      error_arrays.each_with_index { |a, index| error_arrays[index] = block_arrays[index] if a.empty? }
    end
  end
  return nil if error_arrays.empty?

  label = closure == 'lambda' ? 'block' : "'#{closure}'"
  errors = merge_descriptions(0, CountMismatch, error_arrays)
  if errors.size == 1
    "#{label}#{errors[0]}"
  else
    if signatures.size == 1
      sig = signatures[0]
      result = ["#{label} expected (#{signature_string(sig)})"]
      result.concat(error_arrays[0].map { |e| "  rejected:#{e.chop_path(0)}" })
    else
      result = ["#{label} expected one of:"]
      signatures.each_with_index do |sg, index|
        result << "  (#{signature_string(sg)})"
        result.concat(error_arrays[index].map { |e| "    rejected:#{e.chop_path(0)}" })
      end
    end
    result.join("\n")
  end
end

#describe_struct_signature(params_struct, param_hash, missing_ok = false) ⇒ Array<Mismatch>

Validates that all entries in the param_hash exists in the given param_struct, that their type conforms with the corresponding param_struct element and that all required values are provided. An error message is created for each problem found.

Parameters:

  • params_struct (PStructType)

    Struct to use for validation

  • param_hash (Hash<String,Object>)

    The parameters to validate

  • missing_ok (Boolean) (defaults to: false)

    Do not generate errors on missing parameters

Returns:

  • (Array<Mismatch>)

    An array of found errors. An empty array indicates no errors.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 371

def describe_struct_signature(params_struct, param_hash, missing_ok = false)
  param_type_hash = params_struct.hashed_elements
  result =  param_hash.each_key.reject { |name| param_type_hash.include?(name) }.map { |name| InvalidParameter.new(nil, name) }

  params_struct.elements.each do |elem|
    name = elem.name
    value = param_hash[name]
    value_type = elem.value_type
    if param_hash.include?(name)
      result << describe(value_type, TypeCalculator.singleton.infer_set(value).generalize, [ParameterPathElement.new(name)]) unless value_type.instance?(value)
    else
      result << MissingParameter.new(nil, name) unless elem.key_type.assignable?(PUndefType::DEFAULT) unless missing_ok
    end
  end
  result
end

#describe_tuple(expected, actual, path, size_mismatch_class) ⇒ Object



576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 576

def describe_tuple(expected, actual, path, size_mismatch_class)
  return if expected == actual || expected.types.empty? && (actual.is_a?(PArrayType))
  expected_size = expected.size_type || TypeFactory.range(*expected.size_range)

  if actual.is_a?(PTupleType)
    actual_size = actual.size_type || TypeFactory.range(*actual.size_range)

    # not assignable if the number of types in actual is outside number of types in expected
    if expected_size.assignable?(actual_size)
      etypes = expected.types
      descriptions = []
      actual.types.each_with_index do |atype, index|
        adx = index >= etypes.size ? etypes.size - 1 : index
        etype = etypes[adx]
        descriptions.concat(describe(etypes[adx], atype, path + [ArrayPathElement.new(adx)]))
      end
      descriptions
    else
      [size_mismatch_class.new(path, expected_size, actual_size)]
    end
  elsif actual.is_a?(PArrayType)
    t2_entry = actual.element_type

    if t2_entry.nil?
      # Array of anything can not be assigned (unless tuple is tuple of anything) - this case
      # was handled at the top of this method.
      #
      [TypeMismatch.new(path, expected, actual)]
    else
      expected_size = expected.size_type || TypeFactory.range(*expected.size_range)
      actual_size = actual.size_type || PCollectionType::DEFAULT_SIZE
      if expected_size.assignable?(actual_size)
        descriptions = []
        expected.types.each_with_index do |etype, index|
          descriptions.concat(describe(etype, actual.element_type, path + [ArrayPathElement.new(index)]))
        end
        descriptions
      else
        [size_mismatch_class.new(path, expected_size, actual_size)]
      end
    end
  else
    [TypeMismatch.new(path, expected, actual)]
  end
end

#max(a, b) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Why oh why Ruby do you not have a standard Math.max ?



732
733
734
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 732

def max(a, b)
  a >= b ? a : b
end

#merge_descriptions(varying_path_position, size_mismatch_class, variant_descriptions) ⇒ Object



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 501

def merge_descriptions(varying_path_position, size_mismatch_class, variant_descriptions)
  descriptions = variant_descriptions.flatten
  [size_mismatch_class, MissingRequiredBlock, UnexpectedBlock, TypeMismatch].each do |mismatch_class|
    mismatches = descriptions.select { |desc| desc.is_a?(mismatch_class) }
    if mismatches.size == variant_descriptions.size
      # If they all have the same canonical path, then we can compact this into one
      generic_mismatch = mismatches.inject do |prev, curr|
        break nil unless prev.canonical_path == curr.canonical_path
        prev.merge(prev.path, curr)
      end
      unless generic_mismatch.nil?
        # Report the generic mismatch and skip the rest
        descriptions = [generic_mismatch]
        break
      end
    end
  end
  descriptions = descriptions.uniq
  descriptions.size == 1 ? [descriptions[0].chop_path(varying_path_position)] : descriptions
end

#optional(index, required_count) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



737
738
739
740
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 737

def optional(index, required_count)
  count = index + 1
  count > required_count
end

#signature_string(signature) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Produces a string for the signature(s)



678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 678

def signature_string(signature)
  param_types = signature.type.param_types
  param_names = signature.parameter_names

  from, to = param_types.size_range
  if from == 0 && to == 0
    # No parameters function
    return ''
  end

  required_count = from
  types =
    case param_types
    when Puppet::Pops::Types::PTupleType
      param_types.types
    when Puppet::Pops::Types::PArrayType
      [param_types.element_type]
    end
  tc = Puppet::Pops::Types::TypeCalculator.singleton

  # join type with names (types are always present, names are optional)
  # separate entries with comma
  #
  param_names = Array.new(types.size, '') if param_names.empty?
  limit = param_names.size
  result = param_names.each_with_index.map do |name, index|
    type = types[index] || types[-1]
    indicator = ''
    if to == Float::INFINITY && index == limit - 1
      # Last is a repeated_param.
      indicator = from == param_names.size ? '+' : '*'
    elsif optional(index, required_count)
      indicator = '?'
      type = type.optional_type if type.is_a?(Puppet::Pops::Types::POptionalType)
    end
    "#{tc.string(type)} #{name}#{indicator}"
  end.join(', ')

  # If there is a block, include it
  case signature.type.block_type
  when Puppet::Pops::Types::POptionalType
    result << ', ' unless result == ''
    result << "#{signature.type.block_type.optional_type} #{signature.block_name}?"
  when Puppet::Pops::Types::PCallableType
    result << ', ' unless result == ''
    result << "#{signature.type.block_type} #{signature.block_name}"
  when NilClass
    # nothing
  end
  result
end

#validate_default_parameter(subject, param_name, param_type, value) ⇒ Object

Parameters:

  • subject (String)

    string to be prepended to the exception message

  • param_name (String)

    parameter name

  • param_type (PAnyType)

    parameter type

  • value (Object)

    value to be validated against the given type



350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 350

def validate_default_parameter(subject, param_name, param_type, value)
  unless param_type.instance?(value)
    errors = describe(param_type, TypeCalculator.singleton.infer_set(value).generalize, [ParameterPathElement.new(param_name)])
    case errors.size
    when 0
    when 1
      raise Puppet::ParseError.new("#{subject}:#{errors[0]}")
    else
      raise Puppet::ParseError.new("#{subject}:\n #{errors.join("\n ")}")
    end
  end
end

#validate_parameters(subject, params_struct, given_hash, missing_ok = false) ⇒ Object

Validates that all entries in the give_hash exists in the given param_struct, that their type conforms with the corresponding param_struct element and that all required values are provided.

Parameters:

  • subject (String)

    string to be prepended to the exception message

  • params_struct (PStructType)

    Struct to use for validation

  • given_hash (Hash<String,Object>)

    the parameters to validate

  • missing_ok (Boolean) (defaults to: false)

    Do not generate errors on missing parameters



334
335
336
337
338
339
340
341
342
343
# File 'lib/puppet/pops/types/type_mismatch_describer.rb', line 334

def validate_parameters(subject, params_struct, given_hash, missing_ok = false)
  errors = describe_struct_signature(params_struct, given_hash, missing_ok).flatten
  case errors.size
  when 0
  when 1
    raise Puppet::ParseError.new("#{subject}:#{errors[0]}")
  else
    raise Puppet::ParseError.new("#{subject}:\n #{errors.join("\n ")}")
  end
end