diff --git a/lib/puppet/pops/evaluator/access_operator.rb b/lib/puppet/pops/evaluator/access_operator.rb index a6507e8ac..1d34daeaa 100644 --- a/lib/puppet/pops/evaluator/access_operator.rb +++ b/lib/puppet/pops/evaluator/access_operator.rb @@ -1,577 +1,588 @@ # AccessOperator handles operator [] # This operator is part of evaluation. # class Puppet::Pops::Evaluator::AccessOperator # Provides access to the Puppet 3.x runtime (scope, etc.) # This separation has been made to make it easier to later migrate the evaluator to an improved runtime. # include Puppet::Pops::Evaluator::Runtime3Support Issues = Puppet::Pops::Issues TYPEFACTORY = Puppet::Pops::Types::TypeFactory attr_reader :semantic # Initialize with AccessExpression to enable reporting issues # @param access_expression [Puppet::Pops::Model::AccessExpression] the semantic object being evaluated # @return [void] # def initialize(access_expression) @@access_visitor ||= Puppet::Pops::Visitor.new(self, "access", 2, nil) @semantic = access_expression end def access (o, scope, *keys) @@access_visitor.visit_this_2(self, o, scope, keys) end protected def access_Object(o, scope, keys) fail(Issues::OPERATOR_NOT_APPLICABLE, @semantic.left_expr, :operator=>'[]', :left_value => o) end def access_String(o, scope, keys) keys.flatten! result = case keys.size when 0 fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) when 1 # Note that Ruby 1.8.7 requires a length of 1 to produce a String k1 = coerce_numeric(keys[0], @semantic.keys, scope) bad_access_key_type(o, 0, k1, Integer) unless k1.is_a?(Integer) k2 = 1 k1 = k1 < 0 ? o.length + k1 : k1 # abs pos # if k1 is outside, a length of 1 always produces an empty string if k1 < 0 '' else o[ k1, k2 ] end when 2 k1 = coerce_numeric(keys[0], @semantic.keys, scope) k2 = coerce_numeric(keys[1], @semantic.keys, scope) [k1, k2].each_with_index { |k,i| bad_access_key_type(o, i, k, Integer) unless k.is_a?(Integer) } k1 = k1 < 0 ? o.length + k1 : k1 # abs pos (negative is count from end) k2 = k2 < 0 ? o.length - k1 + k2 + 1 : k2 # abs length (negative k2 is length from pos to end count) # if k1 is outside, adjust to first position, and adjust length if k1 < 0 k2 = k2 + k1 k1 = 0 end o[ k1, k2 ] else fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) end # Specified as: an index outside of range, or empty result == empty string (result.nil? || result.empty?) ? '' : result end # Parameterizes a PRegexp Type with a pattern string or r ruby egexp # def access_PRegexpType(o, scope, keys) keys.flatten! unless keys.size == 1 blamed = keys.size == 0 ? @semantic : @semantic.keys[1] fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => o, :min=>1, :actual => keys.size) end assert_keys(keys, o, 1, 1, String, Regexp) Puppet::Pops::Types::TypeFactory.regexp(*keys) end # Evaluates [] with 1 or 2 arguments. One argument is an index lookup, two arguments is a slice from/to. # def access_Array(o, scope, keys) keys.flatten! case keys.size when 0 fail(Puppet::Pops::Issues::BAD_ARRAY_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) when 1 k = coerce_numeric(keys[0], @semantic.keys[0], scope) unless k.is_a?(Integer) bad_access_key_type(o, 0, k, Integer) end o[k] when 2 # A slice [from, to] with support for -1 to mean start, or end respectively. k1 = coerce_numeric(keys[0], @semantic.keys[0], scope) k2 = coerce_numeric(keys[1], @semantic.keys[1], scope) [k1, k2].each_with_index { |k,i| bad_access_key_type(o, i, k, Integer) unless k.is_a?(Integer) } # Help confused Ruby do the right thing (it truncates to the right, but negative index + length can never overlap # the available range. k1 = k1 < 0 ? o.length + k1 : k1 # abs pos (negative is count from end) k2 = k2 < 0 ? o.length - k1 + k2 + 1 : k2 # abs length (negative k2 is length from pos to end count) # if k1 is outside, adjust to first position, and adjust length if k1 < 0 k2 = k2 + k1 k1 = 0 end # Help ruby always return empty array when asking for a sub array result = o[ k1, k2 ] result.nil? ? [] : result else fail(Puppet::Pops::Issues::BAD_ARRAY_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) end end # Evaluates [] with support for one or more arguments. If more than one argument is used, the result # is an array with each lookup. # @note # Does not flatten its keys to enable looking up with a structure # def access_Hash(o, scope, keys) # Look up key in hash, if key is nil or :undef, try alternate form before giving up. # This makes :undef and nil "be the same key". (The alternative is to always only write one or the other # in all hashes - that is much harder to guarantee since the Hash is a regular Ruby hash. # result = keys.collect do |k| o.fetch(k) do |key| case key when nil o[:undef] when :undef o[:nil] else nil end end end case result.size when 0 fail(Puppet::Pops::Issues::BAD_HASH_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) when 1 result.pop else # remove nil elements and return result.compact! result end end # Ruby does not have an infinity constant. TODO: Consider having one constant in Puppet. Now it is in several places. INFINITY = 1.0 / 0.0 def access_PEnumType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, INFINITY, String) Puppet::Pops::Types::TypeFactory.enum(*keys) end def access_PVariantType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, INFINITY, Puppet::Pops::Types::PAbstractType) Puppet::Pops::Types::TypeFactory.variant(*keys) end def access_PTupleType(o, scope, keys) keys.flatten! if TYPEFACTORY.is_range_parameter?(keys[-2]) && TYPEFACTORY.is_range_parameter?(keys[-1]) size_type = TYPEFACTORY.range(keys[-2], keys[-1]) keys = keys[0, keys.size - 2] elsif TYPEFACTORY.is_range_parameter?(keys[-1]) size_type = TYPEFACTORY.range(keys[-1], :default) keys = keys[0, keys.size - 1] end assert_keys(keys, o, 1, INFINITY, Puppet::Pops::Types::PAbstractType) t = Puppet::Pops::Types::TypeFactory.tuple(*keys) # set size type, or nil for default (exactly 1) t.size_type = size_type t end def access_PStructType(o, scope, keys) assert_keys(keys, o, 1, 1, Hash) TYPEFACTORY.struct(keys[0]) end def access_PStringType(o, scope, keys) keys.flatten! case keys.size when 1 size_t = collection_size_t(0, keys[0]) when 2 size_t = collection_size_t(0, keys[0], keys[1]) else fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic, {:actual => keys.size}) end string_t = Puppet::Pops::Types::TypeFactory.string() string_t.size_type = size_t string_t end # Asserts type of each key and calls fail with BAD_TYPE_SPECIFICATION # @param keys [Array] the evaluated keys # @param o [Object] evaluated LHS reported as :base_type # @param min [Integer] the minimum number of keys (typically 1) # @param max [Numeric] the maximum number of keys (use same as min, specific number, or INFINITY) # @param allowed_classes [Class] a variable number of classes that each key must be an instance of (any) # @api private # def assert_keys(keys, o, min, max, *allowed_classes) size = keys.size unless size.between?(min, max || INFINITY) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, :base_type => o, :min=>1, :max => max, :actual => keys.size) end keys.each_with_index do |k, i| unless allowed_classes.any? {|clazz| k.is_a?(clazz) } bad_type_specialization_key_type(o, i, k, *allowed_classes) end end end def bad_access_key_type(lhs, key_index, actual, *expected_classes) fail(Puppet::Pops::Issues::BAD_SLICE_KEY_TYPE, @semantic.keys[key_index], { :left_value => lhs, :actual => bad_key_type_name(actual), :expected_classes => expected_classes }) end def bad_key_type_name(actual) case actual when nil, :undef 'Undef' when :default 'Default' else actual.class.name end end def bad_type_specialization_key_type(type, key_index, actual, *expected_classes) label_provider = Puppet::Pops::Model::ModelLabelProvider.new() expected = expected_classes.map {|c| label_provider.label(c) }.join(' or ') fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, @semantic.keys[key_index], { :type => type, :message => "Cannot use #{bad_key_type_name(actual)} where #{expected} is expected" }) end def access_PPatternType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, INFINITY, String, Regexp, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PRegexpType) Puppet::Pops::Types::TypeFactory.pattern(*keys) end def access_POptionalType(o, scope, keys) keys.flatten! if keys.size == 1 unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Optional-Type', :actual => keys[0].class}) end result = Puppet::Pops::Types::POptionalType.new() result.optional_type = keys[0] result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Optional-Type', :min => 1, :actual => keys.size}) end end def access_PType(o, scope, keys) keys.flatten! if keys.size == 1 unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Type-Type', :actual => keys[0].class}) end result = Puppet::Pops::Types::PType.new() result.type = keys[0] result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Type-Type', :min => 1, :actual => keys.size}) end end def access_PRubyType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, 1, String) # create ruby type based on name of class, not inference of key's type Puppet::Pops::Types::TypeFactory.ruby_type(keys[0]) end def access_PIntegerType(o, scope, keys) keys.flatten! unless keys.size.between?(1, 2) fail(Puppet::Pops::Issues::BAD_INTEGER_SLICE_ARITY, @semantic, {:actual => keys.size}) end keys.each_with_index do |x, index| fail(Puppet::Pops::Issues::BAD_INTEGER_SLICE_TYPE, @semantic.keys[index], {:actual => x.class}) unless (x.is_a?(Integer) || x == :default) end ranged_integer = Puppet::Pops::Types::PIntegerType.new() from, to = keys ranged_integer.from = from == :default ? nil : from ranged_integer.to = to == :default ? nil : to ranged_integer end def access_PFloatType(o, scope, keys) keys.flatten! unless keys.size.between?(1, 2) fail(Puppet::Pops::Issues::BAD_FLOAT_SLICE_ARITY, @semantic, {:actual => keys.size}) end keys.each_with_index do |x, index| fail(Puppet::Pops::Issues::BAD_FLOAT_SLICE_TYPE, @semantic.keys[index], {:actual => x.class}) unless (x.is_a?(Float) || x.is_a?(Integer) || x == :default) end ranged_float = Puppet::Pops::Types::PFloatType.new() from, to = keys ranged_float.from = from == :default || from.nil? ? nil : Float(from) ranged_float.to = to == :default || to.nil? ? nil : Float(to) ranged_float end # A Hash can create a new Hash type, one arg sets value type, two args sets key and value type in new type. # With 3 or 4 arguments, these are used to create a size constraint. # It is not possible to create a collection of Hash types directly. # def access_PHashType(o, scope, keys) keys.flatten! keys[0,2].each_with_index do |k, index| unless k.is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[index], {:base_type => 'Hash-Type', :actual => k.class}) end end case keys.size when 1 result = Puppet::Pops::Types::PHashType.new() result.key_type = o.key_type.copy result.element_type = keys[0] result when 2 result = Puppet::Pops::Types::PHashType.new() result.key_type = keys[0] result.element_type = keys[1] result when 3 result = Puppet::Pops::Types::PHashType.new() result.key_type = keys[0] result.element_type = keys[1] size_t = collection_size_t(1, keys[2]) result when 4 result = Puppet::Pops::Types::PHashType.new() result.key_type = keys[0] result.element_type = keys[1] size_t = collection_size_t(1, keys[2], keys[3]) result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, { :base_type => 'Hash-Type', :min => 1, :max => 4, :actual => keys.size }) end result.size_type = size_t if size_t result end # CollectionType is parameterized with a range def access_PCollectionType(o, scope, keys) keys.flatten! case keys.size when 1 size_t = collection_size_t(1, keys[0]) when 2 size_t = collection_size_t(1, keys[0], keys[1]) else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Collection-Type', :min => 1, :max => 2, :actual => keys.size}) end result = Puppet::Pops::Types::PCollectionType.new() result.size_type = size_t result end # An Array can create a new Array type. It is not possible to create a collection of Array types. # def access_PArrayType(o, scope, keys) keys.flatten! case keys.size when 1 size_t = nil when 2 size_t = collection_size_t(1, keys[1]) when 3 size_t = collection_size_t(1, keys[1], keys[2]) else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Array-Type', :min => 1, :max => 3, :actual => keys.size}) end unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Array-Type', :actual => keys[0].class}) end result = Puppet::Pops::Types::PArrayType.new() result.element_type = keys[0] result.size_type = size_t result end # Produces an PIntegerType (range) given one or two keys. def collection_size_t(start_index, *keys) if keys.size == 1 && keys[0].is_a?(Puppet::Pops::Types::PIntegerType) keys[0].copy else keys.each_with_index do |x, index| fail(Puppet::Pops::Issues::BAD_COLLECTION_SLICE_TYPE, @semantic.keys[start_index + index], {:actual => x.class}) unless (x.is_a?(Integer) || x == :default) end ranged_integer = Puppet::Pops::Types::PIntegerType.new() from, to = keys ranged_integer.from = from == :default ? nil : from ranged_integer.to = to == :default ? nil : to ranged_integer end end # A Resource can create a new more specific Resource type, and/or an array of resource types # If the given type has title set, it can not be specified further. # @example # Resource[File] # => File # Resource[File, 'foo'] # => File[foo] # Resource[File. 'foo', 'bar'] # => [File[foo], File[bar]] # File['foo', 'bar'] # => [File[foo], File[bar]] # File['foo']['bar'] # => Value of the 'bar' parameter in the File['foo'] resource # Resource[File]['foo', 'bar'] # => [File[Foo], File[bar]] # Resource[File, 'foo', 'bar'] # => [File[foo], File[bar]] # Resource[File, 'foo']['bar'] # => Value of the 'bar' parameter in the File['foo'] resource # def access_PResourceType(o, scope, keys) blamed = keys.size == 0 ? @semantic : @semantic.keys[0] - keys_orig_size = keys.size - keys.flatten! - keys.compact! - if keys_orig_size == 0 + if keys.size == 0 fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => Puppet::Pops::Types::TypeCalculator.new().string(o), :min => 1, :max => -1, :actual => 0) end + # Must know which concrete resource type to operate on in all cases. + # It is not allowed to specify the type in an array arg - e.g. Resource[[File, 'foo']] + # type_name is LHS type_name if set, else the first given arg + type_name = o.type_name || keys.shift + type_name = case type_name + when Puppet::Pops::Types::PResourceType + type_name.type_name + when String + type_name.downcase + else + # blame given left expression if it defined the type, else the first given key expression + blame = o.type_name.nil? ? @semantic.keys[0] : @semantic.left_expr + fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_SPECIALIZATION, blame, {:actual => type_name.class}) + end + + # The result is an array if multiple titles are given, or if titles are specified with an array + # (possibly multiple arrays, and nested arrays). + result_type_array = keys.size > 1 || keys[0].is_a?(Array) + keys_orig_size = keys.size + + keys.flatten! + keys.compact! + # If given keys that were just a mix of empty/nil with empty array as a result. # As opposed to calling the function the wrong way (without any arguments), (configurable issue), # Return an empty array # if keys.empty? && keys_orig_size > 0 optionally_fail(Puppet::Pops::Issues::EMPTY_RESOURCE_SPECIALIZATION, blamed) - return [] + return result_type_array ? [] : nil end if !o.title.nil? # lookup resource and return one or more parameter values resource = find_resource(scope, o.type_name, o.title) unless resource fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE, @semantic, {:type_name => o.type_name, :title => o.title}) end result = keys.map do |k| unless is_parameter_of_resource?(scope, resource, k) fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE_PARAMETER, @semantic, {:type_name => o.type_name, :title => o.title, :param_name=>k}) end get_resource_parameter_value(scope, resource, k) end - return result.size <= 1 ? result.pop : result + return result_type_array ? result : result.pop end - # type_name is LHS type_name if set, else the first given arg - type_name = o.type_name || keys.shift - type_name = case type_name - when Puppet::Pops::Types::PResourceType - type_name.type_name - when String - type_name.downcase - else - blame = keys_orig_size != keys.size ? @semantic.keys[0] : @semantic.left_expr - fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_SPECIALIZATION, blame, {:actual => type_name.class}) - end keys = [:no_title] if keys.size < 1 # if there was only a type_name and it was consumed result = keys.each_with_index.map do |t, i| unless t.is_a?(String) || t == :no_title type_to_report = case t when nil, :undef 'Undef' when :default 'Default' else t.class.name end index = keys_orig_size != keys.size ? i+1 : i fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, @semantic.keys[index], { :type => o, :message => "Cannot use #{type_to_report} where String is expected" }) end rtype = Puppet::Pops::Types::PResourceType.new() rtype.type_name = type_name rtype.title = (t == :no_title ? nil : t) rtype end - # returns single type as type, else an array of types - result.size == 1 ? result.pop : result + # returns single type if request was for a single entity, else an array of types (possibly empty) + return result_type_array ? result : result.pop end def access_PHostClassType(o, scope, keys) blamed = keys.size == 0 ? @semantic : @semantic.keys[0] keys_orig_size = keys.size + # The result is an array if multiple classnames are given, or if classnames are specified with an array + # (possibly multiple arrays, and nested arrays). + result_type_array = keys.size > 1 || keys[0].is_a?(Array) + keys.flatten! keys.compact! if keys_orig_size == 0 fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => Puppet::Pops::Types::TypeCalculator.new().string(o), :min => 1, :max => -1, :actual => 0) end # If given keys that were just a mix of empty/nil with empty array as a result. # As opposed to calling the function the wrong way (without any arguments), (configurable issue), # Return an empty array # if keys.empty? && keys_orig_size > 0 optionally_fail(Puppet::Pops::Issues::EMPTY_RESOURCE_SPECIALIZATION, blamed) - return [] + return result_type_array ? [] : nil end if ! o.class_name.nil? # lookup class resource and return one or more parameter values resource = find_resource(scope, 'class', o.class_name) unless resource fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE, @semantic, {:type_name => 'Class', :title => o.class_name}) end result = keys.map do |k| unless is_parameter_of_resource?(scope, resource, k) fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE_PARAMETER, @semantic, {:type_name => 'Class', :title => o.class_name, :param_name=>k}) end get_resource_parameter_value(scope, resource, k) end - return result.size <= 1 ? result.pop : result - # TODO: if [] is applied to specific class, it should be treated the same as getting - # a resource parameter. Now it fails the operation - # - fail(Puppet::Pops::Issues::ILLEGAL_TYPE_SPECIALIZATION, semantic.left_expr, {:kind => 'Class'}) + return result_type_array ? result : result.pop end + # The type argument may be a Resource Type - the Puppet Language allows a reference such as # Class[Foo], and this is interpreted as Class[Resource[Foo]] - which is ok as long as the resource # does not have a title. This should probably be deprecated. # result = keys.each_with_index.map do |c, i| ctype = Puppet::Pops::Types::PHostClassType.new() if c.is_a?(Puppet::Pops::Types::PResourceType) && !c.type_name.nil? && c.title.nil? # Remove leading '::' since all references are global, and 3x runtime does the wrong thing c = c.type_name.downcase.sub(/^::/, '') end unless c.is_a?(String) fail(Puppet::Pops::Issues::ILLEGAL_HOSTCLASS_NAME, @semantic.keys[i], {:name => c}) end if c !~ Puppet::Pops::Patterns::NAME fail(Issues::ILLEGAL_NAME, @semantic.keys[i], {:name=>c}) end ctype.class_name = c.downcase.sub(/^::/,'') ctype end + # returns single type as type, else an array of types - result.size == 1 ? result.pop : result + return result_type_array ? result : result.pop end end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index c7f9f5531..12f822ac8 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,535 +1,540 @@ # A module with bindings between the new evaluator and the 3x runtime. # The intention is to separate all calls into scope, compiler, resource, etc. in this module # to make it easier to later refactor the evaluator for better implementations of the 3x classes. # # @api private module Puppet::Pops::Evaluator::Runtime3Support # Fails the evaluation of _semantic_ with a given issue. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def fail(issue, semantic, options={}, except=nil) optionally_fail(issue, semantic, options, except) # an error should have been raised since fail always fails raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end # Optionally (based on severity) Fails the evaluation of _semantic_ with a given issue # If the given issue is configured to be of severity < :error it is only reported, and the function returns. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def optionally_fail(issue, semantic, options={}, except=nil) if except.nil? # Want a stacktrace, and it must be passed as an exception begin raise EvaluationError.new() rescue EvaluationError => e except = e end end diagnostic_producer.accept(issue, semantic, options, except) end # Binds the given variable name to the given value in the given scope. # The reference object `o` is intended to be used for origin information - the 3x scope implementation # only makes use of location when there is an error. This is now handled by other mechanisms; first a check # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the # scope.setvar still raises an error, the general exception handling for evaluation of the assignment # expression knows about its location. Because of this, there is no need to extract the location for each # setting (extraction is somewhat expensive since 3x requires line instead of offset). # def set_variable(name, value, o, scope) # Scope also checks this but requires that location information are passed as options. # Those are expensive to calculate and a test is instead made here to enable failing with better information. # The error is not specific enough to allow catching it - need to check the actual message text. # TODO: Improve the messy implementation in Scope. # if scope.bound?(name) if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name) fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} ) else fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} ) end end scope.setvar(name, value) end # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist) # def get_variable_value(name, o, scope) # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable) # Not ideal, scope should support numeric lookup directly instead. # TODO: consider fixing scope catch(:undefined_variable) { return scope.lookupvar(name.to_s) } # It is always ok to reference numeric variables even if they are not assigned. They are always undef # if not set by a match expression. # unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name}) end end # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if # variable is bound to nil. # def variable_bound?(name, scope) scope.bound?(name.to_s) end # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes. # def variable_exists?(name, scope) scope.exist?(name.to_s) end def set_match_data(match_data, o, scope) # See set_variable for rationale for not passing file and line to ephemeral_from. # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to # clear the match variables without rolling back the ephemeral stack.) # This implementation does not attempt to fix this, it behaves the same bad way. unless match_data.nil? scope.ephemeral_from(match_data) end end # Creates a local scope with vairalbes set from a hash of variable name to value # def create_local_scope_from(hash, scope) # two dummy values are needed since the scope tries to give an error message (can not happen in this # case - it is just wrong, the error should be reported by the caller who knows in more detail where it # is in the source. # raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash) scope.ephemeral_from(hash) end # Creates a nested match scope def create_match_scope_from(scope) # Create a transparent match scope (for future matches) scope.new_match_scope(nil) end def get_scope_nesting_level(scope) scope.ephemeral_level end def set_scope_nesting_level(scope, level) # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all # ephemeral/local scopes - which is a sure way to create havoc. # scope.unset_ephemeral_var(level) end # Adds a relationship between the given `source` and `target` of the given `relationship_type` # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from) # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to) # @param relationship_type [:relationship, :subscription] the type of the relationship # def add_relationship(source, target, relationship_type, scope) # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation. # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to # add a duplicate. There is also no location information to signal the original place in the logic. The user will have # to go fish. # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong). # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser). # Further, the title undergoes the same munging !!! # # That bug infested nest of messy logic needs serious Exorcism! # # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded # Relationship object at a much later point, and it is responsible for invoking all the messy logic. # # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now # concentrate on transforming the type references to what is expected by the wacky logic. # # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) } # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead # scan the catalog's resources # # GAAAH, it is even worse! # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information # filled out. One or an array of resources are returned. # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target. # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!). # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the # uniqueness key to lookup. # # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long # and convoluted path of evaluation. # In order to do this in a way that is similar to 3.x two resources are created to be used as keys. # # # TODO: logic that creates a PCatalogEntryType should resolve it to ensure it is loaded (to the best of known_resource_types knowledge). # If this is not done, the order in which things are done may be different? OTOH, it probably works anyway :-) # TODO: Not sure if references needs to be resolved via the scope? # # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the # compiler - it is simply passed through here for processing by the compiler at the right time). # if source.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(target) target_resource = Puppet::Resource.new(type, title) end # Add the relationship to the compiler for later evaluation. scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type)) end # Coerce value `v` to numeric or fails. # The given value `v` is coerced to Numeric, and if that fails the operation # calls {#fail}. # @param v [Object] the value to convert # @param o [Object] originating instruction # @param scope [Object] the (runtime specific) scope where evaluation of o takes place # @return [Numeric] value `v` converted to Numeric. # def coerce_numeric(v, o, scope) unless n = Puppet::Pops::Utils.to_n(v) fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v}) end n end # Asserts that the given function name resolves to an available function. The function is loaded # as a side effect. Fails if the function does not exist. # def assert_function_available(name, o, scope) # Check first via 4x API (if it is available), and the function exists if loaders = Puppet.lookup(:loaders) {nil} if loaders && func = loaders.puppet_system_loader.load(:function, name) return end end fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) end def call_function(name, args, o, scope) # Call via 4x API if it is available, and the function exists if loaders = Puppet.lookup(:loaders) {nil} if loaders && func = loaders.puppet_system_loader.load(:function, name) return func.call(scope, *args) end end # TODO: if Puppet[:biff] == true, then 3x functions should be called via loaders above # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # NOTE: Passing an empty string last converts :undef to empty string mapped_args = args.map {|a| convert(a, scope, '') } scope.send("function_#{name}", mapped_args) end # Returns true if the function produces a value def rvalue_function?(name, o, scope) Puppet::Parser::Functions.rvalue?(name) end # The o is used for source reference def create_resource_parameter(o, scope, name, value, operator) file, line = extract_file_line(o) Puppet::Parser::Resource::Param.new( :name => name, :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters) # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message # from the raised exception. (It may be good enough). # resolve in scope. fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work). # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) # Build a resource for each title resource_titles.map do |resource_title| resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => evaluated_parameters, :file => file, :line => line, :exported => exported, :virtual => virtual, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == 'class' # Turn the resource into a PType (a reference to a resource type) # weed out nil's resource_to_ptype(resource) end end # Defines default parameters for a type with the given name. # def create_resource_defaults(o, scope, type_name, evaluated_parameters) # Note that name must be capitalized in this 3x call # The 3x impl creates a Resource instance with a bogus title and then asks the created resource # for the type of the name. # Note, locations are available per parameter. # scope.define_settings(type_name.capitalize, evaluated_parameters) end # Creates resource overrides for all resource type objects in evaluated_resources. The same set of # evaluated parameters are applied to all. # def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work. # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) evaluated_resources.each do |r| resource = Puppet::Parser::Resource.new( r.type_name, r.title, :parameters => evaluated_parameters, :file => file, :line => line, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope ) scope.compiler.add_override(resource) end end # Finds a resource given a type and a title. # def find_resource(scope, type_name, title) scope.compiler.findresource(type_name, title) end # Returns the value of a resource's parameter by first looking up the parameter in the resource # and then in the defaults for the resource. Since the resource exists (it must in order to look up its # parameters, any overrides have already been applied). Defaults are not applied to a resource until it # has been finished (which typically has not taked place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) val = resource[parameter_name] if val.nil? && defaults = scope.lookupdefaults(resource.type) # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds # name and value. param = defaults[parameter_name.to_sym] val = param.value end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? type_calculator.infer(resource) end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true? o # Is the value true? This allows us to control the definition of truth # in one place. case o when '' false when :undef false else !!o end end # Utility method for TrueClass || FalseClass # @param x [Object] the object to test if it is instance of TrueClass or FalseClass def is_boolean? x x.is_a?(TrueClass) || x.is_a?(FalseClass) end def initialize @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2) end # Converts 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert(o, scope, undef_value) @@convert_visitor.visit_this_2(self, o, scope, undef_value) end def convert_NilClass(o, scope, undef_value) undef_value end def convert_Object(o, scope, undef_value) o end def convert_Array(o, scope, undef_value) o.map {|x| convert(x, scope, undef_value) } end def convert_Hash(o, scope, undef_value) result = {} o.each {|k,v| result[convert(k, scope, undef_value)] = convert(v, scope, undef_value) } result end def convert_Regexp(o, scope, undef_value) # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in # source form o.inspect end def convert_Symbol(o, scope, undef_value) case o when :undef undef_value # 3x wants :undef as empty string in function else o # :default, and all others are verbatim since they are new in future evaluator end end def convert_PAbstractType(o, scope, undef_value) o end def convert_PResourceType(o,scope, undef_value) # Needs conversion by calling scope to resolve the name and possibly return a different name # Resolution can only be called with an array, and returns an array. Here there is only one name type, titles = scope.resolve_type_and_titles(o.type_name, [o.title]) # Note: a title of nil makes Resource class throw error with information that is wrong Puppet::Resource.new(type, titles[0].nil? ? '' : titles[0] ) end def convert_PHostClassType(o, scope, undef_value) # Needs conversion by calling scope to resolve the name and possibly return a different name # Resolution can only be called with an array, and returns an array. Here there is only one name type, titles = scope.resolve_type_and_titles('class', [o.class_name]) # Note: a title of nil makes Resource class throw error with information that is wrong Puppet::Resource.new(type, titles[0].nil? ? '' : titles[0] ) end private # Produces an array with [type, title] from a PCatalogEntryType # Used to produce reference resource instances (used when 3x is operating on a resource). # def catalog_type_to_split_type_title(catalog_type) case catalog_type when Puppet::Pops::Types::PHostClassType return ['Class', catalog_type.class_name] when Puppet::Pops::Types::PResourceType return [catalog_type.type_name, catalog_type.title] else raise ArgumentError, "Cannot split the type #{catalog_type.class}, it is neither a PHostClassType, nor a PResourceClass." end end def extract_file_line(o) source_pos = Puppet::Pops::Utils.find_closest_positioned(o) return [nil, -1] unless source_pos [source_pos.locator.file, source_pos.line] end def find_closest_positioned(o) return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) end # Creates a diagnostic producer def diagnostic_producer Puppet::Pops::Validation::DiagnosticProducer.new( ExceptionRaisingAcceptor.new(), # Raises exception on all issues SeverityProducer.new(), # All issues are errors # Puppet::Pops::Validation::SeverityProducer.new(), # All issues are errors Puppet::Pops::Model::ModelLabelProvider.new()) end # Configure the severity of failures class SeverityProducer < Puppet::Pops::Validation::SeverityProducer Issues = Puppet::Pops::Issues def initialize super p = self - p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :warning + # Issues triggering warning only if --debug is on + if Puppet[:debug] + p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :warning + else + p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :ignore + end end end # An acceptor of diagnostics that immediately raises an exception. class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor def accept(diagnostic) super Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:", :emit_warnings => true }) if errors? raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end end class EvaluationError < StandardError end end diff --git a/spec/unit/pops/evaluator/access_ops_spec.rb b/spec/unit/pops/evaluator/access_ops_spec.rb index e0c9877eb..ab485057e 100644 --- a/spec/unit/pops/evaluator/access_ops_spec.rb +++ b/spec/unit/pops/evaluator/access_ops_spec.rb @@ -1,411 +1,435 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/pops/types/type_factory' # relative to this spec file (./) does not work as this file is loaded by rspec require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl/AccessOperator' do include EvaluatorRspecHelper def range(from, to) Puppet::Pops::Types::TypeFactory.range(from, to) end def float_range(from, to) Puppet::Pops::Types::TypeFactory.float_range(from, to) end context 'The evaluator when operating on a String' do it 'can get a single character using a single key index to []' do expect(evaluate(literal('abc')[1])).to eql('b') end it 'can get the last character using the key -1 in []' do expect(evaluate(literal('abc')[-1])).to eql('c') end it 'can get a substring by giving two keys' do expect(evaluate(literal('abcd')[1,2])).to eql('bc') # flattens keys expect(evaluate(literal('abcd')[[1,2]])).to eql('bc') end it 'produces empty string for a substring out of range' do expect(evaluate(literal('abc')[100])).to eql('') end it 'raises an error if arity is wrong for []' do expect{evaluate(literal('abc')[])}.to raise_error(/String supports \[\] with one or two arguments\. Got 0/) expect{evaluate(literal('abc')[1,2,3])}.to raise_error(/String supports \[\] with one or two arguments\. Got 3/) end end context 'The evaluator when operating on an Array' do it 'is tested with the correct assumptions' do expect(literal([1,2,3])[1].current.is_a?(Puppet::Pops::Model::AccessExpression)).to eql(true) end it 'can get an element using a single key index to []' do expect(evaluate(literal([1,2,3])[1])).to eql(2) end it 'can get the last element using the key -1 in []' do expect(evaluate(literal([1,2,3])[-1])).to eql(3) end it 'can get a slice of elements using two keys' do expect(evaluate(literal([1,2,3,4])[1,2])).to eql([2,3]) # flattens keys expect(evaluate(literal([1,2,3,4])[[1,2]])).to eql([2,3]) end it 'produces nil for a missing entry' do expect(evaluate(literal([1,2,3])[100])).to eql(nil) end it 'raises an error if arity is wrong for []' do expect{evaluate(literal([1,2,3,4])[])}.to raise_error(/Array supports \[\] with one or two arguments\. Got 0/) expect{evaluate(literal([1,2,3,4])[1,2,3])}.to raise_error(/Array supports \[\] with one or two arguments\. Got 3/) end end context 'The evaluator when operating on a Hash' do it 'can get a single element giving a single key to []' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3})['b'])).to eql(2) end it 'can lookup an array' do expect(evaluate(literal({[1]=>10,[2]=>20})[[2]])).to eql(20) end it 'produces nil for a missing key' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3})['x'])).to eql(nil) end it 'can get multiple elements by giving multiple keys to []' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['b', 'd'])).to eql([2, 4]) end it 'compacts the result when using multiple keys' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['b', 'x'])).to eql([2]) end it 'produces an empty array if none of multiple given keys were missing' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['x', 'y'])).to eql([]) end it 'raises an error if arity is wrong for []' do expect{evaluate(literal({'a'=>1,'b'=>2,'c'=>3})[])}.to raise_error(/Hash supports \[\] with one or more arguments\. Got 0/) end end context "When applied to a type it" do let(:types) { Puppet::Pops::Types::TypeFactory } # Integer # it 'produces an Integer[from, to]' do expr = fqr('Integer')[1, 3] expect(evaluate(expr)).to eql(range(1,3)) # arguments are flattened expr = fqr('Integer')[[1, 3]] expect(evaluate(expr)).to eql(range(1,3)) end it 'produces an Integer[1]' do expr = fqr('Integer')[1] expect(evaluate(expr)).to eql(range(1,1)) end it 'produces an Integer[from,