diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index b69a5c4e0..f009f20c6 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,20 +1,26 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Node < Puppet::Parser::AST::TopLevelConstruct attr_accessor :names, :context def initialize(names, context = {}, &ruby_code) raise ArgumentError, "names should be an array" unless names.is_a? Array + if context[:parent] + msg = "Deprecation notice: Node inheritance is not supported in Puppet >= 4.0.0. See http://links.puppetlabs.com/puppet-node-inheritance-deprecation" + # Cannot use Puppet.deprecation_warning here since it prints gobbledygook stack trace + Puppet.warning(msg) + end + @names = names @context = context @ruby_code = ruby_code end def instantiate(modname) @names.collect do |name| new_node = Puppet::Resource::Type.new(:node, name, @context.merge(:module_name => modname)) new_node.ruby_code = @ruby_code if @ruby_code new_node end end end diff --git a/lib/puppet/pops/issues.rb b/lib/puppet/pops/issues.rb index 8b598b6df..4fdec9816 100644 --- a/lib/puppet/pops/issues.rb +++ b/lib/puppet/pops/issues.rb @@ -1,501 +1,505 @@ # 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 APPEND_FAILED = issue :APPEND_FAILED, :message do "Append assignment += failed with error: #{message}" end DELETE_FAILED = issue :DELETE_FAILED, :message do "'Delete' assignment -= failed with error: #{message}" 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)" 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 # For unsupported operators (e.g. -= in puppet 3). # UNSUPPORTED_OPERATOR = hard_issue :UNSUPPORTED_OPERATOR, :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 DEPRECATED_NAME_AS_TYPE = issue :DEPRECATED_NAME_AS_TYPE, :name do "Resource references should now be capitalized. The given '#{name}' does not have the correct form" 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 # Issues when an expression is used where it is not legal. # E.g. an arithmetic expression where a hostname is expected. # 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 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 :RESERWED_WORD, :word do "Use of reserved word: #{word}, must be quoted if intended to be a String value" 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 end diff --git a/lib/puppet/pops/validation/checker4_0.rb b/lib/puppet/pops/validation/checker4_0.rb index 416015c94..53f696a6b 100644 --- a/lib/puppet/pops/validation/checker4_0.rb +++ b/lib/puppet/pops/validation/checker4_0.rb @@ -1,689 +1,692 @@ # 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) acceptor.accept(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) unless [:'=', :'+=', :'-='].include? o.operator assign(o.left_expr) rvalue(o.right_expr) 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_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 # 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 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) end def check_ResourceTypeDefinition(o) check_NamedDefinition(o) internal_check_no_capture(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_with_index do |p, index| if p.captures_rest acceptor.accept(Issues::CAPTURES_REST_NOT_SUPPORTED, p, {:container => container, :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]+$/ 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) # A resource expression must have a lower case NAME as its type e.g. 'file { ... }' unless o.type_name.is_a? Model::QualifiedName acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_name, :feature => 'resource type', :container => o) end # This is a runtime check - the model is valid, but will have runtime issues when evaluated # and storeconfigs is not set. if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.exported acceptor.accept(Issues::RT_NO_STORECONFIGS_EXPORT, o) end end def check_ResourceDefaultsExpression(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_ResourceDefaultsExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end def rvalue_ResourceOverrideExpression(o); acceptor.accept(Issues::NOT_RVALUE, 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/lib/puppet/pops/validation/validator_factory_4_0.rb b/lib/puppet/pops/validation/validator_factory_4_0.rb index 7eae59351..3d6f550f6 100644 --- a/lib/puppet/pops/validation/validator_factory_4_0.rb +++ b/lib/puppet/pops/validation/validator_factory_4_0.rb @@ -1,31 +1,32 @@ # Configures validation suitable for 4.0 # class Puppet::Pops::Validation::ValidatorFactory_4_0 < Puppet::Pops::Validation::Factory Issues = Puppet::Pops::Issues # Produces the checker to use def checker diagnostic_producer Puppet::Pops::Validation::Checker4_0.new(diagnostic_producer) end # Produces the label provider to use def label_provider Puppet::Pops::Model::ModelLabelProvider.new() end # Produces the severity producer to use def severity_producer p = super # Configure each issue that should **not** be an error # # Validate as per the current runtime configuration p[Issues::RT_NO_STORECONFIGS_EXPORT] = Puppet[:storeconfigs] ? :ignore : :warning p[Issues::RT_NO_STORECONFIGS] = Puppet[:storeconfigs] ? :ignore : :warning p[Issues::NAME_WITH_HYPHEN] = :error p[Issues::DEPRECATED_NAME_AS_TYPE] = :error p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :ignore + p[Issues::ILLEGAL_NODE_INHERITANCE] = :deprecation p end end diff --git a/spec/integration/parser/node_spec.rb b/spec/integration/parser/node_spec.rb index 1b60f1be9..9d4df4ac0 100644 --- a/spec/integration/parser/node_spec.rb +++ b/spec/integration/parser/node_spec.rb @@ -1,167 +1,186 @@ require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' describe 'node statements' do include PuppetSpec::Compiler include Matchers::Resource - it 'is unable to parse a name that is an invalid number' do - Puppet[:parser] = 'future' - - expect do - compile_to_catalog('node 5name {} ') - end.to raise_error(Puppet::Error, /Illegal number/) - end - - it 'parses a node name that is dotted numbers' do - Puppet[:parser] = 'future' - - catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("1.2.3.4")) - node 1.2.3.4 { notify { matched: } } - MANIFEST - - expect(catalog).to have_resource('Notify[matched]') - end - shared_examples_for 'nodes' do it 'selects a node where the name is just a number' do # Future parser doesn't allow a number in this position catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("5")) node 5 { notify { 'matched': } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'selects the node with a matching name' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node noden {} node nodename { notify { matched: } } node name {} MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'prefers a node with a literal name over one with a regex' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node /noden.me/ { notify { ignored: } } node nodename { notify { matched: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'includes the inherited nodes of the matching node' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node notmatched1 { notify { inherited: } } node nodename inherits notmatched1 { notify { matched: } } node notmatched2 { notify { ignored: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') expect(catalog).to have_resource('Notify[inherited]') end it 'selects a node where one of the names matches' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node different, nodename, other { notify { matched: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'arbitrarily selects one of the matching nodes' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node /not/ { notify { 'is not matched': } } node /name.*/ { notify { 'could be matched': } } node /na.e/ { notify { 'could also be matched': } } MANIFEST expect([catalog.resource('Notify[could be matched]'), catalog.resource('Notify[could also be matched]')].compact).to_not be_empty end it 'selects a node where one of the names matches with a mixture of literals and regex' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node different, /name/, other { notify { matched: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'errors when two nodes with regexes collide after some regex syntax is removed' do expect do compile_to_catalog(<<-MANIFEST) node /a.*(c)?/ { } node 'a.c' { } MANIFEST end.to raise_error(Puppet::Error, /Node 'a.c' is already defined/) end it 'provides captures from the regex in the node body' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node /(.*)/ { notify { "$1": } } MANIFEST expect(catalog).to have_resource('Notify[nodename]') end it 'selects the node with the matching regex' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) node /node.*/ { notify { matched: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'selects a node that is a literal string' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("node.name")) node 'node.name' { notify { matched: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'selects a node that is a prefix of the agent name' do Puppet[:strict_hostname_checking] = false catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("node.name.com")) node 'node.name' { notify { matched: } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'does not treat regex symbols as a regex inside a string literal' do catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodexname")) node 'node.name' { notify { 'not matched': } } node 'nodexname' { notify { 'matched': } } MANIFEST expect(catalog).to have_resource('Notify[matched]') end it 'errors when two nodes have the same name' do expect do compile_to_catalog(<<-MANIFEST) node name { } node 'name' { } MANIFEST end.to raise_error(Puppet::Error, /Node 'name' is already defined/) end end describe 'using classic parser' do before :each do Puppet[:parser] = 'current' end + it_behaves_like 'nodes' + + it 'raises deprecation warning for node inheritance for 3x parser' do + Puppet.expects(:warning).at_least_once + Puppet.expects(:warning).with(regexp_matches(/Deprecation notice\: Node inheritance is not supported in Puppet >= 4\.0\.0/)) + + catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("1.2.3.4")) + node default {} + node '1.2.3.4' inherits default { } + MANIFEST + end + end describe 'using future parser' do before :each do Puppet[:parser] = 'future' end + it_behaves_like 'nodes' + + it 'is unable to parse a name that is an invalid number' do + expect do + compile_to_catalog('node 5name {} ') + end.to raise_error(Puppet::Error, /Illegal number/) + end + + it 'parses a node name that is dotted numbers' do + catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("1.2.3.4")) + node 1.2.3.4 { notify { matched: } } + MANIFEST + + expect(catalog).to have_resource('Notify[matched]') + end + + it 'raises deprecation warning for node inheritance' do + Puppet.expects(:warning).at_least_once + Puppet.expects(:warning).with(regexp_matches(/Deprecation notice: Node inheritance is not supported in Puppet >= 4\.0\.0/)) + catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new("nodename")) + node default {} + node nodename inherits default { } + MANIFEST + end + end end