diff --git a/lib/puppet/pops/issues.rb b/lib/puppet/pops/issues.rb index 3fe06db12..f598ef9d7 100644 --- a/lib/puppet/pops/issues.rb +++ b/lib/puppet/pops/issues.rb @@ -1,549 +1,549 @@ # Defines classes to deal with issues, and message formatting and defines constants with Issues. # @api public # module Puppet::Pops::Issues # Describes an issue, and can produce a message for an occurrence of the issue. # class Issue # The issue code # @return [Symbol] attr_reader :issue_code # A block producing the message # @return [Proc] attr_reader :message_block # Names that must be bound in an occurrence of the issue to be able to produce a message. # These are the names in addition to requirements stipulated by the Issue formatter contract; i.e. :label`, # and `:semantic`. # attr_reader :arg_names # If this issue can have its severity lowered to :warning, :deprecation, or :ignored attr_writer :demotable # Configures the Issue with required arguments (bound by occurrence), and a block producing a message. def initialize issue_code, *args, &block @issue_code = issue_code @message_block = block @arg_names = args @demotable = true end # Returns true if it is allowed to demote this issue def demotable? @demotable end # Formats a message for an occurrence of the issue with argument bindings passed in a hash. # The hash must contain a LabelProvider bound to the key `label` and the semantic model element # bound to the key `semantic`. All required arguments as specified by `arg_names` must be bound # in the given `hash`. # @api public # def format(hash ={}) # Create a Message Data where all hash keys become methods for convenient interpolation # in issue text. msgdata = MessageData.new(*arg_names) begin # Evaluate the message block in the msg data's binding msgdata.format(hash, &message_block) rescue StandardError => e Puppet::Pops::Issues::MessageData raise RuntimeError, "Error while reporting issue: #{issue_code}. #{e.message}", caller end end end # Provides a binding of arguments passed to Issue.format to method names available # in the issue's message producing block. # @api private # class MessageData def initialize *argnames singleton = class << self; self end argnames.each do |name| singleton.send(:define_method, name) do @data[name] end end end def format(hash, &block) @data = hash instance_eval &block end # Returns the label provider given as a key in the hash passed to #format. # If given an argument, calls #label on the label provider (caller would otherwise have to # call label.label(it) # def label(it = nil) raise "Label provider key :label must be set to produce the text of the message!" unless @data[:label] it.nil? ? @data[:label] : @data[:label].label(it) end # Returns the label provider given as a key in the hash passed to #format. # def semantic raise "Label provider key :semantic must be set to produce the text of the message!" unless @data[:semantic] @data[:semantic] end end # Defines an issue with the given `issue_code`, additional required parameters, and a block producing a message. # The block is evaluated in the context of a MessageData which provides convenient access to all required arguments # via accessor methods. In addition to accessors for specified arguments, these are also available: # * `label` - a `LabelProvider` that provides human understandable names for model elements and production of article (a/an/the). # * `semantic` - the model element for which the issue is reported # # @param issue_code [Symbol] the issue code for the issue used as an identifier, should be the same as the constant # the issue is bound to. # @param args [Symbol] required arguments that must be passed when formatting the message, may be empty # @param block [Proc] a block producing the message string, evaluated in a MessageData scope. The produced string # should not end with a period as additional information may be appended. # # @see MessageData # @api public # def self.issue (issue_code, *args, &block) Issue.new(issue_code, *args, &block) end # Creates a non demotable issue. # @see Issue.issue # def self.hard_issue(issue_code, *args, &block) result = Issue.new(issue_code, *args, &block) result.demotable = false result end # @comment Here follows definitions of issues. The intent is to provide a list from which yardoc can be generated # containing more detailed information / explanation of the issue. # These issues are set as constants, but it is unfortunately not possible for the created object to easily know which # name it is bound to. Instead the constant has to be repeated. (Alternatively, it could be done by instead calling # #const_set on the module, but the extra work required to get yardoc output vs. the extra effort to repeat the name # twice makes it not worth it (if doable at all, since there is no tag to artificially construct a constant, and # the parse tag does not produce any result for a constant assignment). # This is allowed (3.1) and has not yet been deprecated. # @todo configuration # NAME_WITH_HYPHEN = issue :NAME_WITH_HYPHEN, :name do "#{label.a_an_uc(semantic)} may not have a name containing a hyphen. The name '#{name}' is not legal" end # When a variable name contains a hyphen and these are illegal. # It is possible to control if a hyphen is legal in a name or not using the setting TODO # @todo describe the setting # @api public # @todo configuration if this is error or warning # VAR_WITH_HYPHEN = issue :VAR_WITH_HYPHEN, :name do "A variable name may not contain a hyphen. The name '#{name}' is not legal" end # A class, definition, or node may only appear at top level or inside other classes # @todo Is this really true for nodes? Can they be inside classes? Isn't that too late? # @api public # NOT_TOP_LEVEL = hard_issue :NOT_TOP_LEVEL do "Classes, definitions, and nodes may only appear at toplevel or inside other classes" end CROSS_SCOPE_ASSIGNMENT = hard_issue :CROSS_SCOPE_ASSIGNMENT, :name do "Illegal attempt to assign to '#{name}'. Cannot assign to variables in other namespaces" end # Assignment can only be made to certain types of left hand expressions such as variables. ILLEGAL_ASSIGNMENT = hard_issue :ILLEGAL_ASSIGNMENT do "Illegal attempt to assign to '#{label.a_an(semantic)}'. Not an assignable reference" end # Variables are immutable, cannot reassign in the same assignment scope ILLEGAL_REASSIGNMENT = hard_issue :ILLEGAL_REASSIGNMENT, :name do "Cannot reassign variable #{name}" end ILLEGAL_RESERVED_ASSIGNMENT = hard_issue :ILLEGAL_RESERVED_ASSIGNMENT, :name do "Attempt to assign to a reserved variable name: '#{name}'" end # Assignment cannot be made to numeric match result variables ILLEGAL_NUMERIC_ASSIGNMENT = issue :ILLEGAL_NUMERIC_ASSIGNMENT, :varname do "Illegal attempt to assign to the numeric match result variable '$#{varname}'. Numeric variables are not assignable" end # parameters cannot have numeric names, clashes with match result variables ILLEGAL_NUMERIC_PARAMETER = issue :ILLEGAL_NUMERIC_PARAMETER, :name do - "The numeric parameter name '$#{varname}' cannot be used (clashes with numeric match result variables)" + "The numeric parameter name '$#{name}' cannot be used (clashes with numeric match result variables)" end # In certain versions of Puppet it may be allowed to assign to a not already assigned key # in an array or a hash. This is an optional validation that may be turned on to prevent accidental # mutation. # ILLEGAL_INDEXED_ASSIGNMENT = issue :ILLEGAL_INDEXED_ASSIGNMENT do "Illegal attempt to assign via [index/key]. Not an assignable reference" end # When indexed assignment ($x[]=) is allowed, the leftmost expression must be # a variable expression. # ILLEGAL_ASSIGNMENT_VIA_INDEX = hard_issue :ILLEGAL_ASSIGNMENT_VIA_INDEX do "Illegal attempt to assign to #{label.a_an(semantic)} via [index/key]. Not an assignable reference" end APPENDS_DELETES_NO_LONGER_SUPPORTED = hard_issue :APPENDS_DELETES_NO_LONGER_SUPPORTED, :operator do "The operator '#{operator}' is no longer supported. See http://links.puppetlabs.com/remove-plus-equals" end # For unsupported operators (e.g. += and -= in puppet 4). # UNSUPPORTED_OPERATOR = hard_issue :UNSUPPORTED_OPERATOR, :operator do "The operator '#{operator}' is not supported." end # For operators that are not supported in specific contexts (e.g. '* =>' in # resource defaults) # UNSUPPORTED_OPERATOR_IN_CONTEXT = hard_issue :UNSUPPORTED_OPERATOR_IN_CONTEXT, :operator do "The operator '#{operator}' in #{label.a_an(semantic)} is not supported." end # For non applicable operators (e.g. << on Hash). # OPERATOR_NOT_APPLICABLE = hard_issue :OPERATOR_NOT_APPLICABLE, :operator, :left_value do "Operator '#{operator}' is not applicable to #{label.a_an(left_value)}." end COMPARISON_NOT_POSSIBLE = hard_issue :COMPARISON_NOT_POSSIBLE, :operator, :left_value, :right_value, :detail do "Comparison of: #{label(left_value)} #{operator} #{label(right_value)}, is not possible. Caused by '#{detail}'." end MATCH_NOT_REGEXP = hard_issue :MATCH_NOT_REGEXP, :detail do "Can not convert right match operand to a regular expression. Caused by '#{detail}'." end MATCH_NOT_STRING = hard_issue :MATCH_NOT_STRING, :left_value do "Left match operand must result in a String value. Got #{label.a_an(left_value)}." end # Some expressions/statements may not produce a value (known as right-value, or rvalue). # This may vary between puppet versions. # NOT_RVALUE = issue :NOT_RVALUE do "Invalid use of expression. #{label.a_an_uc(semantic)} does not produce a value" end # Appending to attributes is only allowed in certain types of resource expressions. # ILLEGAL_ATTRIBUTE_APPEND = hard_issue :ILLEGAL_ATTRIBUTE_APPEND, :name, :parent do "Illegal +> operation on attribute #{name}. This operator can not be used in #{label.a_an(parent)}" end ILLEGAL_NAME = hard_issue :ILLEGAL_NAME, :name do "Illegal name. The given name #{name} does not conform to the naming rule /^((::)?[a-z_]\w*)(::[a-z]\w*)*$/" end ILLEGAL_VAR_NAME = hard_issue :ILLEGAL_VAR_NAME, :name do "Illegal variable name, The given name '#{name}' does not conform to the naming rule /^((::)?[a-z]\w*)*((::)?[a-z_]\w*)$/" end ILLEGAL_NUMERIC_VAR_NAME = hard_issue :ILLEGAL_NUMERIC_VAR_NAME, :name do "Illegal numeric variable name, The given name '#{name}' must be a decimal value if it starts with a digit 0-9" end # In case a model is constructed programmatically, it must create valid type references. # ILLEGAL_CLASSREF = hard_issue :ILLEGAL_CLASSREF, :name do "Illegal type reference. The given name '#{name}' does not conform to the naming rule" end # This is a runtime issue - storeconfigs must be on in order to collect exported. This issue should be # set to :ignore when just checking syntax. # @todo should be a :warning by default # RT_NO_STORECONFIGS = issue :RT_NO_STORECONFIGS do "You cannot collect exported resources without storeconfigs being set; the collection will be ignored" end # This is a runtime issue - storeconfigs must be on in order to export a resource. This issue should be # set to :ignore when just checking syntax. # @todo should be a :warning by default # RT_NO_STORECONFIGS_EXPORT = issue :RT_NO_STORECONFIGS_EXPORT do "You cannot collect exported resources without storeconfigs being set; the export is ignored" end # A hostname may only contain letters, digits, '_', '-', and '.'. # ILLEGAL_HOSTNAME_CHARS = hard_issue :ILLEGAL_HOSTNAME_CHARS, :hostname do "The hostname '#{hostname}' contains illegal characters (only letters, digits, '_', '-', and '.' are allowed)" end # A hostname may only contain letters, digits, '_', '-', and '.'. # ILLEGAL_HOSTNAME_INTERPOLATION = hard_issue :ILLEGAL_HOSTNAME_INTERPOLATION do "An interpolated expression is not allowed in a hostname of a node" end # Issues when an expression is used where it is not legal. # E.g. an arithmetic expression where a hostname is expected. # ILLEGAL_EXPRESSION = hard_issue :ILLEGAL_EXPRESSION, :feature, :container do "Illegal expression. #{label.a_an_uc(semantic)} is unacceptable as #{feature} in #{label.a_an(container)}" end # Issues when an expression is used where it is not legal. # E.g. an arithmetic expression where a hostname is expected. # ILLEGAL_VARIABLE_EXPRESSION = hard_issue :ILLEGAL_VARIABLE_EXPRESSION do "Illegal variable expression. #{label.a_an_uc(semantic)} did not produce a variable name (String or Numeric)." end # Issues when an expression is used illegaly in a query. # query only supports == and !=, and not <, > etc. # ILLEGAL_QUERY_EXPRESSION = hard_issue :ILLEGAL_QUERY_EXPRESSION do "Illegal query expression. #{label.a_an_uc(semantic)} cannot be used in a query" end # If an attempt is made to make a resource default virtual or exported. # NOT_VIRTUALIZEABLE = hard_issue :NOT_VIRTUALIZEABLE do "Resource Defaults are not virtualizable" end # When an attempt is made to use multiple keys (to produce a range in Ruby - e.g. $arr[2,-1]). # This is not supported in 3x, but it allowed in 4x. # UNSUPPORTED_RANGE = issue :UNSUPPORTED_RANGE, :count do "Attempt to use unsupported range in #{label.a_an(semantic)}, #{count} values given for max 1" end ILLEGAL_RELATIONSHIP_OPERAND_TYPE = issue :ILLEGAL_RELATIONSHIP_OPERAND_TYPE, :operand do "Illegal relationship operand, can not form a relationship with #{label.a_an(operand)}. A Catalog type is required." end NOT_CATALOG_TYPE = issue :NOT_CATALOG_TYPE, :type do "Illegal relationship operand, can not form a relationship with something of type #{type}. A Catalog type is required." end BAD_STRING_SLICE_ARITY = issue :BAD_STRING_SLICE_ARITY, :actual do "String supports [] with one or two arguments. Got #{actual}" end BAD_STRING_SLICE_TYPE = issue :BAD_STRING_SLICE_TYPE, :actual do "String-Type [] requires all arguments to be integers (or default). Got #{actual}" end BAD_ARRAY_SLICE_ARITY = issue :BAD_ARRAY_SLICE_ARITY, :actual do "Array supports [] with one or two arguments. Got #{actual}" end BAD_HASH_SLICE_ARITY = issue :BAD_HASH_SLICE_ARITY, :actual do "Hash supports [] with one or more arguments. Got #{actual}" end BAD_INTEGER_SLICE_ARITY = issue :BAD_INTEGER_SLICE_ARITY, :actual do "Integer-Type supports [] with one or two arguments (from, to). Got #{actual}" end BAD_INTEGER_SLICE_TYPE = issue :BAD_INTEGER_SLICE_TYPE, :actual do "Integer-Type [] requires all arguments to be integers (or default). Got #{actual}" end BAD_COLLECTION_SLICE_TYPE = issue :BAD_COLLECTION_SLICE_TYPE, :actual do "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got #{label.a_an(actual)}" end BAD_FLOAT_SLICE_ARITY = issue :BAD_INTEGER_SLICE_ARITY, :actual do "Float-Type supports [] with one or two arguments (from, to). Got #{actual}" end BAD_FLOAT_SLICE_TYPE = issue :BAD_INTEGER_SLICE_TYPE, :actual do "Float-Type [] requires all arguments to be floats, or integers (or default). Got #{actual}" end BAD_SLICE_KEY_TYPE = issue :BAD_SLICE_KEY_TYPE, :left_value, :expected_classes, :actual do expected_text = if expected_classes.size > 1 "one of #{expected_classes.join(', ')} are" else "#{expected_classes[0]} is" end "#{label.a_an_uc(left_value)}[] cannot use #{actual} where #{expected_text} expected" end BAD_TYPE_SLICE_TYPE = issue :BAD_TYPE_SLICE_TYPE, :base_type, :actual do "#{base_type}[] arguments must be types. Got #{actual}" end BAD_TYPE_SLICE_ARITY = issue :BAD_TYPE_SLICE_ARITY, :base_type, :min, :max, :actual do base_type_label = base_type.is_a?(String) ? base_type : label.a_an_uc(base_type) if max == -1 || max == 1.0 / 0.0 # Infinity "#{base_type_label}[] accepts #{min} or more arguments. Got #{actual}" elsif max && max != min "#{base_type_label}[] accepts #{min} to #{max} arguments. Got #{actual}" else "#{base_type_label}[] accepts #{min} #{label.plural_s(min, 'argument')}. Got #{actual}" end end BAD_TYPE_SPECIALIZATION = hard_issue :BAD_TYPE_SPECIALIZATION, :type, :message do "Error creating type specialization of #{label.a_an(type)}, #{message}" end ILLEGAL_TYPE_SPECIALIZATION = issue :ILLEGAL_TYPE_SPECIALIZATION, :kind do "Cannot specialize an already specialized #{kind} type" end ILLEGAL_RESOURCE_SPECIALIZATION = issue :ILLEGAL_RESOURCE_SPECIALIZATION, :actual do "First argument to Resource[] must be a resource type or a String. Got #{actual}." end EMPTY_RESOURCE_SPECIALIZATION = issue :EMPTY_RESOURCE_SPECIALIZATION do "Arguments to Resource[] are all empty/undefined" end ILLEGAL_HOSTCLASS_NAME = hard_issue :ILLEGAL_HOSTCLASS_NAME, :name do "Illegal Class name in class reference. #{label.a_an_uc(name)} cannot be used where a String is expected" end ILLEGAL_DEFINITION_NAME = hard_issue :ILLEGAL_DEFINTION_NAME, :name do "Unacceptable name. The name '#{name}' is unacceptable as the name of #{label.a_an(semantic)}" end CAPTURES_REST_NOT_LAST = hard_issue :CAPTURES_REST_NOT_LAST, :param_name do "Parameter $#{param_name} is not last, and has 'captures rest'" end CAPTURES_REST_NOT_SUPPORTED = hard_issue :CAPTURES_REST_NOT_SUPPORTED, :container, :param_name do "Parameter $#{param_name} has 'captures rest' - not supported in #{label.a_an(container)}" end REQUIRED_PARAMETER_AFTER_OPTIONAL = hard_issue :REQUIRED_PARAMETER_AFTER_OPTIONAL, :param_name do "Parameter $#{param_name} is required but appears after optional parameters" end MISSING_REQUIRED_PARAMETER = hard_issue :MISSING_REQUIRED_PARAMETER, :param_name do "Parameter $#{param_name} is required but no value was given" end NOT_NUMERIC = issue :NOT_NUMERIC, :value do "The value '#{value}' cannot be converted to Numeric." end UNKNOWN_FUNCTION = issue :UNKNOWN_FUNCTION, :name do "Unknown function: '#{name}'." end UNKNOWN_VARIABLE = issue :UNKNOWN_VARIABLE, :name do "Unknown variable: '#{name}'." end RUNTIME_ERROR = issue :RUNTIME_ERROR, :detail do "Error while evaluating #{label.a_an(semantic)}, #{detail}" end UNKNOWN_RESOURCE_TYPE = issue :UNKNOWN_RESOURCE_TYPE, :type_name do "Resource type not found: #{type_name.capitalize}" end ILLEGAL_RESOURCE_TYPE = hard_issue :ILLEGAL_RESOURCE_TYPE, :actual do "Illegal Resource Type expression, expected result to be a type name, or untitled Resource, got #{actual}" end DUPLICATE_TITLE = issue :DUPLICATE_TITLE, :title do "The title '#{title}' has already been used in this resource expression" end DUPLICATE_ATTRIBUTE = issue :DUPLICATE_ATTRIBUE, :attribute do "The attribute '#{attribute}' has already been set in this resource body" end MISSING_TITLE = hard_issue :MISSING_TITLE do "Missing title. The title expression resulted in undef" end MISSING_TITLE_AT = hard_issue :MISSING_TITLE_AT, :index do "Missing title at index #{index}. The title expression resulted in an undef title" end ILLEGAL_TITLE_TYPE_AT = hard_issue :ILLEGAL_TITLE_TYPE_AT, :index, :actual do "Illegal title type at index #{index}. Expected String, got #{actual}" end EMPTY_STRING_TITLE_AT = hard_issue :EMPTY_STRING_TITLE_AT, :index do "Empty string title at #{index}. Title strings must have a length greater than zero." end UNKNOWN_RESOURCE = issue :UNKNOWN_RESOURCE, :type_name, :title do "Resource not found: #{type_name.capitalize}['#{title}']" end UNKNOWN_RESOURCE_PARAMETER = issue :UNKNOWN_RESOURCE_PARAMETER, :type_name, :title, :param_name do "The resource #{type_name.capitalize}['#{title}'] does not have a parameter called '#{param_name}'" end DIV_BY_ZERO = hard_issue :DIV_BY_ZERO do "Division by 0" end RESULT_IS_INFINITY = hard_issue :RESULT_IS_INFINITY, :operator do "The result of the #{operator} expression is Infinity" end # TODO_HEREDOC EMPTY_HEREDOC_SYNTAX_SEGMENT = issue :EMPTY_HEREDOC_SYNTAX_SEGMENT, :syntax do "Heredoc syntax specification has empty segment between '+' : '#{syntax}'" end ILLEGAL_EPP_PARAMETERS = issue :ILLEGAL_EPP_PARAMETERS do "Ambiguous EPP parameter expression. Probably missing '<%-' before parameters to remove leading whitespace" end DISCONTINUED_IMPORT = hard_issue :DISCONTINUED_IMPORT do "Use of 'import' has been discontinued in favor of a manifest directory. See http://links.puppetlabs.com/puppet-import-deprecation" end IDEM_EXPRESSION_NOT_LAST = issue :IDEM_EXPRESSION_NOT_LAST do "This #{label.label(semantic)} is not productive. A non productive construct may only be placed last in a block/sequence" end IDEM_NOT_ALLOWED_LAST = hard_issue :IDEM_NOT_ALLOWED_LAST, :container do "This #{label.label(semantic)} is not productive. #{label.a_an_uc(container)} can not end with a non productive construct" end RESERVED_WORD = hard_issue :RESERVED_WORD, :word do "Use of reserved word: #{word}, must be quoted if intended to be a String value" end RESERVED_TYPE_NAME = hard_issue :RESERVED_TYPE_NAME, :name do "The name: '#{name}' is already defined by Puppet and can not be used as the name of #{label.a_an(semantic)}." end UNMATCHED_SELECTOR = hard_issue :UNMATCHED_SELECTOR, :param_value do "No matching entry for selector parameter with value '#{param_value}'" end ILLEGAL_NODE_INHERITANCE = issue :ILLEGAL_NODE_INHERITANCE do "Node inheritance is not supported in Puppet >= 4.0.0. See http://links.puppetlabs.com/puppet-node-inheritance-deprecation" end ILLEGAL_OVERRIDEN_TYPE = issue :ILLEGAL_OVERRIDEN_TYPE, :actual do "Resource Override can only operate on resources, got: #{label.label(actual)}" end RESERVED_PARAMETER = hard_issue :RESERVED_PARAMETER, :container, :param_name do "The parameter $#{param_name} redefines a built in parameter in #{label.the(container)}" end TYPE_MISMATCH = hard_issue :TYPE_MISMATCH, :expected, :actual do "Expected value of type #{expected}, got #{actual}" end MULTIPLE_ATTRIBUTES_UNFOLD = hard_issue :MULTIPLE_ATTRIBUTES_UNFOLD do "Unfolding of attributes from Hash can only be used once per resource body" end end diff --git a/lib/puppet/pops/validation/checker4_0.rb b/lib/puppet/pops/validation/checker4_0.rb index 91342bc4b..079710d59 100644 --- a/lib/puppet/pops/validation/checker4_0.rb +++ b/lib/puppet/pops/validation/checker4_0.rb @@ -1,760 +1,760 @@ # 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 # 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 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=>'* =>') 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 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_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 =~ /^[0-9]+$/ + 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 if !o.expr.is_a?(Model::QualifiedName) acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, :feature => 'name', :container => o) else # name must be either a decimal 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/unit/pops/validator/validator_spec.rb b/spec/unit/pops/validator/validator_spec.rb index ec0b0be4c..31defdd6b 100644 --- a/spec/unit/pops/validator/validator_spec.rb +++ b/spec/unit/pops/validator/validator_spec.rb @@ -1,175 +1,184 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' # relative to this spec file (./) does not work as this file is loaded by rspec require File.join(File.dirname(__FILE__), '../parser/parser_rspec_helper') describe "validating 4x" do include ParserRspecHelper include PuppetSpec::Pops let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } let(:validator) { Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) } def validate(model) validator.validate(model) acceptor end it 'should raise error for illegal names' do pending "validation was too strict, now too relaxed - validation missing" expect(validate(fqn('Aaa'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) expect(validate(fqn('AAA'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) end it 'should raise error for illegal variable names' do expect(validate(fqn('Aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) expect(validate(fqn('AAA').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) expect(validate(fqn('aaa::_aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) end it 'should not raise error for variable name with underscore first in first name segment' do expect(validate(fqn('_aa').var())).to_not have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) expect(validate(fqn('::_aa').var())).to_not have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME) end context 'for non productive expressions' do [ '1', '3.14', "'a'", '"a"', '"${$a=10}"', # interpolation with side effect 'false', 'true', 'default', 'undef', '[1,2,3]', '{a=>10}', 'if 1 {2}', 'if 1 {2} else {3}', 'if 1 {2} elsif 3 {4}', 'unless 1 {2}', 'unless 1 {2} else {3}', '1 ? 2 => 3', '1 ? { 2 => 3}', '-1', '-foo()', # unary minus on productive '1+2', '1<2', '(1<2)', '!true', '!foo()', # not on productive '$a', '$a[1]', 'name', 'Type', 'Type[foo]' ].each do |expr| it "produces error for non productive: #{expr}" do source = "#{expr}; $a = 10" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::IDEM_EXPRESSION_NOT_LAST) end it "does not produce error when last for non productive: #{expr}" do source = " $a = 10; #{expr}" expect(validate(parse(source))).to_not have_issue(Puppet::Pops::Issues::IDEM_EXPRESSION_NOT_LAST) end end [ 'if 1 {$a = 1}', 'if 1 {2} else {$a=1}', 'if 1 {2} elsif 3 {$a=1}', 'unless 1 {$a=1}', 'unless 1 {2} else {$a=1}', '$a = 1 ? 2 => 3', '$a = 1 ? { 2 => 3}', 'Foo[a] -> Foo[b]', '($a=1)', 'foo()', '$a.foo()' ].each do |expr| it "does not produce error when for productive: #{expr}" do source = "#{expr}; $x = 1" expect(validate(parse(source))).to_not have_issue(Puppet::Pops::Issues::IDEM_EXPRESSION_NOT_LAST) end end ['class', 'define', 'node'].each do |type| it "flags non productive expression last in #{type}" do source = <<-SOURCE #{type} nope { 1 } end SOURCE expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::IDEM_NOT_ALLOWED_LAST) end end end context 'for reserved words' do ['function', 'private', 'type', 'attr'].each do |word| it "produces an error for the word '#{word}'" do source = "$a = #{word}" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::RESERVED_WORD) end end end context 'for reserved type names' do [# type/Type, is a reserved name but results in syntax error because it is a keyword in lower case form 'any', 'unit', 'scalar', 'boolean', 'numeric', 'integer', 'float', 'collection', 'array', 'hash', 'tuple', 'struct', 'variant', 'optional', 'enum', 'regexp', 'pattern', 'runtime', ].each do |name| it "produces an error for 'class #{name}'" do source = "class #{name} {}" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::RESERVED_TYPE_NAME) end it "produces an error for 'define #{name}'" do source = "define #{name} {}" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::RESERVED_TYPE_NAME) end end end context 'for reserved parameter names' do ['name', 'title'].each do |word| it "produces an error when $#{word} is used as a parameter in a class" do source = "class x ($#{word}) {}" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::RESERVED_PARAMETER) end it "produces an error when $#{word} is used as a parameter in a define" do source = "define x ($#{word}) {}" expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::RESERVED_PARAMETER) end end end + context 'for numeric parameter names' do + ['1', '0x2', '03'].each do |word| + it "produces an error when $#{word} is used as a parameter in a class" do + source = "class x ($#{word}) {}" + expect(validate(parse(source))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NUMERIC_PARAMETER) + end + end + end + def parse(source) Puppet::Pops::Parser::Parser.new().parse_string(source) end end