# frozen_string_literal: true

# Defines classes to deal with issues, and message formatting and defines constants with Issues.
# @api public
#
module Puppet::Pops
module 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
        raise RuntimeError, _("Error while reporting issue: %{code}. %{message}") % { code: issue_code, message: 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

    # Obtains the label provider given as a key `:label` in the hash passed to #format. The label provider is
    # return if no arguments are given. If given an argument, returns the result of calling #label on the label
    # provider.
    #
    # @param args [Object] one object to obtain a label for or zero arguments to obtain the label provider
    # @return [LabelProvider,String] the label provider or label depending on if an argument is given or not
    # @raise [Puppet::Error] if no label provider is found
    def label(*args)
      args.empty? ? label_provider : label_provider.label(args[0])
    end

    # Returns the label provider given as key `:label` in the hash passed to #format.
    # @return [LabelProvider] the label provider
    # @raise [Puppet::Error] if no label provider is found
    def label_provider
      label_provider = @data[:label]
      # TRANSLATORS ":label" is a keyword and should not be translated
      raise Puppet::Error, _('Label provider key :label must be set to produce the text of the message!') unless label_provider

      label_provider
    end

    # Returns the label provider given as a key in the hash passed to #format.
    #
    def semantic
      # TRANSLATORS ":semantic" is a keyword and should not be translated
      raise Puppet::Error, _('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
    _("%{issue} may not have a name containing a hyphen. The name '%{name}' is not legal") % { issue: label.a_an_uc(semantic), name: name }
  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") % { name: name }
  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

  NOT_ABSOLUTE_TOP_LEVEL = hard_issue :NOT_ABSOLUTE_TOP_LEVEL do
    _("%{value} may only appear at toplevel") % { value: label.a_an_uc(semantic) }
  end

  CROSS_SCOPE_ASSIGNMENT = hard_issue :CROSS_SCOPE_ASSIGNMENT, :name do
    _("Illegal attempt to assign to '%{name}'. Cannot assign to variables in other namespaces") % { name: name }
  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 '%{value}'. Not an assignable reference") % { value: label.a_an(semantic) }
  end

  # Variables are immutable, cannot reassign in the same assignment scope
  ILLEGAL_REASSIGNMENT = hard_issue :ILLEGAL_REASSIGNMENT, :name do
    if Validation::Checker4_0::RESERVED_PARAMETERS[name]
      _("Cannot reassign built in (or already assigned) variable '$%{var}'") % { var: name }
    else
      _("Cannot reassign variable '$%{var}'") % { var: name }
    end
  end

  # Variables facts and trusted
  ILLEGAL_RESERVED_ASSIGNMENT = hard_issue :ILLEGAL_RESERVED_ASSIGNMENT, :name do
    _("Attempt to assign to a reserved variable name: '$%{var}'") % { var: 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 '$%{var}'. Numeric variables are not assignable") % { var: varname }
  end

  # Assignment can only be made to certain types of left hand expressions such as variables.
  ILLEGAL_ASSIGNMENT_CONTEXT = hard_issue :ILLEGAL_ASSIGNMENT_CONTEXT do
    _("Assignment not allowed here")
  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)") % { name: name }
  end

  ILLEGAL_NONLITERAL_PARAMETER_TYPE = issue :ILLEGAL_NONLITERAL_PARAMETER_TYPE, :name, :type_class do
    _("The parameter '$%{name}' must be a literal type, not %{type_class}") % { name: name, type_class: label.a_an(type_class) }
  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 %{value} via [index/key]. Not an assignable reference") % { value: label.a_an(semantic) }
  end

  ILLEGAL_MULTI_ASSIGNMENT_SIZE = hard_issue :ILLEGAL_MULTI_ASSIGNMENT_SIZE, :expected, :actual do
    _("Mismatched number of assignable entries and values, expected %{expected}, got %{actual}") % { expected: expected, actual: actual }
  end

  MISSING_MULTI_ASSIGNMENT_KEY = hard_issue :MISSING_MULTI_ASSIGNMENT_KEY, :key do
    _("No value for required key '%{key}' in assignment to variables from hash") % { key: key }
  end

  MISSING_MULTI_ASSIGNMENT_VARIABLE = hard_issue :MISSING_MULTI_ASSIGNMENT_VARIABLE, :name do
    _("No value for required variable '$%{name}' in assignment to variables from class reference") % { name: name }
  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.puppet.com/remove-plus-equals") % { operator: operator }
  end

  # For unsupported operators (e.g. += and -= in puppet 4).
  #
  UNSUPPORTED_OPERATOR = hard_issue :UNSUPPORTED_OPERATOR, :operator do
    _("The operator '%{operator}' is not supported.") % { operator: operator }
  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 %{value} is not supported.") % { operator: operator, value: label.a_an(semantic) }
  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 %{left}.") % { operator: operator, left: label.a_an(left_value) }
  end

  OPERATOR_NOT_APPLICABLE_WHEN = hard_issue :OPERATOR_NOT_APPLICABLE_WHEN, :operator, :left_value, :right_value do
    _("Operator '%{operator}' is not applicable to %{left} when right side is %{right}.") % { operator: operator, left: label.a_an(left_value), right: label.a_an(right_value) }
  end

  COMPARISON_NOT_POSSIBLE = hard_issue :COMPARISON_NOT_POSSIBLE, :operator, :left_value, :right_value, :detail do
    _("Comparison of: %{left} %{operator} %{right}, is not possible. Caused by '%{detail}'.") % { left: label(left_value), operator: operator, right: label(right_value), detail: 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}'.") % { detail: detail }
  end

  MATCH_NOT_STRING = hard_issue :MATCH_NOT_STRING, :left_value do
    _("Left match operand must result in a String value. Got %{left}.") % { left: 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. %{value} does not produce a value") % { value: label.a_an_uc(semantic) }
  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 %{attr}. This operator can not be used in %{expression}") % { attr: name, expression: 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*)*$/") % { name: name }
  end

  ILLEGAL_SINGLE_TYPE_MAPPING = hard_issue :ILLEGAL_TYPE_MAPPING, :expression do
    _("Illegal type mapping. Expected a Type on the left side, got %{expression}") % { expression: label.a_an_uc(semantic) }
  end

  ILLEGAL_REGEXP_TYPE_MAPPING = hard_issue :ILLEGAL_TYPE_MAPPING, :expression do
    _("Illegal type mapping. Expected a Tuple[Regexp,String] on the left side, got %{expression}") % { expression: label.a_an_uc(semantic) }
  end

  ILLEGAL_PARAM_NAME = hard_issue :ILLEGAL_PARAM_NAME, :name do
    _("Illegal parameter name. The given name '%{name}' does not conform to the naming rule /^[a-z_]\\w*$/") % { name: name }
  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*)$/") % { name: name }
  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") % { name: name }
  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") % { name: name }
  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)") % { hostname: hostname }
  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. %{expression} is unacceptable as %{feature} in %{container}") % { expression: label.a_an_uc(semantic), feature: feature, container: 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. %{expression} did not produce a variable name (String or Numeric).") % { expression: label.a_an_uc(semantic) }
  end

  # Issues when an expression is used illegally in a query.
  # query only supports == and !=, and not <, > etc.
  #
  ILLEGAL_QUERY_EXPRESSION = hard_issue :ILLEGAL_QUERY_EXPRESSION do
    _("Illegal query expression. %{expression} cannot be used in a query") % { expression: label.a_an_uc(semantic) }
  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

  CLASS_NOT_VIRTUALIZABLE = issue :CLASS_NOT_VIRTUALIZABLE do
    _("Classes 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 %{expression}, %{count} values given for max 1") % { expression: label.a_an(semantic), count: count }
  end

  # Issues when expressions that are not implemented or activated in the current version are used.
  #
  UNSUPPORTED_EXPRESSION = issue :UNSUPPORTED_EXPRESSION do
    _("Expressions of type %{expression} are not supported in this version of Puppet") % { expression: label.a_an(semantic) }
  end

  ILLEGAL_RELATIONSHIP_OPERAND_TYPE = issue :ILLEGAL_RELATIONSHIP_OPERAND_TYPE, :operand do
    _("Illegal relationship operand, can not form a relationship with %{expression}. A Catalog type is required.") % { expression: label.a_an(operand) }
  end

  NOT_CATALOG_TYPE = issue :NOT_CATALOG_TYPE, :type do
    _("Illegal relationship operand, can not form a relationship with something of type %{expression_type}. A Catalog type is required.") % { expression_type: type }
  end

  BAD_STRING_SLICE_ARITY = issue :BAD_STRING_SLICE_ARITY, :actual do
    _("String supports [] with one or two arguments. Got %{actual}") % { actual: 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}") % { actual: actual }
  end

  BAD_ARRAY_SLICE_ARITY = issue :BAD_ARRAY_SLICE_ARITY, :actual do
    _("Array supports [] with one or two arguments. Got %{actual}") % { actual: actual }
  end

  BAD_HASH_SLICE_ARITY = issue :BAD_HASH_SLICE_ARITY, :actual do
    _("Hash supports [] with one or more arguments. Got %{actual}") % { actual: 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}") % { actual: 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}") % { actual: 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 %{actual}") % { actual: 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}") % { actual: 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}") % { actual: actual }
  end

  BAD_SLICE_KEY_TYPE = issue :BAD_SLICE_KEY_TYPE, :left_value, :expected_classes, :actual do
    if expected_classes.size > 1
      _("%{expression}[] cannot use %{actual} where one of the following is expected: %{expected}") % { expression: label.a_an_uc(left_value), actual: actual, expected: expected_classes.join(', ') }
    else
      _("%{expression}[] cannot use %{actual} where %{expected} is expected") % { expression: label.a_an_uc(left_value), actual: actual, expected: expected_classes[0] }
    end
  end

  BAD_STRING_SLICE_KEY_TYPE = issue :BAD_STRING_SLICE_KEY_TYPE, :left_value, :actual_type do
    _("A substring operation does not accept %{label_article} %{actual_type} as a character index. Expected an Integer") % { label_article: label.article(actual_type), actual_type: actual_type }
  end

  BAD_NOT_UNDEF_SLICE_TYPE = issue :BAD_NOT_UNDEF_SLICE_TYPE, :base_type, :actual do
    _("%{expression}[] argument must be a Type or a String. Got %{actual}") % { expression: base_type, actual: actual }
  end

  BAD_TYPE_SLICE_TYPE = issue :BAD_TYPE_SLICE_TYPE, :base_type, :actual do
    _("%{base_type}[] arguments must be types. Got %{actual}") % { base_type: base_type, actual: 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 == Float::INFINITY
      _("%{base_type_label}[] accepts %{min} or more arguments. Got %{actual}") % { base_type_label: base_type_label, min: min, actual: actual }
    elsif max && max != min
      _("%{base_type_label}[] accepts %{min} to %{max} arguments. Got %{actual}") % { base_type_label: base_type_label, min: min, max: max, actual: actual }
    else
      _("%{base_type_label}[] accepts %{min} %{label}. Got %{actual}") % { base_type_label: base_type_label, min: min, label: label.plural_s(min, _('argument')), actual: actual }
    end
  end

  BAD_TYPE_SPECIALIZATION = hard_issue :BAD_TYPE_SPECIALIZATION, :type, :message do
    _("Error creating type specialization of %{base_type}, %{message}") % { base_type: label.a_an(type), message: message }
  end

  ILLEGAL_TYPE_SPECIALIZATION = issue :ILLEGAL_TYPE_SPECIALIZATION, :kind do
    _("Cannot specialize an already specialized %{kind} type") % { kind: kind }
  end

  ILLEGAL_RESOURCE_SPECIALIZATION = issue :ILLEGAL_RESOURCE_SPECIALIZATION, :actual do
    _("First argument to Resource[] must be a resource type or a String. Got %{actual}.") % { actual: 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. %{expression} cannot be used where a String is expected") % { expression: label.a_an_uc(name) }
  end

  ILLEGAL_DEFINITION_NAME = hard_issue :ILLEGAL_DEFINITION_NAME, :name do
    _("Unacceptable name. The name '%{name}' is unacceptable as the name of %{value}") % { name: name, value: label.a_an(semantic) }
  end

  ILLEGAL_DEFINITION_LOCATION = issue :ILLEGAL_DEFINITION_LOCATION, :name, :file do
    _("Unacceptable location. The name '%{name}' is unacceptable in file '%{file}'") % { name: name, file: file }
  end

  ILLEGAL_TOP_CONSTRUCT_LOCATION = issue :ILLEGAL_TOP_CONSTRUCT_LOCATION do
    if semantic.is_a?(Puppet::Pops::Model::NamedDefinition)
      _("The %{value} '%{name}' is unacceptable as a top level construct in this location") % { name: semantic.name, value: label(semantic) }
    else
      _("This %{value} is unacceptable as a top level construct in this location") % { value: label(semantic) }
    end
  end

  CAPTURES_REST_NOT_LAST = hard_issue :CAPTURES_REST_NOT_LAST, :param_name do
    _("Parameter $%{param} is not last, and has 'captures rest'") % { param: param_name }
  end

  CAPTURES_REST_NOT_SUPPORTED = hard_issue :CAPTURES_REST_NOT_SUPPORTED, :container, :param_name do
    _("Parameter $%{param} has 'captures rest' - not supported in %{container}") % { param: param_name, container: label.a_an(container) }
  end

  REQUIRED_PARAMETER_AFTER_OPTIONAL = hard_issue :REQUIRED_PARAMETER_AFTER_OPTIONAL, :param_name do
    _("Parameter $%{param} is required but appears after optional parameters") % { param: param_name }
  end

  MISSING_REQUIRED_PARAMETER = hard_issue :MISSING_REQUIRED_PARAMETER, :param_name do
    _("Parameter $%{param} is required but no value was given") % { param: param_name }
  end

  NOT_NUMERIC = issue :NOT_NUMERIC, :value do
    _("The value '%{value}' cannot be converted to Numeric.") % { value: value }
  end

  NUMERIC_COERCION = issue :NUMERIC_COERCION, :before, :after do
    _("The string '%{before}' was automatically coerced to the numerical value %{after}") % { before: before, after: after }
  end

  UNKNOWN_FUNCTION = issue :UNKNOWN_FUNCTION, :name do
    _("Unknown function: '%{name}'.") % { name: name }
  end

  UNKNOWN_VARIABLE = issue :UNKNOWN_VARIABLE, :name do
    _("Unknown variable: '%{name}'.") % { name: name }
  end

  RUNTIME_ERROR = issue :RUNTIME_ERROR, :detail do
    _("Error while evaluating %{expression}, %{detail}") % { expression: label.a_an(semantic), detail: detail }
  end

  UNKNOWN_RESOURCE_TYPE = issue :UNKNOWN_RESOURCE_TYPE, :type_name do
    _("Resource type not found: %{res_type}") % { res_type: type_name }
  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}") % { actual: actual }
  end

  DUPLICATE_TITLE = issue :DUPLICATE_TITLE, :title do
    _("The title '%{title}' has already been used in this resource expression") % { title: title }
  end

  DUPLICATE_ATTRIBUTE = issue :DUPLICATE_ATTRIBUE, :attribute  do
    _("The attribute '%{attribute}' has already been set") % { attribute: attribute }
  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") % { index: index }
  end

  ILLEGAL_TITLE_TYPE_AT = hard_issue :ILLEGAL_TITLE_TYPE_AT, :index, :actual do
    _("Illegal title type at index %{index}. Expected String, got %{actual}") % { index: index, actual: 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.") % { index: index }
  end

  UNKNOWN_RESOURCE = issue :UNKNOWN_RESOURCE, :type_name, :title do
    _("Resource not found: %{type_name}['%{title}']") % { type_name: type_name, title: title }
  end

  UNKNOWN_RESOURCE_PARAMETER = issue :UNKNOWN_RESOURCE_PARAMETER, :type_name, :title, :param_name do
    _("The resource %{type_name}['%{title}'] does not have a parameter called '%{param}'") % { type_name: type_name.capitalize, title: title, param: 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") % { operator: operator }
  end

  # TODO_HEREDOC
  EMPTY_HEREDOC_SYNTAX_SEGMENT = issue :EMPTY_HEREDOC_SYNTAX_SEGMENT, :syntax do
    _("Heredoc syntax specification has empty segment between '+' : '%{syntax}'") % { syntax: 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
    # TRANSLATORS "import" is a function name and should not be translated
    _("Use of 'import' has been discontinued in favor of a manifest directory. See http://links.puppet.com/puppet-import-deprecation")
  end

  IDEM_EXPRESSION_NOT_LAST = issue :IDEM_EXPRESSION_NOT_LAST do
    _("This %{expression} has no effect. A value was produced and then forgotten (one or more preceding expressions may have the wrong form)") % { expression: label.label(semantic) }
  end

  RESOURCE_WITHOUT_TITLE = issue :RESOURCE_WITHOUT_TITLE, :name do
    _("This expression is invalid. Did you try declaring a '%{name}' resource without a title?") % { name: name }
  end

  IDEM_NOT_ALLOWED_LAST = hard_issue :IDEM_NOT_ALLOWED_LAST, :container do
    _("This %{expression} has no effect. %{container} can not end with a value-producing expression without other effect") % { expression: label.label(semantic), container: label.a_an_uc(container) }
  end

  RESERVED_WORD = hard_issue :RESERVED_WORD, :word do
    _("Use of reserved word: %{word}, must be quoted if intended to be a String value") % { word: word }
  end

  FUTURE_RESERVED_WORD = issue :FUTURE_RESERVED_WORD, :word do
    _("Use of future reserved word: '%{word}'") % { word: word }
  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 %{expression}.") % { name: name, expression: label.a_an(semantic) }
  end

  UNMATCHED_SELECTOR = hard_issue :UNMATCHED_SELECTOR, :param_value do
    _("No matching entry for selector parameter with value '%{param}'") % { param: param_value }
  end

  ILLEGAL_NODE_INHERITANCE = issue :ILLEGAL_NODE_INHERITANCE do
    _("Node inheritance is not supported in Puppet >= 4.0.0. See http://links.puppet.com/puppet-node-inheritance-deprecation")
  end

  ILLEGAL_OVERRIDDEN_TYPE = issue :ILLEGAL_OVERRIDDEN_TYPE, :actual do
    _("Resource Override can only operate on resources, got: %{actual}") % { actual: label.label(actual) }
  end

  DUPLICATE_PARAMETER = hard_issue :DUPLICATE_PARAMETER, :param_name do
    _("The parameter '%{param}' is declared more than once in the parameter list") % { param: param_name }
  end

  DUPLICATE_KEY = issue :DUPLICATE_KEY, :key do
    _("The key '%{key}' is declared more than once") % { key: key }
  end

  DUPLICATE_DEFAULT = hard_issue :DUPLICATE_DEFAULT, :container do
    _("This %{container} already has a 'default' entry - this is a duplicate") % { container: label.label(container) }
  end

  RESERVED_PARAMETER = hard_issue :RESERVED_PARAMETER, :container, :param_name do
    _("The parameter $%{param} redefines a built in parameter in %{container}") % { param: param_name, container: label.the(container) }
  end

  TYPE_MISMATCH = hard_issue :TYPE_MISMATCH, :expected, :actual do
    _("Expected value of type %{expected}, got %{actual}") % { expected: expected, actual: 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

  ILLEGAL_CATALOG_RELATED_EXPRESSION = hard_issue :ILLEGAL_CATALOG_RELATED_EXPRESSION do
    _("This %{expression} appears in a context where catalog related expressions are not allowed") % { expression: label.label(semantic) }
  end

  SYNTAX_ERROR = hard_issue :SYNTAX_ERROR, :where do
    _("Syntax error at %{location}") % { location: 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, :value do
    _("Illegal number '%{value}'") % { value: value }
  end

  ILLEGAL_UNICODE_ESCAPE = issue :ILLEGAL_UNICODE_ESCAPE do
    _("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}") % { value: value }
  end

  INVALID_OCTAL_NUMBER = hard_issue :INVALID_OCTAL_NUMBER, :value do
    _("Not a valid octal number %{value}") % { value: value }
  end

  INVALID_DECIMAL_NUMBER = hard_issue :INVALID_DECIMAL_NUMBER, :value do
    _("Not a valid decimal number %{value}") % { value: 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}'") % { ch: ch }
  end

  UNCLOSED_QUOTE = hard_issue :UNCLOSED_QUOTE, :after, :followed_by do
    _("Unclosed quote after %{after} followed by '%{followed_by}'") % { after: after, followed_by: followed_by }
  end

  UNCLOSED_MLCOMMENT = hard_issue :UNCLOSED_MLCOMMENT do
    _('Unclosed multiline comment')
  end

  EPP_INTERNAL_ERROR = hard_issue :EPP_INTERNAL_ERROR, :error do
    _("Internal error: %{error}") % { error: error }
  end

  EPP_UNBALANCED_TAG = hard_issue :EPP_UNBALANCED_TAG do
    _('Unbalanced epp tag, reached <eof> 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}'") % { 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}'") % { actual: 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_EMPTY_ENDTAG = hard_issue :HEREDOC_EMPTY_ENDTAG do
    _('Heredoc with an empty endtag')
  end

  HEREDOC_MULTIPLE_AT_ESCAPES = hard_issue :HEREDOC_MULTIPLE_AT_ESCAPES, :escapes do
    _("An escape char for @() may only appear once. Got '%{escapes}'") % { escapes: escapes.join(', ') }
  end

  HEREDOC_DIRTY_MARGIN = hard_issue :HEREDOC_DIRTY_MARGIN, :heredoc_line do
    _("Heredoc with text in the margin is not allowed (line %{heredoc_line} in this heredoc)") % { heredoc_line: heredoc_line }
  end

  ILLEGAL_BOM = hard_issue :ILLEGAL_BOM, :format_name, :bytes do
    _("Illegal %{format} Byte Order mark at beginning of input: %{bom} - remove these from the puppet source") % { format: format_name, bom: bytes }
  end

  NO_SUCH_FILE_OR_DIRECTORY = hard_issue :NO_SUCH_FILE_OR_DIRECTORY, :file do
    _('No such file or directory: %{file}') % { file: file }
  end

  NOT_A_FILE = hard_issue :NOT_A_FILE, :file do
    _('%{file} is not a file') % { file: file }
  end

  NUMERIC_OVERFLOW = hard_issue :NUMERIC_OVERFLOW, :value do
    if value > 0
      _("%{expression} resulted in a value outside of Puppet Integer max range, got '%{value}'") % { expression: label.a_an_uc(semantic), value: ("%#+x" % value) }
    else
      _("%{expression} resulted in a value outside of Puppet Integer min range, got '%{value}'") % { expression: label.a_an_uc(semantic), value: ("%#+x" % value) }
    end
  end

  HIERA_UNSUPPORTED_VERSION = hard_issue :HIERA_UNSUPPORTED_VERSION, :version do
    _("This runtime does not support hiera.yaml version %{version}") % { version: version }
  end

  HIERA_VERSION_3_NOT_GLOBAL = hard_issue :HIERA_VERSION_3_NOT_GLOBAL, :where do
    _("hiera.yaml version 3 cannot be used in %{location}") % { location: label.a_an(where) }
  end

  HIERA_UNSUPPORTED_VERSION_IN_GLOBAL = hard_issue :HIERA_UNSUPPORTED_VERSION_IN_GLOBAL do
    _('hiera.yaml version 4 cannot be used in the global layer')
  end

  HIERA_UNDEFINED_VARIABLE = hard_issue :HIERA_UNDEFINED_VARIABLE, :name do
    _("Undefined variable '%{name}'") % { name: name }
  end

  HIERA_BACKEND_MULTIPLY_DEFINED = hard_issue :HIERA_BACKEND_MULTIPLY_DEFINED, :name, :first_line do
    msg = _("Backend '%{name}' is defined more than once.") % { name: name }
    fl = first_line
    if fl
      msg += ' ' + _("First defined at %{error_location}") % { error_location: Puppet::Util::Errors.error_location(nil, fl) }
    end
    msg
  end

  HIERA_NO_PROVIDER_FOR_BACKEND = hard_issue :HIERA_NO_PROVIDER_FOR_BACKEND, :name do
    _("No data provider is registered for backend '%{name}'") % { name: name }
  end

  HIERA_HIERARCHY_NAME_MULTIPLY_DEFINED = hard_issue :HIERA_HIERARCHY_NAME_MULTIPLY_DEFINED, :name, :first_line do
    msg = _("Hierarchy name '%{name}' defined more than once.") % { name: name }
    fl = first_line
    if fl
      msg += ' ' + _("First defined at %{error_location}") % { error_location: Puppet::Util::Errors.error_location(nil, fl) }
    end
    msg
  end

  HIERA_V3_BACKEND_NOT_GLOBAL = hard_issue :HIERA_V3_BACKEND_NOT_GLOBAL do
    _("'hiera3_backend' is only allowed in the global layer")
  end

  HIERA_DEFAULT_HIERARCHY_NOT_IN_MODULE = hard_issue :HIERA_DEFAULT_HIERARCHY_NOT_IN_MODULE do
    _("'default_hierarchy' is only allowed in the module layer")
  end

  HIERA_V3_BACKEND_REPLACED_BY_DATA_HASH = hard_issue :HIERA_V3_BACKEND_REPLACED_BY_DATA_HASH, :function_name do
    _("Use \"data_hash: %{function_name}_data\" instead of \"hiera3_backend: %{function_name}\"") % { function_name: function_name }
  end

  HIERA_MISSING_DATA_PROVIDER_FUNCTION = hard_issue :HIERA_MISSING_DATA_PROVIDER_FUNCTION, :name do
    _("One of %{keys} must be defined in hierarchy '%{name}'") % { keys: label.combine_strings(Lookup::HieraConfig::FUNCTION_KEYS), name: name }
  end

  HIERA_MULTIPLE_DATA_PROVIDER_FUNCTIONS = hard_issue :HIERA_MULTIPLE_DATA_PROVIDER_FUNCTIONS, :name do
    _("Only one of %{keys} can be defined in hierarchy '%{name}'") % { keys: label.combine_strings(Lookup::HieraConfig::FUNCTION_KEYS), name: name }
  end

  HIERA_MULTIPLE_DATA_PROVIDER_FUNCTIONS_IN_DEFAULT = hard_issue :HIERA_MULTIPLE_DATA_PROVIDER_FUNCTIONS_IN_DEFAULT do
    _("Only one of %{keys} can be defined in defaults") % { keys: label.combine_strings(Lookup::HieraConfig::FUNCTION_KEYS) }
  end

  HIERA_MULTIPLE_LOCATION_SPECS = hard_issue :HIERA_MULTIPLE_LOCATION_SPECS, :name do
    _("Only one of %{keys} can be defined in hierarchy '%{name}'") % { keys: label.combine_strings(Lookup::HieraConfig::LOCATION_KEYS), name: name }
  end

  HIERA_OPTION_RESERVED_BY_PUPPET = hard_issue :HIERA_OPTION_RESERVED_BY_PUPPET, :key, :name do
    _("Option key '%{key}' used in hierarchy '%{name}' is reserved by Puppet") % { key: key, name: name }
  end

  HIERA_DEFAULT_OPTION_RESERVED_BY_PUPPET = hard_issue :HIERA_DEFAULT_OPTION_RESERVED_BY_PUPPET, :key do
    _("Option key '%{key}' used in defaults is reserved by Puppet") % { key: key }
  end

  HIERA_DATA_PROVIDER_FUNCTION_NOT_FOUND = hard_issue :HIERA_DATA_PROVIDER_FUNCTION_NOT_FOUND, :function_type, :function_name do
    _("Unable to find '%{function_type}' function named '%{function_name}'") % { function_type: function_type, function_name: function_name }
  end

  HIERA_INTERPOLATION_ALIAS_NOT_ENTIRE_STRING = hard_issue :HIERA_INTERPOLATION_ALIAS_NOT_ENTIRE_STRING do
    _("'alias' interpolation is only permitted if the expression is equal to the entire string")
  end

  HIERA_INTERPOLATION_UNKNOWN_INTERPOLATION_METHOD = hard_issue :HIERA_INTERPOLATION_UNKNOWN_INTERPOLATION_METHOD, :name do
    _("Unknown interpolation method '%{name}'") % { name: name }
  end

  HIERA_INTERPOLATION_METHOD_SYNTAX_NOT_ALLOWED = hard_issue :HIERA_INTERPOLATION_METHOD_SYNTAX_NOT_ALLOWED do
    _('Interpolation using method syntax is not allowed in this context')
  end

  SERIALIZATION_ENDLESS_RECURSION = hard_issue :SERIALIZATION_ENDLESS_RECURSION, :type_name do
    _('Endless recursion detected when attempting to serialize value of class %{type_name}') % { :type_name => type_name }
  end

  SERIALIZATION_DEFAULT_CONVERTED_TO_STRING = issue :SERIALIZATION_DEFAULT_CONVERTED_TO_STRING, :path, :klass, :value do
    _("%{path} contains the special value default. It will be converted to the String 'default'") % { path: path }
  end

  SERIALIZATION_UNKNOWN_CONVERTED_TO_STRING = issue :SERIALIZATION_UNKNOWN_CONVERTED_TO_STRING, :path, :klass, :value do
    _("%{path} contains %{klass} value. It will be converted to the String '%{value}'") % { path: path, klass: label.a_an(klass), value: value }
  end

  SERIALIZATION_UNKNOWN_KEY_CONVERTED_TO_STRING = issue :SERIALIZATION_UNKNOWN_KEY_CONVERTED_TO_STRING, :path, :klass, :value do
    _("%{path} contains a hash with %{klass} key. It will be converted to the String '%{value}'") % { path: path, klass: label.a_an(klass), value: value }
  end

  FEATURE_NOT_SUPPORTED_WHEN_SCRIPTING = issue :NOT_SUPPORTED_WHEN_SCRIPTING, :feature do
    _("The feature '%{feature}' is only available when compiling a catalog") % { feature: feature }
  end

  CATALOG_OPERATION_NOT_SUPPORTED_WHEN_SCRIPTING = issue :CATALOG_OPERATION_NOT_SUPPORTED_WHEN_SCRIPTING, :operation do
    _("The catalog operation '%{operation}' is only available when compiling a catalog") % { operation: operation }
  end

  EXPRESSION_NOT_SUPPORTED_WHEN_SCRIPTING = issue :EXPRESSION_NOT_SUPPORTED_WHEN_SCRIPTING, :klass do
    _("%{expr} is only available when compiling a catalog") % { expr: label.a_an_uc(klass) }
  end

  TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING = issue :TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, :operation do
    _("The task operation '%{operation}' is not available when compiling a catalog") % { operation: operation }
  end

  EXPRESSION_NOT_SUPPORTED_WHEN_COMPILING = issue :EXPRESSION_NOT_SUPPORTED_WHEN_COMPILING, :klass do
    _("%{expr} is not available when compiling a catalog") % { expr: label.a_an_uc(klass) }
  end

  TASK_MISSING_BOLT = issue :TASK_MISSING_BOLT, :action do
    _("The 'bolt' library is required to %{action}") % { action: action }
  end

  UNKNOWN_TASK = issue :UNKNOWN_TASK, :task_name do
    _('Task not found: %{task_name}') % { task_name: task_name }
  end

  LOADER_FAILURE = issue :LOADER_FAILURE, :task_name do
    _('Failed to load: %{task_name}') % { task_name: task_name }
  end
end
end
