diff --git a/lib/puppet/pops/evaluator/collector_transformer.rb b/lib/puppet/pops/evaluator/collector_transformer.rb index 94319d3dc..70ad4f811 100644 --- a/lib/puppet/pops/evaluator/collector_transformer.rb +++ b/lib/puppet/pops/evaluator/collector_transformer.rb @@ -1,219 +1,209 @@ class Puppet::Pops::Evaluator::CollectorTransformer def initialize @@query_visitor ||= Puppet::Pops::Visitor.new(nil, "query", 1, 1) @@match_visitor ||= Puppet::Pops::Visitor.new(nil, "match", 1, 1) @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new() end def transform(o, scope) raise ArgumentError, "Expected CollectExpression" unless o.is_a? Puppet::Pops::Model::CollectExpression raise "LHS is not a type" unless o.type_expr.is_a? Puppet::Pops::Model::QualifiedReference type = o.type_expr.value().downcase() if type == 'class' fail "Classes cannot be collected" end resource_type = scope.find_resource_type(type) fail "Resource type #{type} doesn't exist" unless resource_type adapter = Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) line_num = adapter.line position = adapter.pos file_path = adapter.locator.file if !o.operations.empty? overrides = { - :parameters => o.operations.map{ |x| to_3x_param(x).evaluate(scope)}, + :parameters => o.operations.map{ |x| @@evaluator.evaluate(x, scope)}.flatten, :file => file_path, :line => [line_num, position], :source => scope.source, :scope => scope } end code = query_unless_nop(o.query, scope) case o.query when Puppet::Pops::Model::VirtualQuery newcoll = Puppet::Pops::Evaluator::Collectors::CatalogCollector.new(scope, resource_type.name, code, overrides) when Puppet::Pops::Model::ExportedQuery match = match_unless_nop(o.query, scope) newcoll = Puppet::Pops::Evaluator::Collectors::ExportedCollector.new(scope, resource_type.name, match, code, overrides) end scope.compiler.add_collection(newcoll) newcoll end protected def query(o, scope) @@query_visitor.visit_this_1(self, o, scope) end def match(o, scope) @@match_visitor.visit_this_1(self, o, scope) end def query_unless_nop(query, scope) unless query.expr.nil? || query.expr.is_a?(Puppet::Pops::Model::Nop) query(query.expr, scope) end end def match_unless_nop(query, scope) unless query.expr.nil? || query.expr.is_a?(Puppet::Pops::Model::Nop) match(query.expr, scope) end end def query_AndExpression(o, scope) left_code = query(o.left_expr, scope) right_code = query(o.right_expr, scope) proc do |resource| left_code.call(resource) && right_code.call(resource) end end def query_OrExpression(o, scope) left_code = query(o.left_expr, scope) right_code = query(o.right_expr, scope) proc do |resource| left_code.call(resource) || right_code.call(resource) end end def query_ComparisonExpression(o, scope) left_code = query(o.left_expr, scope) right_code = query(o.right_expr, scope) case o.operator when :'==' if left_code == "tag" # Ensure that to_s and downcase is done once, i.e. outside the proc block and # then use raw_tagged? instead of tagged? if right_code.is_a?(Array) tags = right_code else tags = [ right_code ] end tags = tags.collect do |t| raise ArgumentError, 'Cannot transform a number to a tag' if t.is_a?(Numeric) t.to_s.downcase end proc do |resource| resource.raw_tagged?(tags) end else proc do |resource| if (tmp = resource[left_code]).is_a?(Array) @@compare_operator.include?(tmp, right_code, scope) else @@compare_operator.equals(tmp, right_code) end end end when :'!=' proc do |resource| !@@compare_operator.equals(resource[left_code], right_code) end end end def query_VariableExpression(o, scope) @@evaluator.evaluate(o, scope) end def query_LiteralBoolean(o, scope) @@evaluator.evaluate(o, scope) end def query_LiteralString(o, scope) @@evaluator.evaluate(o, scope) end def query_ConcatenatedString(o, scope) @@evaluator.evaluate(o, scope) end def query_LiteralNumber(o, scope) @@evaluator.evaluate(o, scope) end def query_QualifiedName(o, scope) @@evaluator.evaluate(o, scope) end def query_ParenthesizedExpression(o, scope) query(o.expr, scope) end def query_Object(o, scope) raise ArgumentError, "Cannot transform object of class #{o.class}" end def match_AndExpression(o, scope) left_match = match(o.left_expr, scope) right_match = match(o.right_expr, scope) return [left_match, 'and', right_match] end def match_OrExpression(o, scope) left_match = match(o.left_expr, scope) right_match = match(o.right_expr, scope) return [left_match, 'or', right_match] end def match_ComparisonExpression(o, scope) left_match = match(o.left_expr, scope) right_match = match(o.right_expr, scope) return [left_match, o.operator.to_s, right_match] end def match_VariableExpression(o, scope) @@evaluator.evaluate(o, scope) end def match_LiteralBoolean(o, scope) @@evaluator.evaluate(o, scope) end def match_LiteralString(o, scope) @@evaluator.evaluate(o, scope) end def match_ConcatenatedString(o, scope) @@evaluator.evaluate(o, scope) end def match_LiteralNumber(o, scope) @@evaluator.evaluate(o, scope) end def match_QualifiedName(o, scope) @@evaluator.evaluate(o, scope) end def match_ParenthesizedExpression(o, scope) match(o.expr, scope) end def match_Object(o, scope) raise ArgumentError, "Cannot transform object of class #{o.class}" end - - # Produces (name => expr) or (name +> expr) - def to_3x_param(o) - bridge = Puppet::Parser::AST::PopsBridge::Expression.new(:value => o.value_expr) - args = { :value => bridge } - args[:add] = true if o.operator == :'+>' - args[:param] = o.attribute_name - args= Puppet::Pops::Model::AstTransformer.new().merge_location(args, o) - Puppet::Parser::AST::ResourceParam.new(args) - end end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index 48ca19280..9b60516ac 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,489 +1,490 @@ # 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 NAME_SPACE_SEPARATOR = '::'.freeze # 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) { x = scope.lookupvar(name.to_s) # Must convert :undef back to nil - this can happen when an undefined variable is used in a # parameter's default value expression - there nil must be :undef to work with the rest of 3x. # Now that the value comes back to 4x it is changed to nil. return (x == :undef) ? nil : x } # 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, 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. # # 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) || source.is_a?(Puppet::Pops::Evaluator::Collectors::AbstractCollector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = Puppet::Pops::Evaluator::Runtime3Converter.instance.catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) || target.is_a?(Puppet::Pops::Evaluator::Collectors::AbstractCollector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = Puppet::Pops::Evaluator::Runtime3Converter.instance.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 def call_function(name, args, o, scope, &block) # Call via 4x API if the function exists there loaders = scope.compiler.loaders # find the loader that loaded the code, or use the private_environment_loader (sees env + all modules) adapter = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::LoaderAdapter) loader = adapter.nil? ? loaders.private_environment_loader : adapter.loader if loader && func = loader.load(:function, name) return func.call(scope, *args, &block) end # Call via 3x API if function exists there fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # NOTE: Passing an empty string last converts nil/:undef to empty string mapped_args = Puppet::Pops::Evaluator::Runtime3Converter.map_args(args, scope, '') result = scope.send("function_#{name}", mapped_args, &block) # Prevent non r-value functions from leaking their result (they are not written to care about this) Puppet::Parser::Functions.rvalue?(name) ? result : nil 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, nil), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end def convert(value, scope, undef_value) Puppet::Pops::Evaluator::Runtime3Converter.convert(value, scope, undef_value) end CLASS_STRING = 'class'.freeze 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_STRING # 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(capitalize_qualified_name(type_name), evaluated_parameters) + scope.define_settings(capitalize_qualified_name(type_name), evaluated_parameters.flatten) end # Capitalizes each segment of a qualified name # def capitalize_qualified_name(name) name.split(/::/).map(&:capitalize).join(NAME_SPACE_SEPARATOR) 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) - + # A *=> results in an array of arrays + evaluated_parameters = evaluated_parameters.flatten evaluated_resources.each do |r| unless r.is_a?(Puppet::Pops::Types::PResourceType) && r.type_name != 'class' fail(Puppet::Pops::Issues::ILLEGAL_OVERRIDEN_TYPE, o, {:actual => r} ) end 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 taken place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) # This gets the parameter value, or nil (for both valid parameters and parameters that do not exist). val = resource[parameter_name] # Sometimes the resource is a Puppet::Parser::Resource and sometimes it is # a Puppet::Resource. The Puppet::Resource case occurs when puppet language # is evaluated against an already completed catalog (where all instances of # Puppet::Parser::Resource are converted to Puppet::Resource instances). # Evaluating against an already completed catalog is really only found in # the language specification tests, where the puppet language is used to # test itself. if resource.is_a?(Puppet::Parser::Resource) # The defaults must be looked up in the scope where the resource was created (not in the given # scope where the lookup takes place. resource_scope = resource.scope if val.nil? && resource_scope && defaults = resource_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. # NOTE: meta parameters that are unset ends up here, and there are no defaults for those encoded # in the defaults, they may receive hardcoded defaults later (e.g. 'tag'). param = defaults[parameter_name.to_sym] # Some parameters (meta parameters like 'tag') does not return a param from which the value can be obtained # at all times. Instead, they return a nil param until a value has been set. val = param.nil? ? nil : param.value end end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) return false unless name.is_a?(String) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? # inference returns the meta type since the 3x Resource is an alternate way to describe a type type_calculator.infer(resource).type end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true?(value, o) # Is the value true? This allows us to control the definition of truth # in one place. case value # Support :undef since it may come from a 3x structure when :undef false when String @migration_checker.report_empty_string_true(value, o) true else !!value 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 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::Model::ModelLabelProvider.new()) end # Configure the severity of failures class SeverityProducer < Puppet::Pops::Validation::SeverityProducer Issues = Puppet::Pops::Issues def initialize super p = self # 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 # Store config issues, ignore or warning p[Issues::RT_NO_STORECONFIGS_EXPORT] = Puppet[:storeconfigs] ? :ignore : :warning p[Issues::RT_NO_STORECONFIGS] = Puppet[:storeconfigs] ? :ignore : :warning 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, # log warnings :exception_class => Puppet::PreformattedError }) 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/lib/puppet/pops/model/model_meta.rb b/lib/puppet/pops/model/model_meta.rb index 246216aa9..958f89b8a 100644 --- a/lib/puppet/pops/model/model_meta.rb +++ b/lib/puppet/pops/model/model_meta.rb @@ -1,576 +1,576 @@ # # The Puppet Pops Metamodel # # This module contains a formal description of the Puppet Pops (*P*uppet *OP*eration instruction*S*). # It describes a Metamodel containing DSL instructions, a description of PuppetType and related # classes needed to evaluate puppet logic. # The metamodel resembles the existing AST model, but it is a semantic model of instructions and # the types that they operate on rather than an Abstract Syntax Tree, although closely related. # # The metamodel is anemic (has no behavior) except basic datatype and type # assertions and reference/containment assertions. # The metamodel is also a generalized description of the Puppet DSL to enable the # same metamodel to be used to express Puppet DSL models (instances) with different semantics as # the language evolves. # # The metamodel is concretized by a validator for a particular version of # the Puppet DSL language. # # This metamodel is expressed using RGen. # require 'rgen/metamodel_builder' module Puppet::Pops::Model extend RGen::MetamodelBuilder::ModuleExtension # A base class for modeled objects that makes them Visitable, and Adaptable. # class PopsObject < RGen::MetamodelBuilder::MMBase abstract end # A Positioned object has an offset measured in an opaque unit (representing characters) from the start # of a source text (starting # from 0), and a length measured in the same opaque unit. The resolution of the opaque unit requires the # aid of a Locator instance that knows about the measure. This information is stored in the model's # root node - a Program. # # The offset and length are optional if the source of the model is not from parsed text. # class Positioned < PopsObject abstract has_attr 'offset', Integer has_attr 'length', Integer end # @abstract base class for expressions class Expression < Positioned abstract end # A Nop - the "no op" expression. # @note not really needed since the evaluator can evaluate nil with the meaning of NoOp # @todo deprecate? May be useful if there is the need to differentiate between nil and Nop when transforming model. # class Nop < Expression end # A binary expression is abstract and has a left and a right expression. The order of evaluation # and semantics are determined by the concrete subclass. # class BinaryExpression < Expression abstract # # @!attribute [rw] left_expr # @return [Expression] contains_one_uni 'left_expr', Expression, :lowerBound => 1 contains_one_uni 'right_expr', Expression, :lowerBound => 1 end # An unary expression is abstract and contains one expression. The semantics are determined by # a concrete subclass. # class UnaryExpression < Expression abstract contains_one_uni 'expr', Expression, :lowerBound => 1 end # A class that simply evaluates to the contained expression. # It is of value in order to preserve user entered parentheses in transformations, and # transformations from model to source. # class ParenthesizedExpression < UnaryExpression; end # A boolean not expression, reversing the truth of the unary expr. # class NotExpression < UnaryExpression; end # An arithmetic expression reversing the polarity of the numeric unary expr. # class UnaryMinusExpression < UnaryExpression; end # Unfolds an array (a.k.a 'splat') class UnfoldExpression < UnaryExpression; end OpAssignment = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'=', :'+=', :'-='], :name => 'OpAssignment') # An assignment expression assigns a value to the lval() of the left_expr. # class AssignmentExpression < BinaryExpression has_attr 'operator', OpAssignment, :lowerBound => 1 end OpArithmetic = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'+', :'-', :'*', :'%', :'/', :'<<', :'>>' ], :name => 'OpArithmetic') # An arithmetic expression applies an arithmetic operator on left and right expressions. # class ArithmeticExpression < BinaryExpression has_attr 'operator', OpArithmetic, :lowerBound => 1 end OpRelationship = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'->', :'<-', :'~>', :'<~'], :name => 'OpRelationship') # A relationship expression associates the left and right expressions # class RelationshipExpression < BinaryExpression has_attr 'operator', OpRelationship, :lowerBound => 1 end # A binary expression, that accesses the value denoted by right in left. i.e. typically # expressed concretely in a language as left[right]. # class AccessExpression < Expression contains_one_uni 'left_expr', Expression, :lowerBound => 1 contains_many_uni 'keys', Expression, :lowerBound => 1 end OpComparison = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'==', :'!=', :'<', :'>', :'<=', :'>=' ], :name => 'OpComparison') # A comparison expression compares left and right using a comparison operator. # class ComparisonExpression < BinaryExpression has_attr 'operator', OpComparison, :lowerBound => 1 end OpMatch = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'!~', :'=~'], :name => 'OpMatch') # A match expression matches left and right using a matching operator. # class MatchExpression < BinaryExpression has_attr 'operator', OpMatch, :lowerBound => 1 end # An 'in' expression checks if left is 'in' right # class InExpression < BinaryExpression; end # A boolean expression applies a logical connective operator (and, or) to left and right expressions. # class BooleanExpression < BinaryExpression abstract end # An and expression applies the logical connective operator and to left and right expression # and does not evaluate the right expression if the left expression is false. # class AndExpression < BooleanExpression; end # An or expression applies the logical connective operator or to the left and right expression # and does not evaluate the right expression if the left expression is true # class OrExpression < BooleanExpression; end # A literal list / array containing 0:M expressions. # class LiteralList < Expression contains_many_uni 'values', Expression end # A Keyed entry has a key and a value expression. It is typically used as an entry in a Hash. # class KeyedEntry < Positioned contains_one_uni 'key', Expression, :lowerBound => 1 contains_one_uni 'value', Expression, :lowerBound => 1 end # A literal hash is a collection of KeyedEntry objects # class LiteralHash < Expression contains_many_uni 'entries', KeyedEntry end # A block contains a list of expressions # class BlockExpression < Expression contains_many_uni 'statements', Expression end # A case option entry in a CaseStatement # class CaseOption < Expression contains_many_uni 'values', Expression, :lowerBound => 1 contains_one_uni 'then_expr', Expression, :lowerBound => 1 end # A case expression has a test, a list of options (multi values => block map). # One CaseOption may contain a LiteralDefault as value. This option will be picked if nothing # else matched. # class CaseExpression < Expression contains_one_uni 'test', Expression, :lowerBound => 1 contains_many_uni 'options', CaseOption end # A query expression is an expression that is applied to some collection. # The contained optional expression may contain different types of relational expressions depending # on what the query is applied to. # class QueryExpression < Expression abstract contains_one_uni 'expr', Expression, :lowerBound => 0 end # An exported query is a special form of query that searches for exported objects. # class ExportedQuery < QueryExpression end # A virtual query is a special form of query that searches for virtual objects. # class VirtualQuery < QueryExpression end OpAttribute = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'=>', :'+>', ], :name => 'OpAttribute') class AbstractAttributeOperation < Positioned end # An attribute operation sets or appends a value to a named attribute. # class AttributeOperation < AbstractAttributeOperation has_attr 'attribute_name', String, :lowerBound => 1 has_attr 'operator', OpAttribute, :lowerBound => 1 contains_one_uni 'value_expr', Expression, :lowerBound => 1 end # An attribute operation containing an expression that must evaluate to a Hash # class AttributesOperation < AbstractAttributeOperation contains_one_uni 'expr', Expression, :lowerBound => 1 end # An object that collects stored objects from the central cache and returns # them to the current host. Operations may optionally be applied. # class CollectExpression < Expression contains_one_uni 'type_expr', Expression, :lowerBound => 1 contains_one_uni 'query', QueryExpression, :lowerBound => 1 - contains_many_uni 'operations', AttributeOperation + contains_many_uni 'operations', AbstractAttributeOperation end class Parameter < Positioned has_attr 'name', String, :lowerBound => 1 contains_one_uni 'value', Expression contains_one_uni 'type_expr', Expression, :lowerBound => 0 has_attr 'captures_rest', Boolean end # Abstract base class for definitions. # class Definition < Expression abstract end # Abstract base class for named and parameterized definitions. class NamedDefinition < Definition abstract has_attr 'name', String, :lowerBound => 1 contains_many_uni 'parameters', Parameter contains_one_uni 'body', Expression end # A resource type definition (a 'define' in the DSL). # class ResourceTypeDefinition < NamedDefinition end # A node definition matches hosts using Strings, or Regular expressions. It may inherit from # a parent node (also using a String or Regular expression). # class NodeDefinition < Definition contains_one_uni 'parent', Expression contains_many_uni 'host_matches', Expression, :lowerBound => 1 contains_one_uni 'body', Expression end class LocatableExpression < Expression has_many_attr 'line_offsets', Integer has_attr 'locator', Object, :lowerBound => 1, :transient => true end # Contains one expression which has offsets reported virtually (offset against the Program's # overall locator). # class SubLocatedExpression < Expression contains_one_uni 'expr', Expression, :lowerBound => 1 # line offset index for contained expressions has_many_attr 'line_offsets', Integer # Number of preceding lines (before the line_offsets) has_attr 'leading_line_count', Integer # The offset of the leading source line (i.e. size of "left margin"). has_attr 'leading_line_offset', Integer # The locator for the sub-locatable's children (not for the sublocator itself) # The locator is not serialized and is recreated on demand from the indexing information # in self. # has_attr 'locator', Object, :lowerBound => 1, :transient => true end # A heredoc is a wrapper around a LiteralString or a ConcatenatedStringExpression with a specification # of syntax. The expectation is that "syntax" has meaning to a validator. A syntax of nil or '' means # "unspecified syntax". # class HeredocExpression < Expression has_attr 'syntax', String contains_one_uni 'text_expr', Expression, :lowerBound => 1 end # A class definition # class HostClassDefinition < NamedDefinition has_attr 'parent_class', String end # i.e {|parameters| body } class LambdaExpression < Expression contains_many_uni 'parameters', Parameter contains_one_uni 'body', Expression end # If expression. If test is true, the then_expr part should be evaluated, else the (optional) # else_expr. An 'elsif' is simply an else_expr = IfExpression, and 'else' is simply else == Block. # a 'then' is typically a Block. # class IfExpression < Expression contains_one_uni 'test', Expression, :lowerBound => 1 contains_one_uni 'then_expr', Expression, :lowerBound => 1 contains_one_uni 'else_expr', Expression end # An if expression with boolean reversed test. # class UnlessExpression < IfExpression end # An abstract call. # class CallExpression < Expression abstract # A bit of a crutch; functions are either procedures (void return) or has an rvalue # this flag tells the evaluator that it is a failure to call a function that is void/procedure # where a value is expected. # has_attr 'rval_required', Boolean, :defaultValueLiteral => "false" contains_one_uni 'functor_expr', Expression, :lowerBound => 1 contains_many_uni 'arguments', Expression contains_one_uni 'lambda', Expression end # A function call where the functor_expr should evaluate to something callable. # class CallFunctionExpression < CallExpression; end # A function call where the given functor_expr should evaluate to the name # of a function. # class CallNamedFunctionExpression < CallExpression; end # A method/function call where the function expr is a NamedAccess and with support for # an optional lambda block # class CallMethodExpression < CallExpression end # Abstract base class for literals. # class Literal < Expression abstract end # A literal value is an abstract value holder. The type of the contained value is # determined by the concrete subclass. # class LiteralValue < Literal abstract end # A Regular Expression Literal. # class LiteralRegularExpression < LiteralValue has_attr 'value', Object, :lowerBound => 1, :transient => true has_attr 'pattern', String, :lowerBound => 1 end # A Literal String # class LiteralString < LiteralValue has_attr 'value', String, :lowerBound => 1 end class LiteralNumber < LiteralValue abstract end # A literal number has a radix of decimal (10), octal (8), or hex (16) to enable string conversion with the input radix. # By default, a radix of 10 is used. # class LiteralInteger < LiteralNumber has_attr 'radix', Integer, :lowerBound => 1, :defaultValueLiteral => "10" has_attr 'value', Integer, :lowerBound => 1 end class LiteralFloat < LiteralNumber has_attr 'value', Float, :lowerBound => 1 end # The DSL `undef`. # class LiteralUndef < Literal; end # The DSL `default` class LiteralDefault < Literal; end # DSL `true` or `false` class LiteralBoolean < LiteralValue has_attr 'value', Boolean, :lowerBound => 1 end # A text expression is an interpolation of an expression. If the embedded expression is # a QualifiedName, it is taken as a variable name and resolved. All other expressions are evaluated. # The result is transformed to a string. # class TextExpression < UnaryExpression; end # An interpolated/concatenated string. The contained segments are expressions. Verbatim sections # should be LiteralString instances, and interpolated expressions should either be # TextExpression instances (if QualifiedNames should be turned into variables), or any other expression # if such treatment is not needed. # class ConcatenatedString < Expression contains_many_uni 'segments', Expression end # A DSL NAME (one or multiple parts separated by '::'). # class QualifiedName < LiteralValue has_attr 'value', String, :lowerBound => 1 end # Represents a parsed reserved word class ReservedWord < LiteralValue has_attr 'word', String, :lowerBound => 1 end # A DSL CLASSREF (one or multiple parts separated by '::' where (at least) the first part starts with an upper case letter). # class QualifiedReference < LiteralValue has_attr 'value', String, :lowerBound => 1 end # A Variable expression looks up value of expr (some kind of name) in scope. # The expression is typically a QualifiedName, or QualifiedReference. # class VariableExpression < UnaryExpression; end # Epp start class EppExpression < Expression # EPP can be specified without giving any parameter specification. # However, the parameters of the lambda in that case are the empty # array, which is the same as when the parameters are explicity # specified as empty. This attribute tracks that difference. has_attr 'parameters_specified', Boolean contains_one_uni 'body', Expression end # A string to render class RenderStringExpression < LiteralString end # An expression to evluate and render class RenderExpression < UnaryExpression end # A resource body describes one resource instance # class ResourceBody < Positioned contains_one_uni 'title', Expression contains_many_uni 'operations', AbstractAttributeOperation end ResourceFormEnum = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:regular, :virtual, :exported ], :name => 'ResourceFormEnum') # An abstract resource describes the form of the resource (regular, virtual or exported) # and adds convenience methods to ask if it is virtual or exported. # All derived classes may not support all forms, and these needs to be validated # class AbstractResource < Expression abstract has_attr 'form', ResourceFormEnum, :lowerBound => 1, :defaultValueLiteral => "regular" has_attr 'virtual', Boolean, :derived => true has_attr 'exported', Boolean, :derived => true end # A resource expression is used to instantiate one or many resource. Resources may optionally # be virtual or exported, an exported resource is always virtual. # class ResourceExpression < AbstractResource contains_one_uni 'type_name', Expression, :lowerBound => 1 contains_many_uni 'bodies', ResourceBody end # A resource defaults sets defaults for a resource type. This class inherits from AbstractResource # but does only support the :regular form (this is intentional to be able to produce better error messages # when illegal forms are applied to a model. # class ResourceDefaultsExpression < AbstractResource contains_one_uni 'type_ref', Expression contains_many_uni 'operations', AbstractAttributeOperation end # A resource override overrides already set values. # class ResourceOverrideExpression < AbstractResource contains_one_uni 'resources', Expression, :lowerBound => 1 contains_many_uni 'operations', AbstractAttributeOperation end # A selector entry describes a map from matching_expr to value_expr. # class SelectorEntry < Positioned contains_one_uni 'matching_expr', Expression, :lowerBound => 1 contains_one_uni 'value_expr', Expression, :lowerBound => 1 end # A selector expression represents a mapping from a left_expr to a matching SelectorEntry. # class SelectorExpression < Expression contains_one_uni 'left_expr', Expression, :lowerBound => 1 contains_many_uni 'selectors', SelectorEntry end # A named access expression looks up a named part. (e.g. $a.b) # class NamedAccessExpression < BinaryExpression; end # A Program is the top level construct returned by the parser # it contains the parsed result in the body, and has a reference to the full source text, # and its origin. The line_offset's is an array with the start offset of each line. # class Program < PopsObject contains_one_uni 'body', Expression has_many 'definitions', Definition has_attr 'source_text', String has_attr 'source_ref', String has_many_attr 'line_offsets', Integer has_attr 'locator', Object, :lowerBound => 1, :transient => true end end diff --git a/lib/puppet/pops/validation/checker4_0.rb b/lib/puppet/pops/validation/checker4_0.rb index 618ac5a0e..136a2e99a 100644 --- a/lib/puppet/pops/validation/checker4_0.rb +++ b/lib/puppet/pops/validation/checker4_0.rb @@ -1,776 +1,782 @@ # A Validator validates a model. # # Validation is performed on each model element in isolation. Each method should validate the model element's state # but not validate its referenced/contained elements except to check their validity in their respective role. # The intent is to drive the validation with a tree iterator that visits all elements in a model. # # # TODO: Add validation of multiplicities - this is a general validation that can be checked for all # Model objects via their metamodel. (I.e an extra call to multiplicity check in polymorph check). # This is however mostly valuable when validating model to model transformations, and is therefore T.B.D # class Puppet::Pops::Validation::Checker4_0 Issues = Puppet::Pops::Issues Model = Puppet::Pops::Model attr_reader :acceptor attr_reader :migration_checker # Initializes the validator with a diagnostics producer. This object must respond to # `:will_accept?` and `:accept`. # def initialize(diagnostics_producer) @@check_visitor ||= Puppet::Pops::Visitor.new(nil, "check", 0, 0) @@rvalue_visitor ||= Puppet::Pops::Visitor.new(nil, "rvalue", 0, 0) @@hostname_visitor ||= Puppet::Pops::Visitor.new(nil, "hostname", 1, 2) @@assignment_visitor ||= Puppet::Pops::Visitor.new(nil, "assign", 0, 1) @@query_visitor ||= Puppet::Pops::Visitor.new(nil, "query", 0, 0) @@top_visitor ||= Puppet::Pops::Visitor.new(nil, "top", 1, 1) @@relation_visitor ||= Puppet::Pops::Visitor.new(nil, "relation", 0, 0) @@idem_visitor ||= Puppet::Pops::Visitor.new(self, "idem", 0, 0) @acceptor = diagnostics_producer # Use null migration checker unless given in context @migration_checker ||= (Puppet.lookup(:migration_checker) { Puppet::Pops::Migration::MigrationChecker.new() }) end # Validates the entire model by visiting each model element and calling `check`. # The result is collected (or acted on immediately) by the configured diagnostic provider/acceptor # given when creating this Checker. # def validate(model) # tree iterate the model, and call check for each element check(model) model.eAllContents.each {|m| check(m) } end # Performs regular validity check def check(o) @@check_visitor.visit_this_0(self, o) end # Performs check if this is a vaid hostname expression # @param single_feature_name [String, nil] the name of a single valued hostname feature of the value's container. e.g. 'parent' def hostname(o, semantic, single_feature_name = nil) @@hostname_visitor.visit_this_2(self, o, semantic, single_feature_name) end # Performs check if this is valid as a query def query(o) @@query_visitor.visit_this_0(self, o) end # Performs check if this is valid as a relationship side def relation(o) @@relation_visitor.visit_this_0(self, o) end # Performs check if this is valid as a rvalue def rvalue(o) @@rvalue_visitor.visit_this_0(self, o) end # Performs check if this is valid as a container of a definition (class, define, node) def top(o, definition) @@top_visitor.visit_this_1(self, o, definition) end # Checks the LHS of an assignment (is it assignable?). # If args[0] is true, assignment via index is checked. # def assign(o, via_index = false) @@assignment_visitor.visit_this_1(self, o, via_index) end # Checks if the expression has side effect ('idem' is latin for 'the same', here meaning that the evaluation state # is known to be unchanged after the expression has been evaluated). The result is not 100% authoritative for # negative answers since analysis of function behavior is not possible. # @return [Boolean] true if expression is known to have no effect on evaluation state # def idem(o) @@idem_visitor.visit_this_0(self, o) end # Returns the last expression in a block, or the expression, if that expression is idem def ends_with_idem(o) if o.is_a?(Puppet::Pops::Model::BlockExpression) last = o.statements[-1] idem(last) ? last : nil else idem(o) ? o : nil end end #---ASSIGNMENT CHECKS def assign_VariableExpression(o, via_index) varname_string = varname_to_s(o.expr) if varname_string =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME acceptor.accept(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o, :varname => varname_string) end # Can not assign to something in another namespace (i.e. a '::' in the name is not legal) if acceptor.will_accept? Issues::CROSS_SCOPE_ASSIGNMENT if varname_string =~ /::/ acceptor.accept(Issues::CROSS_SCOPE_ASSIGNMENT, o, :name => varname_string) end end # TODO: Could scan for reassignment of the same variable if done earlier in the same container # Or if assigning to a parameter (more work). # TODO: Investigate if there are invalid cases for += assignment end def assign_AccessExpression(o, via_index) # Are indexed assignments allowed at all ? $x[x] = '...' if acceptor.will_accept? Issues::ILLEGAL_INDEXED_ASSIGNMENT acceptor.accept(Issues::ILLEGAL_INDEXED_ASSIGNMENT, o) else # Then the left expression must be assignable-via-index assign(o.left_expr, true) end end def assign_Object(o, via_index) # Can not assign to anything else (differentiate if this is via index or not) # i.e. 10 = 'hello' vs. 10['x'] = 'hello' (the root is reported as being in error in both cases) # acceptor.accept(via_index ? Issues::ILLEGAL_ASSIGNMENT_VIA_INDEX : Issues::ILLEGAL_ASSIGNMENT, o) end #---CHECKS def check_Object(o) end def check_Factory(o) check(o.current) end def check_AccessExpression(o) # Only min range is checked, all other checks are RT checks as they depend on the resulting type # of the LHS. if o.keys.size < 1 acceptor.accept(Issues::MISSING_INDEX, o) end end def check_AssignmentExpression(o) case o.operator when :'=' assign(o.left_expr) rvalue(o.right_expr) when :'+=', :'-=' acceptor.accept(Issues::APPENDS_DELETES_NO_LONGER_SUPPORTED, o, {:operator => o.operator}) else acceptor.accept(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end end # Checks that operation with :+> is contained in a ResourceOverride or Collector. # # Parent of an AttributeOperation can be one of: # * CollectExpression # * ResourceOverride # * ResourceBody (ILLEGAL this is a regular resource expression) # * ResourceDefaults (ILLEGAL) # def check_AttributeOperation(o) if o.operator == :'+>' # Append operator use is constrained parent = o.eContainer unless parent.is_a?(Model::CollectExpression) || parent.is_a?(Model::ResourceOverrideExpression) acceptor.accept(Issues::ILLEGAL_ATTRIBUTE_APPEND, o, {:name=>o.attribute_name, :parent=>parent}) end end rvalue(o.value_expr) end def check_AttributesOperation(o) # Append operator use is constrained - parent = o.eContainer - parent = parent.eContainer unless parent.nil? - unless parent.is_a?(Model::ResourceExpression) - acceptor.accept(Issues::UNSUPPORTED_OPERATOR_IN_CONTEXT, o, :operator=>'* =>') + parent1 = o.eContainer + case parent1 + when Model::AbstractResource + when Model::CollectExpression + else + # protect against just testing a snippet that has no parent, error message will be a bit strange + # but it is not for a real program. + parent2 = parent1.nil? ? o : parent1.eContainer + unless parent2.is_a?(Model::AbstractResource) + acceptor.accept(Issues::UNSUPPORTED_OPERATOR_IN_CONTEXT, parent2, :operator=>'* =>') + end end - rvalue(o.expr) end def check_BinaryExpression(o) rvalue(o.left_expr) rvalue(o.right_expr) end def check_BlockExpression(o) o.statements[0..-2].each do |statement| if idem(statement) acceptor.accept(Issues::IDEM_EXPRESSION_NOT_LAST, statement) break # only flag the first end end migration_checker.report_array_last_in_block(o.statements[-1]) end def check_CallNamedFunctionExpression(o) case o.functor_expr when Puppet::Pops::Model::QualifiedName # ok nil when Puppet::Pops::Model::RenderStringExpression # helpful to point out this easy to make Epp error acceptor.accept(Issues::ILLEGAL_EPP_PARAMETERS, o) else acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end end def check_EppExpression(o) if o.eContainer.is_a?(Puppet::Pops::Model::LambdaExpression) internal_check_no_capture(o.eContainer, o) end end def check_MethodCallExpression(o) unless o.functor_expr.is_a? Model::QualifiedName acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o) end end def check_CaseExpression(o) rvalue(o.test) # There should only be one LiteralDefault case option value # TODO: Implement this check end def check_CaseOption(o) o.values.each { |v| rvalue(v) } end def check_CollectExpression(o) unless o.type_expr.is_a? Model::QualifiedReference acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_expr, :feature=> 'type name', :container => o) end # If a collect expression tries to collect exported resources and storeconfigs is not on # then it will not work... This was checked in the parser previously. This is a runtime checking # thing as opposed to a language thing. if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.query.is_a?(Model::ExportedQuery) acceptor.accept(Issues::RT_NO_STORECONFIGS, o) end end # Only used for function names, grammar should not be able to produce something faulty, but # check anyway if model is created programatically (it will fail in transformation to AST for sure). def check_NamedAccessExpression(o) name = o.right_expr unless name.is_a? Model::QualifiedName acceptor.accept(Issues::ILLEGAL_EXPRESSION, name, :feature=> 'function name', :container => o.eContainer) end end RESERVED_TYPE_NAMES = { 'type' => true, 'any' => true, 'unit' => true, 'scalar' => true, 'boolean' => true, 'numeric' => true, 'integer' => true, 'float' => true, 'collection' => true, 'array' => true, 'hash' => true, 'tuple' => true, 'struct' => true, 'variant' => true, 'optional' => true, 'enum' => true, 'regexp' => true, 'pattern' => true, 'runtime' => true, } # for 'class', 'define', and function def check_NamedDefinition(o) top(o.eContainer, o) if o.name !~ Puppet::Pops::Patterns::CLASSREF acceptor.accept(Issues::ILLEGAL_DEFINITION_NAME, o, {:name=>o.name}) end if RESERVED_TYPE_NAMES[o.name()] acceptor.accept(Issues::RESERVED_TYPE_NAME, o, {:name => o.name}) end if violator = ends_with_idem(o.body) acceptor.accept(Issues::IDEM_NOT_ALLOWED_LAST, violator, {:container => o}) end end def check_HostClassDefinition(o) check_NamedDefinition(o) internal_check_no_capture(o) internal_check_reserved_params(o) end def check_ResourceTypeDefinition(o) check_NamedDefinition(o) internal_check_no_capture(o) internal_check_reserved_params(o) end def internal_check_capture_last(o) accepted_index = o.parameters.size() -1 o.parameters.each_with_index do |p, index| if p.captures_rest && index != accepted_index acceptor.accept(Issues::CAPTURES_REST_NOT_LAST, p, {:param_name => p.name}) end end end def internal_check_no_capture(o, container = o) o.parameters.each do |p| if p.captures_rest acceptor.accept(Issues::CAPTURES_REST_NOT_SUPPORTED, p, {:container => container, :param_name => p.name}) end end end RESERVED_PARAMETERS = { 'name' => true, 'title' => true, } def internal_check_reserved_params(o) o.parameters.each do |p| if RESERVED_PARAMETERS[p.name] acceptor.accept(Issues::RESERVED_PARAMETER, p, {:container => o, :param_name => p.name}) end end end def check_IfExpression(o) rvalue(o.test) end def check_KeyedEntry(o) rvalue(o.key) rvalue(o.value) # In case there are additional things to forbid than non-rvalues # acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.key, :feature => 'hash key', :container => o.eContainer) end def check_LambdaExpression(o) internal_check_capture_last(o) end def check_LiteralList(o) o.values.each {|v| rvalue(v) } end def check_LiteralFloat(o) migration_checker.report_ambiguous_float(o) end def check_LiteralInteger(o) migration_checker.report_ambiguous_integer(o) end def check_NodeDefinition(o) # Check that hostnames are valid hostnames (or regular expressions) hostname(o.host_matches, o) hostname(o.parent, o, 'parent') unless o.parent.nil? top(o.eContainer, o) if violator = ends_with_idem(o.body) acceptor.accept(Issues::IDEM_NOT_ALLOWED_LAST, violator, {:container => o}) end unless o.parent.nil? acceptor.accept(Issues::ILLEGAL_NODE_INHERITANCE, o.parent) end end # No checking takes place - all expressions using a QualifiedName need to check. This because the # rules are slightly different depending on the container (A variable allows a numeric start, but not # other names). This means that (if the lexer/parser so chooses) a QualifiedName # can be anything when it represents a Bare Word and evaluates to a String. # def check_QualifiedName(o) end # Checks that the value is a valid UpperCaseWord (a CLASSREF), and optionally if it contains a hypen. # DOH: QualifiedReferences are created with LOWER CASE NAMES at parse time def check_QualifiedReference(o) # Is this a valid qualified name? if o.value !~ Puppet::Pops::Patterns::CLASSREF acceptor.accept(Issues::ILLEGAL_CLASSREF, o, {:name=>o.value}) end end def check_QueryExpression(o) query(o.expr) if o.expr # is optional end def relation_Object(o) rvalue(o) end def relation_CollectExpression(o); end def relation_RelationshipExpression(o); end def check_Parameter(o) if o.name =~ /^(?:0x)?[0-9]+$/ acceptor.accept(Issues::ILLEGAL_NUMERIC_PARAMETER, o, :name => o.name) end end #relationship_side: resource # | resourceref # | collection # | variable # | quotedtext # | selector # | casestatement # | hasharrayaccesses def check_RelationshipExpression(o) relation(o.left_expr) relation(o.right_expr) end def check_ResourceExpression(o) # The expression for type name cannot be statically checked - this is instead done at runtime # to enable better error message of the result of the expression rather than the static instruction. # (This can be revised as there are static constructs that are illegal, but require updating many # tests that expect the detailed reporting). end def check_ResourceBody(o) seenUnfolding = false o.operations.each do |ao| if ao.is_a?(Puppet::Pops::Model::AttributesOperation) if seenUnfolding acceptor.accept(Issues::MULTIPLE_ATTRIBUTES_UNFOLD, ao) else seenUnfolding = true end end end end def check_ResourceDefaultsExpression(o) if o.form && o.form != :regular acceptor.accept(Issues::NOT_VIRTUALIZEABLE, o) end end def check_ResourceOverrideExpression(o) if o.form && o.form != :regular acceptor.accept(Issues::NOT_VIRTUALIZEABLE, o) end end def check_ReservedWord(o) acceptor.accept(Issues::RESERVED_WORD, o, :word => o.word) end def check_SelectorExpression(o) rvalue(o.left_expr) end def check_SelectorEntry(o) rvalue(o.matching_expr) end def check_UnaryExpression(o) rvalue(o.expr) end def check_UnlessExpression(o) rvalue(o.test) # TODO: Unless may not have an else part that is an IfExpression (grammar denies this though) end # Checks that variable is either strictly 0, or a non 0 starting decimal number, or a valid VAR_NAME def check_VariableExpression(o) # The expression must be a qualified name or an integer name_expr = o.expr return if name_expr.is_a?(Model::LiteralInteger) if !name_expr.is_a?(Model::QualifiedName) acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, :feature => 'name', :container => o) else # name must be either a decimal string value, or a valid NAME name = o.expr.value if name[0,1] =~ /[0-9]/ unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME acceptor.accept(Issues::ILLEGAL_NUMERIC_VAR_NAME, o, :name => name) end else unless name =~ Puppet::Pops::Patterns::VAR_NAME acceptor.accept(Issues::ILLEGAL_VAR_NAME, o, :name => name) end end end end #--- HOSTNAME CHECKS # Transforms Array of host matching expressions into a (Ruby) array of AST::HostName def hostname_Array(o, semantic, single_feature_name) if single_feature_name acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=>single_feature_name, :container=>semantic}) end o.each {|x| hostname(x, semantic, false) } end def hostname_String(o, semantic, single_feature_name) # The 3.x checker only checks for illegal characters - if matching /[^-\w.]/ the name is invalid, # but this allows pathological names like "a..b......c", "----" # TODO: Investigate if more illegal hostnames should be flagged. # if o =~ Puppet::Pops::Patterns::ILLEGAL_HOSTNAME_CHARS acceptor.accept(Issues::ILLEGAL_HOSTNAME_CHARS, semantic, :hostname => o) end end def hostname_LiteralValue(o, semantic, single_feature_name) hostname_String(o.value.to_s, o, single_feature_name) end def hostname_ConcatenatedString(o, semantic, single_feature_name) # Puppet 3.1. only accepts a concatenated string without interpolated expressions if the_expr = o.segments.index {|s| s.is_a?(Model::TextExpression) } acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o.segments[the_expr].expr) elsif o.segments.size() != 1 # corner case, bad model, concatenation of several plain strings acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o) else # corner case, may be ok, but lexer may have replaced with plain string, this is # here if it does not hostname_String(o.segments[0], o.segments[0], false) end end def hostname_QualifiedName(o, semantic, single_feature_name) hostname_String(o.value.to_s, o, single_feature_name) end def hostname_QualifiedReference(o, semantic, single_feature_name) hostname_String(o.value.to_s, o, single_feature_name) end def hostname_LiteralNumber(o, semantic, single_feature_name) # always ok end def hostname_LiteralDefault(o, semantic, single_feature_name) # always ok end def hostname_LiteralRegularExpression(o, semantic, single_feature_name) # always ok end def hostname_Object(o, semantic, single_feature_name) acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=> single_feature_name || 'hostname', :container=>semantic}) end #---QUERY CHECKS # Anything not explicitly allowed is flagged as error. def query_Object(o) acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o) end # Puppet AST only allows == and != # def query_ComparisonExpression(o) acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o) unless [:'==', :'!='].include? o.operator end # Allows AND, OR, and checks if left/right are allowed in query. def query_BooleanExpression(o) query o.left_expr query o.right_expr end def query_ParenthesizedExpression(o) query(o.expr) end def query_VariableExpression(o); end def query_QualifiedName(o); end def query_LiteralNumber(o); end def query_LiteralString(o); end def query_LiteralBoolean(o); end #---RVALUE CHECKS # By default, all expressions are reported as being rvalues # Implement specific rvalue checks for those that are not. # def rvalue_Expression(o); end def rvalue_CollectExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end def rvalue_Definition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end def rvalue_NodeDefinition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end def rvalue_UnaryExpression(o) ; rvalue o.expr ; end #---TOP CHECK def top_NilClass(o, definition) # ok, reached the top, no more parents end def top_Object(o, definition) # fail, reached a container that is not top level acceptor.accept(Issues::NOT_TOP_LEVEL, definition) end def top_BlockExpression(o, definition) # ok, if this is a block representing the body of a class, or is top level top o.eContainer, definition end def top_HostClassDefinition(o, definition) # ok, stop scanning parents end def top_Program(o, definition) # ok end # A LambdaExpression is a BlockExpression, and this method is needed to prevent the polymorph method for BlockExpression # to accept a lambda. # A lambda can not iteratively create classes, nodes or defines as the lambda does not have a closure. # def top_LambdaExpression(o, definition) # fail, stop scanning parents acceptor.accept(Issues::NOT_TOP_LEVEL, definition) end #--IDEM CHECK def idem_Object(o) false end def idem_Nop(o) true end def idem_NilClass(o) true end def idem_Literal(o) true end def idem_LiteralList(o) true end def idem_LiteralHash(o) true end def idem_Factory(o) idem(o.current) end def idem_AccessExpression(o) true end def idem_BinaryExpression(o) true end def idem_RelationshipExpression(o) # Always side effect false end def idem_AssignmentExpression(o) # Always side effect false end # Handles UnaryMinusExpression, NotExpression, VariableExpression def idem_UnaryExpression(o) true end # Allow (no-effect parentheses) to be used around a productive expression def idem_ParenthesizedExpression(o) idem(o.expr) end def idem_RenderExpression(o) false end def idem_RenderStringExpression(o) false end def idem_BlockExpression(o) # productive if there is at least one productive expression ! o.statements.any? {|expr| !idem(expr) } end # Returns true even though there may be interpolated expressions that have side effect. # Report as idem anyway, as it is very bad design to evaluate an interpolated string for its # side effect only. def idem_ConcatenatedString(o) true end # Heredoc is just a string, but may contain interpolated string (which may have side effects). # This is still bad design and should be reported as idem. def idem_HeredocExpression(o) true end # May technically have side effects inside the Selector, but this is bad design - treat as idem def idem_SelectorExpression(o) true end def idem_IfExpression(o) [o.test, o.then_expr, o.else_expr].all? {|e| idem(e) } end # Case expression is idem, if test, and all options are idem def idem_CaseExpression(o) return false if !idem(o.test) ! o.options.any? {|opt| !idem(opt) } end # An option is idem if values and the then_expression are idem def idem_CaseOption(o) return false if o.values.any? { |value| !idem(value) } idem(o.then_expr) end #--- NON POLYMORPH, NON CHECKING CODE # Produces string part of something named, or nil if not a QualifiedName or QualifiedReference # def varname_to_s(o) case o when Model::QualifiedName o.value when Model::QualifiedReference o.value else nil end end end diff --git a/spec/integration/parser/collector_spec.rb b/spec/integration/parser/collector_spec.rb index aa227f613..02fdcbb61 100755 --- a/spec/integration/parser/collector_spec.rb +++ b/spec/integration/parser/collector_spec.rb @@ -1,317 +1,324 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/compiler' require 'puppet/parser/collector' describe Puppet::Parser::Collector do include PuppetSpec::Compiler def expect_the_message_to_be(expected_messages, code, node = Puppet::Node.new('the node')) catalog = compile_to_catalog(code, node) messages = catalog.resources.find_all { |resource| resource.type == 'Notify' }. collect { |notify| notify[:message] } messages.should include(*expected_messages) end shared_examples_for "virtual resource collection" do it "matches everything when no query given" do expect_the_message_to_be(["the other message", "the message"], <<-MANIFEST) @notify { "testing": message => "the message" } @notify { "other": message => "the other message" } Notify <| |> MANIFEST end it "matches regular resources " do expect_the_message_to_be(["changed", "changed"], <<-MANIFEST) notify { "testing": message => "the message" } notify { "other": message => "the other message" } Notify <| |> { message => "changed" } MANIFEST end it "matches on tags" do expect_the_message_to_be(["wanted"], <<-MANIFEST) @notify { "testing": tag => ["one"], message => "wanted" } @notify { "other": tag => ["two"], message => "unwanted" } Notify <| tag == one |> MANIFEST end it "matches on title" do expect_the_message_to_be(["the message"], <<-MANIFEST) @notify { "testing": message => "the message" } Notify <| title == "testing" |> MANIFEST end it "matches on other parameters" do expect_the_message_to_be(["the message"], <<-MANIFEST) @notify { "testing": message => "the message" } @notify { "other testing": message => "the wrong message" } Notify <| message == "the message" |> MANIFEST end it "matches against elements of an array valued parameter" do expect_the_message_to_be([["the", "message"]], <<-MANIFEST) @notify { "testing": message => ["the", "message"] } @notify { "other testing": message => ["not", "here"] } Notify <| message == "message" |> MANIFEST end it "matches with bare word" do expect_the_message_to_be(["wanted"], <<-MANIFEST) @notify { "testing": tag => ["one"], message => "wanted" } Notify <| tag == one |> MANIFEST end it "matches with single quoted string" do expect_the_message_to_be(["wanted"], <<-MANIFEST) @notify { "testing": tag => ["one"], message => "wanted" } Notify <| tag == 'one' |> MANIFEST end it "matches with double quoted string" do expect_the_message_to_be(["wanted"], <<-MANIFEST) @notify { "testing": tag => ["one"], message => "wanted" } Notify <| tag == "one" |> MANIFEST end it "matches with double quoted string with interpolated expression" do expect_the_message_to_be(["wanted"], <<-MANIFEST) @notify { "testing": tag => ["one"], message => "wanted" } $x = 'one' Notify <| tag == "$x" |> MANIFEST end it "allows criteria to be combined with 'and'" do expect_the_message_to_be(["the message"], <<-MANIFEST) @notify { "testing": message => "the message" } @notify { "other": message => "the message" } Notify <| title == "testing" and message == "the message" |> MANIFEST end it "allows criteria to be combined with 'or'" do expect_the_message_to_be(["the message", "other message"], <<-MANIFEST) @notify { "testing": message => "the message" } @notify { "other": message => "other message" } @notify { "yet another": message => "different message" } Notify <| title == "testing" or message == "other message" |> MANIFEST end it "allows criteria to be combined with 'or'" do expect_the_message_to_be(["the message", "other message"], <<-MANIFEST) @notify { "testing": message => "the message" } @notify { "other": message => "other message" } @notify { "yet another": message => "different message" } Notify <| title == "testing" or message == "other message" |> MANIFEST end it "allows criteria to be grouped with parens" do expect_the_message_to_be(["the message", "different message"], <<-MANIFEST) @notify { "testing": message => "different message", withpath => true } @notify { "other": message => "the message" } @notify { "yet another": message => "the message", withpath => true } Notify <| (title == "testing" or message == "the message") and withpath == true |> MANIFEST end it "does not do anything if nothing matches" do expect_the_message_to_be([], <<-MANIFEST) @notify { "testing": message => "different message" } Notify <| title == "does not exist" |> MANIFEST end it "excludes items with inequalities" do expect_the_message_to_be(["good message"], <<-MANIFEST) @notify { "testing": message => "good message" } @notify { "the wrong one": message => "bad message" } Notify <| title != "the wrong one" |> MANIFEST end it "does not exclude resources with unequal arrays" do expect_the_message_to_be(["message", ["not this message", "or this one"]], <<-MANIFEST) @notify { "testing": message => "message" } @notify { "the wrong one": message => ["not this message", "or this one"] } Notify <| message != "not this message" |> MANIFEST end it "does not exclude tags with inequalities" do expect_the_message_to_be(["wanted message", "the way it works"], <<-MANIFEST) @notify { "testing": tag => ["wanted"], message => "wanted message" } @notify { "other": tag => ["why"], message => "the way it works" } Notify <| tag != "why" |> MANIFEST end it "does not collect classes" do node = Puppet::Node.new('the node') expect do catalog = compile_to_catalog(<<-MANIFEST, node) class theclass { @notify { "testing": message => "good message" } } Class <| |> MANIFEST end.to raise_error(/Classes cannot be collected/) end it "does not collect resources that don't exist" do node = Puppet::Node.new('the node') expect do catalog = compile_to_catalog(<<-MANIFEST, node) class theclass { @notify { "testing": message => "good message" } } SomeResource <| |> MANIFEST end.to raise_error(/Resource type someresource doesn't exist/) end context "overrides" do it "modifies an existing array" do expect_the_message_to_be([["original message", "extra message"]], <<-MANIFEST) @notify { "testing": message => ["original message"] } Notify <| |> { message +> "extra message" } MANIFEST end it "converts a scalar to an array" do expect_the_message_to_be([["original message", "extra message"]], <<-MANIFEST) @notify { "testing": message => "original message" } Notify <| |> { message +> "extra message" } MANIFEST end it "collects with override when inside a class (#10963)" do expect_the_message_to_be(["overridden message"], <<-MANIFEST) @notify { "testing": message => "original message" } include collector_test class collector_test { Notify <| |> { message => "overridden message" } } MANIFEST end it "collects with override when inside a define (#10963)" do expect_the_message_to_be(["overridden message"], <<-MANIFEST) @notify { "testing": message => "original message" } collector_test { testing: } define collector_test() { Notify <| |> { message => "overridden message" } } MANIFEST end # Catches regression in implemented behavior, this is not to be taken as this is the wanted behavior # but it has been this way for a long time. it "collects and overrides user defined resources immediately (before queue is evaluated)" do expect_the_message_to_be(["overridden"], <<-MANIFEST) define foo($message) { notify { "testing": message => $message } } foo { test: message => 'given' } Foo <| |> { message => 'overridden' } MANIFEST end # Catches regression in implemented behavior, this is not to be taken as this is the wanted behavior # but it has been this way for a long time. it "collects and overrides user defined resources immediately (virtual resources not queued)" do expect_the_message_to_be(["overridden"], <<-MANIFEST) define foo($message) { @notify { "testing": message => $message } } foo { test: message => 'given' } Notify <| |> # must be collected or the assertion does not find it Foo <| |> { message => 'overridden' } MANIFEST end # Catches regression in implemented behavior, this is not to be taken as this is the wanted behavior # but it has been this way for a long time. # Note difference from none +> case where the override takes effect it "collects and overrides user defined resources with +>" do expect_the_message_to_be([["given", "overridden"]], <<-MANIFEST) define foo($message) { notify { "$name": message => $message } } foo { test: message => ['given'] } Notify <| |> { message +> ['overridden'] } MANIFEST end it "collects and overrides virtual resources multiple times using multiple collects" do expect_the_message_to_be(["overridden2"], <<-MANIFEST) @notify { "testing": message => "original" } Notify <| |> { message => 'overridden1' } Notify <| |> { message => 'overridden2' } MANIFEST end it "collects and overrides non virtual resources multiple times using multiple collects" do expect_the_message_to_be(["overridden2"], <<-MANIFEST) notify { "testing": message => "original" } Notify <| |> { message => 'overridden1' } Notify <| |> { message => 'overridden2' } MANIFEST end end end describe "in the current parser" do before :each do Puppet[:parser] = 'current' end it_behaves_like "virtual resource collection" end describe "in the future parser" do before :each do Puppet[:parser] = 'future' end it_behaves_like "virtual resource collection" + + it "collects and overrides attributes specified with a * => hash" do + expect_the_message_to_be(["overridden1"], <<-MANIFEST) + notify { "testing": message => "original" } + Notify <| |> { * => { message => 'overridden1' }} + MANIFEST + end end end diff --git a/spec/integration/parser/resource_expressions_spec.rb b/spec/integration/parser/resource_expressions_spec.rb index bac1f77ad..63ee2d7a0 100644 --- a/spec/integration/parser/resource_expressions_spec.rb +++ b/spec/integration/parser/resource_expressions_spec.rb @@ -1,299 +1,302 @@ require 'spec_helper' require 'puppet_spec/language' describe "Puppet resource expressions" do extend PuppetSpec::Language describe "future parser" do before :each do Puppet[:parser] = 'future' end produces( "$a = notify $b = example $c = { message => hello } @@Resource[$a] { $b: * => $c } realize(Resource[$a, $b]) " => "Notify[example][message] == 'hello'") context "resource titles" do produces( "notify { thing: }" => "defined(Notify[thing])", "$x = thing notify { $x: }" => "defined(Notify[thing])", "notify { [thing]: }" => "defined(Notify[thing])", "$x = [thing] notify { $x: }" => "defined(Notify[thing])", "notify { [[nested, array]]: }" => "defined(Notify[nested]) and defined(Notify[array])", "$x = [[nested, array]] notify { $x: }" => "defined(Notify[nested]) and defined(Notify[array])", "notify { []: }" => [], # this asserts nothing added "$x = [] notify { $x: }" => [], # this asserts nothing added "notify { default: }" => "!defined(Notify['default'])", # nothing created because this is just a local default "$x = default notify { $x: }" => "!defined(Notify['default'])") fails( "notify { '': }" => /Empty string title/, "$x = '' notify { $x: }" => /Empty string title/, "notify { 1: }" => /Illegal title type.*Expected String, got Integer/, "$x = 1 notify { $x: }" => /Illegal title type.*Expected String, got Integer/, "notify { [1]: }" => /Illegal title type.*Expected String, got Integer/, "$x = [1] notify { $x: }" => /Illegal title type.*Expected String, got Integer/, "notify { 3.0: }" => /Illegal title type.*Expected String, got Float/, "$x = 3.0 notify { $x: }" => /Illegal title type.*Expected String, got Float/, "notify { [3.0]: }" => /Illegal title type.*Expected String, got Float/, "$x = [3.0] notify { $x: }" => /Illegal title type.*Expected String, got Float/, "notify { true: }" => /Illegal title type.*Expected String, got Boolean/, "$x = true notify { $x: }" => /Illegal title type.*Expected String, got Boolean/, "notify { [true]: }" => /Illegal title type.*Expected String, got Boolean/, "$x = [true] notify { $x: }" => /Illegal title type.*Expected String, got Boolean/, "notify { [false]: }" => /Illegal title type.*Expected String, got Boolean/, "$x = [false] notify { $x: }" => /Illegal title type.*Expected String, got Boolean/, "notify { undef: }" => /Missing title.*undef/, "$x = undef notify { $x: }" => /Missing title.*undef/, "notify { [undef]: }" => /Missing title.*undef/, "$x = [undef] notify { $x: }" => /Missing title.*undef/, "notify { {nested => hash}: }" => /Illegal title type.*Expected String, got Hash/, "$x = {nested => hash} notify { $x: }" => /Illegal title type.*Expected String, got Hash/, "notify { [{nested => hash}]: }" => /Illegal title type.*Expected String, got Hash/, "$x = [{nested => hash}] notify { $x: }" => /Illegal title type.*Expected String, got Hash/, "notify { /regexp/: }" => /Illegal title type.*Expected String, got Regexp/, "$x = /regexp/ notify { $x: }" => /Illegal title type.*Expected String, got Regexp/, "notify { [/regexp/]: }" => /Illegal title type.*Expected String, got Regexp/, "$x = [/regexp/] notify { $x: }" => /Illegal title type.*Expected String, got Regexp/, "notify { [dupe, dupe]: }" => /The title 'dupe' has already been used/, "notify { dupe:; dupe: }" => /The title 'dupe' has already been used/, "notify { [dupe]:; dupe: }" => /The title 'dupe' has already been used/, "notify { [default, default]:}" => /The title 'default' has already been used/, "notify { default:; default:}" => /The title 'default' has already been used/, "notify { [default]:; default:}" => /The title 'default' has already been used/) end context "type names" do produces( "notify { testing: }" => "defined(Notify[testing])") produces( "$a = notify; Resource[$a] { testing: }" => "defined(Notify[testing])") produces( "Resource['notify'] { testing: }" => "defined(Notify[testing])") produces( "Resource[sprintf('%s', 'notify')] { testing: }" => "defined(Notify[testing])") produces( "$a = ify; Resource[\"not$a\"] { testing: }" => "defined(Notify[testing])") produces( "Notify { testing: }" => "defined(Notify[testing])") produces( "Resource[Notify] { testing: }" => "defined(Notify[testing])") produces( "Resource['Notify'] { testing: }" => "defined(Notify[testing])") produces( "class a { notify { testing: } } class { a: }" => "defined(Notify[testing])") produces( "class a { notify { testing: } } Class { a: }" => "defined(Notify[testing])") produces( "class a { notify { testing: } } Resource['class'] { a: }" => "defined(Notify[testing])") produces( "define a::b { notify { testing: } } a::b { title: }" => "defined(Notify[testing])") produces( "define a::b { notify { testing: } } A::B { title: }" => "defined(Notify[testing])") produces( "define a::b { notify { testing: } } Resource['a::b'] { title: }" => "defined(Notify[testing])") fails( "'class' { a: }" => /Illegal Resource Type expression.*got String/) fails( "'' { testing: }" => /Illegal Resource Type expression.*got String/) fails( "1 { testing: }" => /Illegal Resource Type expression.*got Integer/) fails( "3.0 { testing: }" => /Illegal Resource Type expression.*got Float/) fails( "true { testing: }" => /Illegal Resource Type expression.*got Boolean/) fails( "'not correct' { testing: }" => /Illegal Resource Type expression.*got String/) fails( "Notify[hi] { testing: }" => /Illegal Resource Type expression.*got Notify\['hi'\]/) fails( "[Notify, File] { testing: }" => /Illegal Resource Type expression.*got Array\[Type\[Resource\]\]/) fails( "define a::b { notify { testing: } } 'a::b' { title: }" => /Illegal Resource Type expression.*got String/) fails( "Does::Not::Exist { title: }" => /Invalid resource type does::not::exist/) end context "local defaults" do produces( "notify { example:; default: message => defaulted }" => "Notify[example][message] == 'defaulted'", "notify { example: message => specific; default: message => defaulted }" => "Notify[example][message] == 'specific'", "notify { example: message => undef; default: message => defaulted }" => "Notify[example][message] == undef", "notify { [example, other]: ; default: message => defaulted }" => "Notify[example][message] == 'defaulted' and Notify[other][message] == 'defaulted'", "notify { [example, default]: message => set; other: }" => "Notify[example][message] == 'set' and Notify[other][message] == 'set'") end context "order of evaluation" do fails("notify { hi: message => value; bye: message => Notify[hi][message] }" => /Resource not found: Notify\['hi'\]/) produces("notify { hi: message => (notify { param: message => set }); bye: message => Notify[param][message] }" => "defined(Notify[hi]) and Notify[bye][message] == 'set'") fails("notify { bye: message => Notify[param][message]; hi: message => (notify { param: message => set }) }" => /Resource not found: Notify\['param'\]/) end context "parameters" do produces( "notify { title: message => set }" => "Notify[title][message] == 'set'", "$x = set notify { title: message => $x }" => "Notify[title][message] == 'set'", "notify { title: *=> { message => set } }" => "Notify[title][message] == 'set'", "$x = { message => set } notify { title: * => $x }" => "Notify[title][message] == 'set'", # picks up defaults "$x = { owner => the_x } $y = { mode => '0666' } $t = '/tmp/x' file { default: * => $x; $t: path => '/somewhere', * => $y }" => "File[$t][mode] == '0666' and File[$t][owner] == 'the_x' and File[$t][path] == '/somewhere'", # explicit wins over default - no error "$x = { owner => the_x, mode => '0777' } $y = { mode => '0666' } $t = '/tmp/x' file { default: * => $x; $t: path => '/somewhere', * => $y }" => "File[$t][mode] == '0666' and File[$t][owner] == 'the_x' and File[$t][path] == '/somewhere'") + produces("notify{title:}; Notify[title] { * => { message => set}}" => "Notify[title][message] == 'set'") + produces("Notify { * => { message => set}}; notify{title:}" => "Notify[title][message] == 'set'") + fails("notify { title: unknown => value }" => /Invalid parameter unknown/) # this really needs to be a better error message. fails("notify { title: * => { hash => value }, message => oops }" => /Invalid parameter hash/) # should this be a better error message? fails("notify { title: message => oops, * => { hash => value } }" => /Invalid parameter hash/) fails("notify { title: * => { unknown => value } }" => /Invalid parameter unknown/) fails(" $x = { mode => '0666' } $y = { owner => the_y } $t = '/tmp/x' file { $t: * => $x, * => $y }" => /Unfolding of attributes from Hash can only be used once per resource body/) end context "virtual" do produces( "@notify { example: }" => "!defined(Notify[example])", "@notify { example: } realize(Notify[example])" => "defined(Notify[example])", "@notify { virtual: message => set } notify { real: message => Notify[virtual][message] }" => "Notify[real][message] == 'set'") end context "exported" do produces( "@@notify { example: }" => "!defined(Notify[example])", "@@notify { example: } realize(Notify[example])" => "defined(Notify[example])", "@@notify { exported: message => set } notify { real: message => Notify[exported][message] }" => "Notify[real][message] == 'set'") end context "explicit undefs" do # PUP-3505 produces(" $x = 10 define foo($x = undef) { notify { example: message => \"'$x'\" } } foo {'blah': x => undef } " => "Notify[example][message] == \"''\"") end end describe "current parser" do before :each do Puppet[:parser] = 'current' end produces( "notify { thing: }" => ["Notify[thing]"], "$x = thing notify { $x: }" => ["Notify[thing]"], "notify { [thing]: }" => ["Notify[thing]"], "$x = [thing] notify { $x: }" => ["Notify[thing]"], "notify { [[nested, array]]: }" => ["Notify[nested]", "Notify[array]"], "$x = [[nested, array]] notify { $x: }" => ["Notify[nested]", "Notify[array]"], # deprecate? "notify { 1: }" => ["Notify[1]"], "$x = 1 notify { $x: }" => ["Notify[1]"], # deprecate? "notify { [1]: }" => ["Notify[1]"], "$x = [1] notify { $x: }" => ["Notify[1]"], # deprecate? "notify { 3.0: }" => ["Notify[3.0]"], "$x = 3.0 notify { $x: }" => ["Notify[3.0]"], # deprecate? "notify { [3.0]: }" => ["Notify[3.0]"], "$x = [3.0] notify { $x: }" => ["Notify[3.0]"]) # :( fails( "notify { true: }" => /Syntax error/) produces("$x = true notify { $x: }" => ["Notify[true]"]) # this makes no sense given the [false] case produces( "notify { [true]: }" => ["Notify[true]"], "$x = [true] notify { $x: }" => ["Notify[true]"]) # *sigh* fails( "notify { false: }" => /Syntax error/, "$x = false notify { $x: }" => /No title provided and :notify is not a valid resource reference/, "notify { [false]: }" => /No title provided and :notify is not a valid resource reference/, "$x = [false] notify { $x: }" => /No title provided and :notify is not a valid resource reference/) # works for variable value, not for literal. deprecate? fails("notify { undef: }" => /Syntax error/) produces( "$x = undef notify { $x: }" => ["Notify[undef]"], # deprecate? "notify { [undef]: }" => ["Notify[undef]"], "$x = [undef] notify { $x: }" => ["Notify[undef]"]) fails("notify { {nested => hash}: }" => /Syntax error/) #produces("$x = {nested => hash} notify { $x: }" => ["Notify[{nested => hash}]"]) #it is created, but isn't possible to reference the resource. deprecate? #produces("notify { [{nested => hash}]: }" => ["Notify[{nested => hash}]"]) #it is created, but isn't possible to reference the resource. deprecate? #produces("$x = [{nested => hash}] notify { $x: }" => ["Notify[{nested => hash}]"]) #it is created, but isn't possible to reference the resource. deprecate? fails( "notify { /regexp/: }" => /Syntax error/, "$x = /regexp/ notify { $x: }" => /Syntax error/, "notify { [/regexp/]: }" => /Syntax error/, "$x = [/regexp/] notify { $x: }" => /Syntax error/, "notify { default: }" => /Syntax error/, "$x = default notify { $x: }" => /Syntax error/, "notify { [default]: }" => /Syntax error/, "$x = [default] notify { $x: }" => /Syntax error/) end end