diff --git a/lib/puppet/pops/issues.rb b/lib/puppet/pops/issues.rb index e025410fb..ba4534eb9 100644 --- a/lib/puppet/pops/issues.rb +++ b/lib/puppet/pops/issues.rb @@ -1,640 +1,640 @@ # 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 '$#{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 a variable is not a NAME # 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)} has no effect. A value-producing expression without other effect 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)} has no effect. #{label.a_an_uc(container)} can not end with a value-producing expression without other effect" 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 SYNTAX_ERROR = hard_issue :SYNTAX_ERROR, :where do "Syntax error at #{where}" end ILLEGAL_CLASS_REFERENCE = hard_issue :ILLEGAL_CLASS_REFERENCE do 'Illegal class reference' end ILLEGAL_FULLY_QUALIFIED_CLASS_REFERENCE = hard_issue :ILLEGAL_FULLY_QUALIFIED_CLASS_REFERENCE do 'Illegal fully qualified class reference' end ILLEGAL_FULLY_QUALIFIED_NAME = hard_issue :ILLEGAL_FULLY_QUALIFIED_NAME do 'Illegal fully qualified name' end ILLEGAL_NAME_OR_BARE_WORD = hard_issue :ILLEGAL_NAME_OR_BARE_WORD do 'Illegal name or bare word' end ILLEGAL_NUMBER = hard_issue :ILLEGAL_NUMBER do 'Illegal number' end ILLEGAL_UNICODE_ESCAPE = issue :ILLEGAL_UNICODE_ESCAPE do - "Unicode escape '\\u' was not followed by 4 hex digits" + "Unicode escape '\\u' was not followed by 4 hex digits or 1-6 hex digits in {} or was > 10ffff" end INVALID_HEX_NUMBER = hard_issue :INVALID_HEX_NUMBER, :value do "Not a valid hex number #{value}" end INVALID_OCTAL_NUMBER = hard_issue :INVALID_OCTAL_NUMBER, :value do "Not a valid octal number #{value}" end INVALID_DECIMAL_NUMBER = hard_issue :INVALID_DECIMAL_NUMBER, :value do "Not a valid decimal number #{value}" end NO_INPUT_TO_LEXER = hard_issue :NO_INPUT_TO_LEXER do "Internal Error: No string or file given to lexer to process." end UNRECOGNIZED_ESCAPE = issue :UNRECOGNIZED_ESCAPE, :ch do "Unrecognized escape sequence '\\#{ch}'" end UNCLOSED_QUOTE = hard_issue :UNCLOSED_QUOTE, :after, :followed_by do "Unclosed quote after #{after} followed by '#{followed_by}'" end EPP_INTERNAL_ERROR = hard_issue :EPP_INTERNAL_ERROR, :error do "Internal error: #{error}" end EPP_UNBALANCED_TAG = hard_issue :EPP_UNBALANCED_TAG do 'Unbalanced epp tag, reached without closing tag.' end EPP_UNBALANCED_COMMENT = hard_issue :EPP_UNBALANCED_COMMENT do 'Reaching end after opening <%# without seeing %>' end EPP_UNBALANCED_EXPRESSION = hard_issue :EPP_UNBALANCED_EXPRESSION do 'Unbalanced embedded expression - opening <% and reaching end of input' end HEREDOC_UNCLOSED_PARENTHESIS = hard_issue :HEREDOC_UNCLOSED_PARENTHESIS, :followed_by do "Unclosed parenthesis after '@(' followed by '#{followed_by}'" end HEREDOC_WITHOUT_END_TAGGED_LINE = hard_issue :HEREDOC_WITHOUT_END_TAGGED_LINE do 'Heredoc without end-tagged line' end HEREDOC_INVALID_ESCAPE = hard_issue :HEREDOC_INVALID_ESCAPE, :actual do "Invalid heredoc escape char. Only t, r, n, s, u, L, $ allowed. Got '#{actual}'" end HEREDOC_INVALID_SYNTAX = hard_issue :HEREDOC_INVALID_SYNTAX do 'Invalid syntax in heredoc expected @(endtag[:syntax][/escapes])' end HEREDOC_WITHOUT_TEXT = hard_issue :HEREDOC_WITHOUT_TEXT do 'Heredoc without any following lines of text' end HEREDOC_MULTIPLE_AT_ESCAPES = hard_issue :HEREDOC_MULTIPLE_AT_ESCAPES, :escapes do "An escape char for @() may only appear once. Got '#{escapes.join(', ')}'" end end diff --git a/lib/puppet/pops/parser/slurp_support.rb b/lib/puppet/pops/parser/slurp_support.rb index 175ac80e2..64e79af24 100644 --- a/lib/puppet/pops/parser/slurp_support.rb +++ b/lib/puppet/pops/parser/slurp_support.rb @@ -1,97 +1,108 @@ # This module is an integral part of the Lexer. # It defines the string slurping behavior - finding the string and non string parts in interpolated # strings, translating escape sequences in strings to their single character equivalence. # # PERFORMANCE NOTE: The various kinds of slurping could be made even more generic, but requires # additional parameter passing and evaluation of conditional logic. # TODO: More detailed performance analysis of excessive character escaping and interpolation. # module Puppet::Pops::Parser::SlurpSupport include Puppet::Pops::Parser::LexerSupport SLURP_SQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*[']/ SLURP_DQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*(["]|[$]\{?)/ SLURP_UQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*([$]\{?|\z)/ SLURP_ALL_PATTERN = /.*(\z)/m SQ_ESCAPES = %w{ \\ ' } DQ_ESCAPES = %w{ \\ $ ' " r n t s u}+["\r\n", "\n"] UQ_ESCAPES = %w{ \\ $ r n t s u}+["\r\n", "\n"] def slurp_sqstring # skip the leading ' @scanner.pos += 1 str = slurp(@scanner, SLURP_SQ_PATTERN, SQ_ESCAPES, :ignore_invalid_escapes) lex_error(Puppet::Pops::Issues::UNCLOSED_QUOTE, :after => "\"'\"", :followed_by => followed_by) unless str str[0..-2] # strip closing "'" from result end def slurp_dqstring scn = @scanner last = scn.matched str = slurp(scn, SLURP_DQ_PATTERN, DQ_ESCAPES, false) unless str lex_error(Puppet::Pops::Issues::UNCLOSED_QUOTE, :after => format_quote(last), :followed_by => followed_by) end # Terminator may be a single char '"', '$', or two characters '${' group match 1 (scn[1]) from the last slurp holds this terminator = scn[1] [str[0..(-1 - terminator.length)], terminator] end # Copy from old lexer - can do much better def slurp_uqstring scn = @scanner last = scn.matched ignore = true str = slurp(scn, @lexing_context[:uq_slurp_pattern], @lexing_context[:escapes], :ignore_invalid_escapes) # Terminator may be a single char '$', two characters '${', or empty string '' at the end of intput. # Group match 1 holds this. # The exceptional case is found by looking at the subgroup 1 of the most recent match made by the scanner (i.e. @scanner[1]). # This is the last match made by the slurp method (having called scan_until on the scanner). # If there is a terminating character is must be stripped and returned separately. # terminator = scn[1] [str[0..(-1 - terminator.length)], terminator] end # Slurps a string from the given scanner until the given pattern and then replaces any escaped # characters given by escapes into their control-character equivalent or in case of line breaks, replaces the # pattern \r?\n with an empty string. # The returned string contains the terminating character. Returns nil if the scanner can not scan until the given # pattern. # def slurp(scanner, pattern, escapes, ignore_invalid_escapes) str = scanner.scan_until(pattern) || return # Process unicode escapes first as they require getting 4 hex digits # If later a \u is found it is warned not to be a unicode escape if escapes.include?('u') - str.gsub!(/\\u([\da-fA-F]{4})/m) { - [$1.hex].pack("U") + str.gsub!(/\\u(?:([\da-fA-F]{4})|\{([\da-fA-F]{1,6})\})/m) { + [($1 || $2).hex].pack("U") } end + begin str.gsub!(/\\([^\r\n]|(?:\r?\n))/m) { ch = $1 if escapes.include? ch case ch when 'r' ; "\r" when 'n' ; "\n" when 't' ; "\t" when 's' ; " " when 'u' lex_warning(Puppet::Pops::Issues::ILLEGAL_UNICODE_ESCAPE) "\\u" when "\n" ; '' when "\r\n"; '' else ch end else lex_warning(Puppet::Pops::Issues::UNRECOGNIZED_ESCAPE, :ch => ch) unless ignore_invalid_escapes "\\#{ch}" end } + rescue ArgumentError => e + # A invalid byte sequence may be the result of faulty input as well, but that could not possibly + # have reached this far... Unfortunately there is no more specific error and a match on message is + # required to differentiate from other internal problems. + if e.message =~ /invalid byte sequence/ + lex_error(Puppet::Pops::Issues::ILLEGAL_UNICODE_ESCAPE) + else + raise e + end + end str end end diff --git a/spec/unit/pops/parser/lexer2_spec.rb b/spec/unit/pops/parser/lexer2_spec.rb index 0250888da..f9b9d0ae2 100644 --- a/spec/unit/pops/parser/lexer2_spec.rb +++ b/spec/unit/pops/parser/lexer2_spec.rb @@ -1,618 +1,630 @@ require 'spec_helper' require 'matchers/match_tokens2' require 'puppet/pops' require 'puppet/pops/parser/lexer2' module EgrammarLexer2Spec def tokens_scanned_from(s) lexer = Puppet::Pops::Parser::Lexer2.new lexer.string = s tokens = lexer.fullscan[0..-2] end def epp_tokens_scanned_from(s) lexer = Puppet::Pops::Parser::Lexer2.new lexer.string = s tokens = lexer.fullscan_epp[0..-2] end end describe 'Lexer2' do include EgrammarLexer2Spec { :LISTSTART => '[', :RBRACK => ']', :LBRACE => '{', :RBRACE => '}', :LPAREN => '(', :RPAREN => ')', :EQUALS => '=', :ISEQUAL => '==', :GREATEREQUAL => '>=', :GREATERTHAN => '>', :LESSTHAN => '<', :LESSEQUAL => '<=', :NOTEQUAL => '!=', :NOT => '!', :COMMA => ',', :DOT => '.', :COLON => ':', :AT => '@', :LLCOLLECT => '<<|', :RRCOLLECT => '|>>', :LCOLLECT => '<|', :RCOLLECT => '|>', :SEMIC => ';', :QMARK => '?', :OTHER => '\\', :FARROW => '=>', :PARROW => '+>', :APPENDS => '+=', :DELETES => '-=', :PLUS => '+', :MINUS => '-', :DIV => '/', :TIMES => '*', :LSHIFT => '<<', :RSHIFT => '>>', :MATCH => '=~', :NOMATCH => '!~', :IN_EDGE => '->', :OUT_EDGE => '<-', :IN_EDGE_SUB => '~>', :OUT_EDGE_SUB => '<~', :PIPE => '|', }.each do |name, string| it "should lex a token named #{name.to_s}" do tokens_scanned_from(string).should match_tokens2(name) end end it "should lex [ in position after non whitespace as LBRACK" do tokens_scanned_from("a[").should match_tokens2(:NAME, :LBRACK) end { "case" => :CASE, "class" => :CLASS, "default" => :DEFAULT, "define" => :DEFINE, # "import" => :IMPORT, # done as a function in egrammar "if" => :IF, "elsif" => :ELSIF, "else" => :ELSE, "inherits" => :INHERITS, "node" => :NODE, "and" => :AND, "or" => :OR, "undef" => :UNDEF, "false" => :BOOLEAN, "true" => :BOOLEAN, "in" => :IN, "unless" => :UNLESS, }.each do |string, name| it "should lex a keyword from '#{string}'" do tokens_scanned_from(string).should match_tokens2(name) end end # TODO: Complete with all edge cases [ 'A', 'A::B', '::A', '::A::B',].each do |string| it "should lex a CLASSREF on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:CLASSREF, string]) end end # TODO: Complete with all edge cases [ 'a', 'a::b', '::a', '::a::b',].each do |string| it "should lex a NAME on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:NAME, string]) end end [ 'a-b', 'a--b', 'a-b-c', '_x'].each do |string| it "should lex a BARE WORD STRING on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:WORD, string]) end end [ '_x::y', 'x::_y'].each do |string| it "should consider the bare word '#{string}' to be a WORD" do tokens_scanned_from(string).should match_tokens2(:WORD) end end { '-a' => [:MINUS, :NAME], '--a' => [:MINUS, :MINUS, :NAME], 'a-' => [:NAME, :MINUS], 'a- b' => [:NAME, :MINUS, :NAME], 'a--' => [:NAME, :MINUS, :MINUS], 'a-$3' => [:NAME, :MINUS, :VARIABLE], }.each do |source, expected| it "should lex leading and trailing hyphens from #{source}" do tokens_scanned_from(source).should match_tokens2(*expected) end end { 'false'=>false, 'true'=>true}.each do |string, value| it "should lex a BOOLEAN on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:BOOLEAN, value]) end end [ '0', '1', '2982383139'].each do |string| it "should lex a decimal integer NUMBER on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:NUMBER, string]) end end { ' 1' => '1', '1 ' => '1', ' 1 ' => '1'}.each do |string, value| it "should lex a NUMBER with surrounding space '#{string}'" do tokens_scanned_from(string).should match_tokens2([:NUMBER, value]) end end [ '0.0', '0.1', '0.2982383139', '29823.235', '10e23', '10e-23', '1.234e23'].each do |string| it "should lex a decimal floating point NUMBER on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:NUMBER, string]) end end [ '00', '01', '0123', '0777'].each do |string| it "should lex an octal integer NUMBER on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:NUMBER, string]) end end [ '0x0', '0x1', '0xa', '0xA', '0xabcdef', '0xABCDEF'].each do |string| it "should lex an hex integer NUMBER on the form '#{string}'" do tokens_scanned_from(string).should match_tokens2([:NUMBER, string]) end end { "''" => '', "'a'" => 'a', "'a\\'b'" =>"a'b", "'a\\rb'" =>"a\\rb", "'a\\nb'" =>"a\\nb", "'a\\tb'" =>"a\\tb", "'a\\sb'" =>"a\\sb", "'a\\$b'" =>"a\\$b", "'a\\\"b'" =>"a\\\"b", "'a\\\\b'" =>"a\\b", "'a\\\\'" =>"a\\", }.each do |source, expected| it "should lex a single quoted STRING on the form #{source}" do tokens_scanned_from(source).should match_tokens2([:STRING, expected]) end end { "''" => [2, ""], "'a'" => [3, "a"], "'a\\'b'" => [6, "a'b"], }.each do |source, expected| it "should lex a single quoted STRING on the form #{source} as having length #{expected[0]}" do length, value = expected tokens_scanned_from(source).should match_tokens2([:STRING, value, {:line => 1, :pos=>1, :length=> length}]) end end { '""' => '', '"a"' => 'a', '"a\'b"' => "a'b", }.each do |source, expected| it "should lex a double quoted STRING on the form #{source}" do tokens_scanned_from(source).should match_tokens2([:STRING, expected]) end end { '"a$x b"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>2 }], [:VARIABLE, 'x', {:line => 1, :pos=>3, :length=>2 }], [:DQPOST, ' b', {:line => 1, :pos=>5, :length=>3 }]], '"a$x.b"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>2 }], [:VARIABLE, 'x', {:line => 1, :pos=>3, :length=>2 }], [:DQPOST, '.b', {:line => 1, :pos=>5, :length=>3 }]], '"$x.b"' => [[:DQPRE, '', {:line => 1, :pos=>1, :length=>1 }], [:VARIABLE, 'x', {:line => 1, :pos=>2, :length=>2 }], [:DQPOST, '.b', {:line => 1, :pos=>4, :length=>3 }]], '"a$x"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>2 }], [:VARIABLE, 'x', {:line => 1, :pos=>3, :length=>2 }], [:DQPOST, '', {:line => 1, :pos=>5, :length=>1 }]], '"a${x}"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>4 }], [:VARIABLE, 'x', {:line => 1, :pos=>5, :length=>1 }], [:DQPOST, '', {:line => 1, :pos=>7, :length=>1 }]], '"a${_x}"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>4 }], [:VARIABLE, '_x', {:line => 1, :pos=>5, :length=>2 }], [:DQPOST, '', {:line => 1, :pos=>8, :length=>1 }]], '"a${y::_x}"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>4 }], [:VARIABLE, 'y::_x', {:line => 1, :pos=>5, :length=>5 }], [:DQPOST, '', {:line => 1, :pos=>11, :length=>1 }]], }.each do |source, expected| it "should lex an interpolated variable 'x' from #{source}" do tokens_scanned_from(source).should match_tokens2(*expected) end end { '"$"' => '$', '"a$"' => 'a$', '"a$%b"' => "a$%b", '"a$$"' => "a$$", '"a$$%"' => "a$$%", }.each do |source, expected| it "should lex interpolation including false starts #{source}" do tokens_scanned_from(source).should match_tokens2([:STRING, expected]) end end it "differentiates between foo[x] and foo [x] (whitespace)" do tokens_scanned_from("$a[1]").should match_tokens2(:VARIABLE, :LBRACK, :NUMBER, :RBRACK) tokens_scanned_from("$a [1]").should match_tokens2(:VARIABLE, :LISTSTART, :NUMBER, :RBRACK) tokens_scanned_from("a[1]").should match_tokens2(:NAME, :LBRACK, :NUMBER, :RBRACK) tokens_scanned_from("a [1]").should match_tokens2(:NAME, :LISTSTART, :NUMBER, :RBRACK) tokens_scanned_from(" if \n\r\t\nif if ").should match_tokens2(:IF, :IF, :IF) end it "skips whitepsace" do tokens_scanned_from(" if if if ").should match_tokens2(:IF, :IF, :IF) tokens_scanned_from(" if \n\r\t\nif if ").should match_tokens2(:IF, :IF, :IF) end it "skips single line comments" do tokens_scanned_from("if # comment\nif").should match_tokens2(:IF, :IF) end ["if /* comment */\nif", "if /* comment\n */\nif", "if /*\n comment\n */\nif", ].each do |source| it "skips multi line comments" do tokens_scanned_from(source).should match_tokens2(:IF, :IF) end end { "=~" => [:MATCH, "=~ /./"], "!~" => [:NOMATCH, "!~ /./"], "," => [:COMMA, ", /./"], "(" => [:LPAREN, "( /./"], "[" => [:LISTSTART, "[ /./"], "[" => [[:NAME, :LBRACK], "a[ /./"], "[" => [[:NAME, :LISTSTART], "a [ /./"], "{" => [:LBRACE, "{ /./"], "+" => [:PLUS, "+ /./"], "-" => [:MINUS, "- /./"], "*" => [:TIMES, "* /./"], ";" => [:SEMIC, "; /./"], }.each do |token, entry| it "should lex regexp after '#{token}'" do expected = [entry[0], :REGEX].flatten tokens_scanned_from(entry[1]).should match_tokens2(*expected) end end it "should lex a simple expression" do tokens_scanned_from('1 + 1').should match_tokens2([:NUMBER, '1'], :PLUS, [:NUMBER, '1']) end { "1" => ["1 /./", [:NUMBER, :DIV, :DOT, :DIV]], "'a'" => ["'a' /./", [:STRING, :DIV, :DOT, :DIV]], "true" => ["true /./", [:BOOLEAN, :DIV, :DOT, :DIV]], "false" => ["false /./", [:BOOLEAN, :DIV, :DOT, :DIV]], "/./" => ["/./ /./", [:REGEX, :DIV, :DOT, :DIV]], "a" => ["a /./", [:NAME, :DIV, :DOT, :DIV]], "A" => ["A /./", [:CLASSREF, :DIV, :DOT, :DIV]], ")" => [") /./", [:RPAREN, :DIV, :DOT, :DIV]], "]" => ["] /./", [:RBRACK, :DIV, :DOT, :DIV]], "|>" => ["|> /./", [:RCOLLECT, :DIV, :DOT, :DIV]], "|>>" => ["|>> /./", [:RRCOLLECT, :DIV, :DOT, :DIV]], "$x" => ["$x /1/", [:VARIABLE, :DIV, :NUMBER, :DIV]], "a-b" => ["a-b /1/", [:WORD, :DIV, :NUMBER, :DIV]], '"a$a"' => ['"a$a" /./', [:DQPRE, :VARIABLE, :DQPOST, :DIV, :DOT, :DIV]], }.each do |token, entry| it "should not lex regexp after '#{token}'" do tokens_scanned_from(entry[ 0 ]).should match_tokens2(*entry[ 1 ]) end end it 'should lex assignment' do tokens_scanned_from("$a = 10").should match_tokens2([:VARIABLE, "a"], :EQUALS, [:NUMBER, '10']) end # TODO: Tricky, and heredoc not supported yet # it "should not lex regexp after heredoc" do # tokens_scanned_from("1 / /./").should match_tokens2(:NUMBER, :DIV, :REGEX) # end it "should lex regexp at beginning of input" do tokens_scanned_from(" /./").should match_tokens2(:REGEX) end it "should lex regexp right of div" do tokens_scanned_from("1 / /./").should match_tokens2(:NUMBER, :DIV, :REGEX) end context 'when lexer lexes heredoc' do it 'lexes tag, syntax and escapes, margin and right trim' do code = <<-CODE @(END:syntax/t) Tex\\tt\\n |- END CODE tokens_scanned_from(code).should match_tokens2([:HEREDOC, 'syntax'], :SUBLOCATE, [:STRING, "Tex\tt\\n"]) end it 'lexes "tag", syntax and escapes, margin, right trim and interpolation' do code = <<-CODE @("END":syntax/t) Tex\\tt\\n$var After |- END CODE tokens_scanned_from(code).should match_tokens2( [:HEREDOC, 'syntax'], :SUBLOCATE, [:DQPRE, "Tex\tt\\n"], [:VARIABLE, "var"], [:DQPOST, " After"] ) end context 'with bad syntax' do def expect_issue(code, issue) expect { tokens_scanned_from(code) }.to raise_error(Puppet::ParseErrorWithIssue) { |e| expect(e.issue_code).to be(issue.issue_code) } end it 'detects and reports HEREDOC_UNCLOSED_PARENTHESIS' do code = <<-CODE @(END:syntax/t Text |- END CODE expect_issue(code, Puppet::Pops::Issues::HEREDOC_UNCLOSED_PARENTHESIS) end it 'detects and reports HEREDOC_WITHOUT_END_TAGGED_LINE' do code = <<-CODE @(END:syntax/t) Text CODE expect_issue(code, Puppet::Pops::Issues::HEREDOC_WITHOUT_END_TAGGED_LINE) end it 'detects and reports HEREDOC_INVALID_ESCAPE' do code = <<-CODE @(END:syntax/x) Text |- END CODE expect_issue(code, Puppet::Pops::Issues::HEREDOC_INVALID_ESCAPE) end it 'detects and reports HEREDOC_INVALID_SYNTAX' do code = <<-CODE @(END:syntax/t/p) Text |- END CODE expect_issue(code, Puppet::Pops::Issues::HEREDOC_INVALID_SYNTAX) end it 'detects and reports HEREDOC_WITHOUT_TEXT' do code = '@(END:syntax/t)' expect_issue(code, Puppet::Pops::Issues::HEREDOC_WITHOUT_TEXT) end it 'detects and reports HEREDOC_MULTIPLE_AT_ESCAPES' do code = <<-CODE @(END:syntax/tst) Tex\\tt\\n |- END CODE expect_issue(code, Puppet::Pops::Issues::HEREDOC_MULTIPLE_AT_ESCAPES) end end end context 'when dealing with multi byte characters' do it 'should support unicode characters' do code = <<-CODE "x\\u2713y" CODE if Puppet::Pops::Parser::Locator::RUBYVER < Puppet::Pops::Parser::Locator::RUBY_1_9_3 # Ruby 1.8.7 reports the multibyte char as several octal characters tokens_scanned_from(code).should match_tokens2([:STRING, "x\342\234\223y"]) else # >= Ruby 1.9.3 reports \u tokens_scanned_from(code).should match_tokens2([:STRING, "x\u2713y"]) end end + it 'should support unicode characters in long form' do + code = <<-CODE + "x\\u{1f452}y" + CODE + if Puppet::Pops::Parser::Locator::RUBYVER < Puppet::Pops::Parser::Locator::RUBY_1_9_3 + # Ruby 1.8.7 reports the multibyte char as several octal characters + tokens_scanned_from(code).should match_tokens2([:STRING, "x\360\237\221\222y"]) + else + # >= Ruby 1.9.3 reports \u + tokens_scanned_from(code).should match_tokens2([:STRING, "x\u{1f452}y"]) + end + end it 'should not select LISTSTART token when preceded by multibyte chars' do # This test is sensitive to the number of multibyte characters and position of the expressions # within the string - it is designed to fail if the position is calculated on the byte offset of the '[' # instead of the char offset. # code = "$a = '\u00f6\u00fc\u00fc\u00fc\u00fc\u00e4\u00e4\u00f6\u00e4'\nnotify {'x': message => B['dkda'] }\n" tokens_scanned_from(code).should match_tokens2( :VARIABLE, :EQUALS, :STRING, [:NAME, 'notify'], :LBRACE, [:STRING, 'x'], :COLON, :NAME, :FARROW, :CLASSREF, :LBRACK, :STRING, :RBRACK, :RBRACE) end end context 'when lexing epp' do it 'epp can contain just text' do code = <<-CODE This is just text CODE epp_tokens_scanned_from(code).should match_tokens2(:EPP_START, [:RENDER_STRING, " This is just text\n"]) end it 'epp can contain text with interpolated rendered expressions' do code = <<-CODE This is <%= $x %> just text CODE epp_tokens_scanned_from(code).should match_tokens2( :EPP_START, [:RENDER_STRING, " This is "], [:RENDER_EXPR, nil], [:VARIABLE, "x"], [:EPP_END, "%>"], [:RENDER_STRING, " just text\n"] ) end it 'epp can contain text with trimmed interpolated rendered expressions' do code = <<-CODE This is <%= $x -%> just text CODE epp_tokens_scanned_from(code).should match_tokens2( :EPP_START, [:RENDER_STRING, " This is "], [:RENDER_EXPR, nil], [:VARIABLE, "x"], [:EPP_END_TRIM, "-%>"], [:RENDER_STRING, "just text\n"] ) end it 'epp can contain text with expressions that are not rendered' do code = <<-CODE This is <% $x=10 %> just text CODE epp_tokens_scanned_from(code).should match_tokens2( :EPP_START, [:RENDER_STRING, " This is "], [:VARIABLE, "x"], :EQUALS, [:NUMBER, "10"], [:RENDER_STRING, " just text\n"] ) end it 'epp can skip leading space in tail text' do code = <<-CODE This is <% $x=10 -%> just text CODE epp_tokens_scanned_from(code).should match_tokens2( :EPP_START, [:RENDER_STRING, " This is "], [:VARIABLE, "x"], :EQUALS, [:NUMBER, "10"], [:RENDER_STRING, "just text\n"] ) end it 'epp can skip comments' do code = <<-CODE This is <% $x=10 -%> <%# This is an epp comment -%> just text CODE epp_tokens_scanned_from(code).should match_tokens2( :EPP_START, [:RENDER_STRING, " This is "], [:VARIABLE, "x"], :EQUALS, [:NUMBER, "10"], [:RENDER_STRING, "just text\n"] ) end it 'epp can escape epp tags' do code = <<-CODE This is <% $x=10 -%> <%% this is escaped epp %%> CODE epp_tokens_scanned_from(code).should match_tokens2( :EPP_START, [:RENDER_STRING, " This is "], [:VARIABLE, "x"], :EQUALS, [:NUMBER, "10"], [:RENDER_STRING, "<% this is escaped epp %>\n"] ) end context 'with bad epp syntax' do def expect_issue(code, issue) expect { epp_tokens_scanned_from(code) }.to raise_error(Puppet::ParseErrorWithIssue) { |e| expect(e.issue_code).to be(issue.issue_code) } end it 'detects and reports EPP_UNBALANCED_TAG' do expect_issue('<% asf', Puppet::Pops::Issues::EPP_UNBALANCED_TAG) end it 'detects and reports EPP_UNBALANCED_COMMENT' do expect_issue('<%# asf', Puppet::Pops::Issues::EPP_UNBALANCED_COMMENT) end it 'detects and reports EPP_UNBALANCED_EXPRESSION' do expect_issue('asf <%', Puppet::Pops::Issues::EPP_UNBALANCED_EXPRESSION) end end end context 'when parsing bad code' do def expect_issue(code, issue) expect { tokens_scanned_from(code) }.to raise_error(Puppet::ParseErrorWithIssue) do |e| expect(e.issue_code).to be(issue.issue_code) end end it 'detects and reports issue ILLEGAL_CLASS_REFERENCE' do expect_issue('A::3', Puppet::Pops::Issues::ILLEGAL_CLASS_REFERENCE) end it 'detects and reports issue ILLEGAL_FULLY_QUALIFIED_CLASS_REFERENCE' do expect_issue('::A::3', Puppet::Pops::Issues::ILLEGAL_FULLY_QUALIFIED_CLASS_REFERENCE) end it 'detects and reports issue ILLEGAL_FULLY_QUALIFIED_NAME' do expect_issue('::a::3', Puppet::Pops::Issues::ILLEGAL_FULLY_QUALIFIED_NAME) end it 'detects and reports issue ILLEGAL_NAME_OR_BARE_WORD' do expect_issue('a::3', Puppet::Pops::Issues::ILLEGAL_NAME_OR_BARE_WORD) end it 'detects and reports issue ILLEGAL_NUMBER' do expect_issue('3g', Puppet::Pops::Issues::ILLEGAL_NUMBER) end it 'detects and reports issue INVALID_HEX_NUMBER' do expect_issue('0x3g', Puppet::Pops::Issues::INVALID_HEX_NUMBER) end it 'detects and reports issue INVALID_OCTAL_NUMBER' do expect_issue('038', Puppet::Pops::Issues::INVALID_OCTAL_NUMBER) end it 'detects and reports issue INVALID_DECIMAL_NUMBER' do expect_issue('4.3g', Puppet::Pops::Issues::INVALID_DECIMAL_NUMBER) end it 'detects and reports issue NO_INPUT_TO_LEXER' do expect { Puppet::Pops::Parser::Lexer2.new.fullscan }.to raise_error(Puppet::ParseErrorWithIssue) { |e| expect(e.issue_code).to be(Puppet::Pops::Issues::NO_INPUT_TO_LEXER.issue_code) } end it 'detects and reports issue UNCLOSED_QUOTE' do expect_issue('"asd', Puppet::Pops::Issues::UNCLOSED_QUOTE) end end end