diff --git a/lib/puppet/pops/binder/config/binder_config.rb b/lib/puppet/pops/binder/config/binder_config.rb index 3e83e4afb..625aeef96 100644 --- a/lib/puppet/pops/binder/config/binder_config.rb +++ b/lib/puppet/pops/binder/config/binder_config.rb @@ -1,107 +1,107 @@ module Puppet::Pops::Binder::Config # Class holding the Binder Configuration # The configuration is obtained from the file 'binder_config.yaml' # that must reside in the root directory of the site # @api public # class BinderConfig # The layering configuration is an array of layers from most to least significant. # Each layer is represented by a Hash containing :name and :include and optionally :exclude # # @return [Array, Hash>] # @api public # attr_reader :layering_config # @return ] ({}) optional mapping of bindings-scheme to handler class name attr_reader :scheme_extensions # @return [String] the loaded config file attr_accessor :config_file DEFAULT_LAYERS = [ { 'name' => 'site', 'include' => [ 'confdir:/default?optional'] }, { 'name' => 'modules', 'include' => [ 'module:/*::default'] }, ] DEFAULT_SCHEME_EXTENSIONS = {} def default_config() # This is hardcoded now, but may be a user supplied default configuration later {'version' => 1, 'layers' => default_layers } end def confdir() Puppet.settings[:confdir] end # Creates a new Config. The configuration is loaded from the file 'binder_config.yaml' which # is expected to be found in confdir. # # @param diagnostics [DiagnosticProducer] collector of diagnostics # @api public # def initialize(diagnostics) @config_file = Puppet.settings[:binder_config] # if file is stated, it must exist # otherwise it is optional $confdir/binder_conf.yaml # and if that fails, the default case @config_file when NilClass # use the config file if it exists rootdir = confdir if rootdir.is_a?(String) expanded_config_file = File.expand_path(File.join(rootdir, '/binder_config.yaml')) if Puppet::FileSystem.exist?(expanded_config_file) @config_file = expanded_config_file end else raise ArgumentError, "No Puppet settings 'confdir', or it is not a String" end when String unless Puppet::FileSystem.exist?(@config_file) raise ArgumentError, "Cannot find the given binder configuration file '#{@config_file}'" end else raise ArgumentError, "The setting binder_config is expected to be a String, got: #{@config_file.class.name}." end unless @config_file.is_a?(String) && Puppet::FileSystem.exist?(@config_file) @config_file = nil # use defaults end validator = BinderConfigChecker.new(diagnostics) begin data = @config_file ? YAML.load_file(@config_file) : default_config() validator.validate(data, @config_file) rescue Errno::ENOENT diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, @config_file) rescue Errno::ENOTDIR diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, @config_file) rescue ::SyntaxError => e diagnostics.accept(Issues::CONFIG_FILE_SYNTAX_ERROR, @config_file, :detail => e.message) end unless diagnostics.errors? - @layering_config = data['layers'] or default_layers - @scheme_extensions = (data['extensions'] and data['extensions']['scheme_handlers'] or default_scheme_extensions) + @layering_config = data['layers'] || default_layers + @scheme_extensions = (data['extensions'] && data['extensions']['scheme_handlers'] || default_scheme_extensions) else @layering_config = [] @scheme_extensions = {} end end # The default_xxx methods exists to make it easier to do mocking in tests. # @api private def default_layers DEFAULT_LAYERS end # @api private def default_scheme_extensions DEFAULT_SCHEME_EXTENSIONS end end end diff --git a/lib/puppet/pops/binder/config/binder_config_checker.rb b/lib/puppet/pops/binder/config/binder_config_checker.rb index c573502a6..628d29bf2 100644 --- a/lib/puppet/pops/binder/config/binder_config_checker.rb +++ b/lib/puppet/pops/binder/config/binder_config_checker.rb @@ -1,142 +1,142 @@ module Puppet::Pops::Binder::Config # Validates the consistency of a Binder::BinderConfig class BinderConfigChecker # Create an instance with a diagnostic producer that will receive the result during validation # @param diagnostics [DiagnosticProducer] The producer that will receive the diagnostic # @api public # def initialize(diagnostics) @diagnostics = diagnostics t = Puppet::Pops::Types @type_calculator = t::TypeCalculator.new() @array_of_string_type = t::TypeFactory.array_of(t::TypeFactory.string()) end # Validate the consistency of the given data. Diagnostics will be emitted to the DiagnosticProducer # that was set when this checker was created # # @param data [Object] The data read from the config file # @param config_file [String] The full path of the file. Used in error messages # @api public # def validate(data, config_file) @unique_layer_names = Set.new() if data.is_a?(Hash) check_top_level(data, config_file) else accept(Issues::CONFIG_IS_NOT_HASH, config_file) end end private def accept(issue, semantic, options = {}) @diagnostics.accept(issue, semantic, options) end def check_top_level(data, config_file) if layers = (data['layers'] || data[:layers]) check_layers(layers, config_file) else accept(Issues::CONFIG_LAYERS_MISSING, config_file) end - if version = (data['version'] or data[:version]) + if version = (data['version'] || data[:version]) accept(Issues::CONFIG_WRONG_VERSION, config_file, {:expected => 1, :actual => version}) unless version == 1 else accept(Issues::CONFIG_VERSION_MISSING, config_file) end if extensions = data['extensions'] check_extensions(extensions, config_file) end end def check_layers(layers, config_file) unless layers.is_a?(Array) accept(Issues::LAYERS_IS_NOT_ARRAY, config_file, :klass => data.class) else layers.each {|layer| check_layer(layer, config_file) } end end def check_layer(layer, config_file) unless layer.is_a?(Hash) accept(Issues::LAYER_IS_NOT_HASH, config_file, :klass => layer.class) return end layer.each_pair do |k, v| case k when 'name' unless v.is_a?(String) accept(Issues::LAYER_NAME_NOT_STRING, config_file, :class_name => v.class.name) end unless @unique_layer_names.add?(v) accept(Issues::DUPLICATE_LAYER_NAME, config_file, :name => v.to_s ) end when 'include' check_bindings_references('include', v, config_file) when 'exclude' check_bindings_references('exclude', v, config_file) when Symbol accept(Issues::LAYER_ATTRIBUTE_IS_SYMBOL, config_file, :name => k.to_s) else accept(Issues::UNKNOWN_LAYER_ATTRIBUTE, config_file, :name => k.to_s ) end end end # references to bindings is a single String URI, or an array of String URI # @param kind [String] 'include' or 'exclude' (used in issue messages) # @param value [String, Array] one or more String URI binding references # @param config_file [String] reference to the loaded config file # def check_bindings_references(kind, value, config_file) return check_reference(value, kind, config_file) if value.is_a?(String) accept(Issues::BINDINGS_REF_NOT_STRING_OR_ARRAY, config_file, :kind => kind ) unless value.is_a?(Array) value.each {|ref| check_reference(ref, kind, config_file) } end # A reference is a URI in string form having a scheme and a path (at least '/') # def check_reference(value, kind, config_file) begin uri = URI.parse(value) unless uri.scheme accept(Issues::MISSING_SCHEME, config_file, :uri => uri) end unless uri.path accept(Issues::REF_WITHOUT_PATH, config_file, :uri => uri, :kind => kind) end rescue InvalidURIError => e accept(Issues::BINDINGS_REF_INVALID_URI, config_file, :msg => e.message) end end def check_extensions(extensions, config_file) unless extensions.is_a?(Hash) accept(Issues::EXTENSIONS_NOT_HASH, config_file, :actual => extensions.class.name) return end # check known extensions extensions.each_key do |key| unless ['scheme_handlers'].include? key accept(Issues::UNKNOWN_EXTENSION, config_file, :extension => key) end end if binding_schemes = extensions['scheme_handlers'] unless binding_schemes.is_a?(Hash) accept(Issues::EXTENSION_BINDING_NOT_HASH, config_file, :extension => 'scheme_handlers', :actual => binding_schemes.class.name) end end end end end diff --git a/lib/puppet/pops/binder/config/issues.rb b/lib/puppet/pops/binder/config/issues.rb index 11e37fb32..b73d550b2 100644 --- a/lib/puppet/pops/binder/config/issues.rb +++ b/lib/puppet/pops/binder/config/issues.rb @@ -1,86 +1,90 @@ module Puppet::Pops::Binder::Config::Issues # (see Puppet::Pops::Issues#issue) def self.issue (issue_code, *args, &block) Puppet::Pops::Issues.issue(issue_code, *args, &block) end CONFIG_FILE_NOT_FOUND = issue :CONFIG_FILE_NOT_FOUND do "The binder configuration file: #{semantic} can not be found." end CONFIG_FILE_SYNTAX_ERROR = issue :CONFIG_FILE_SYNTAX_ERROR, :detail do "Syntax error in configuration file: #{detail}" end CONFIG_IS_NOT_HASH = issue :CONFIG_IS_NOT_HASH do "The configuration file '#{semantic}' has no hash at the top level" end CONFIG_LAYERS_MISSING = issue :CONFIG_LAYERS_MISSING do "The configuration file '#{semantic}' has no 'layers' entry in the top level hash" end + CONFIG_CATEGORIES_MISSING = issue :CONFIG_CATEGORIES_MISSING do + "The configuration file '#{semantic}' has no 'categories' entry in the top level hash" + end + CONFIG_VERSION_MISSING = issue :CONFIG_VERSION_MISSING do "The configuration file '#{semantic}' has no 'version' entry in the top level hash" end LAYERS_IS_NOT_ARRAY = issue :LAYERS_IS_NOT_ARRAY, :klass do "The configuration file '#{semantic}' should contain a 'layers' key with an Array value, got: #{klass.name}" end LAYER_IS_NOT_HASH = issue :LAYER_IS_NOT_HASH, :klass do "The configuration file '#{semantic}' should contain one hash per layer, got #{klass.name} instead of Hash" end DUPLICATE_LAYER_NAME = issue :DUPLICATE_LAYER_NAME, :name do "Duplicate layer '#{name}' in configuration file #{semantic}" end UNKNOWN_LAYER_ATTRIBUTE = issue :UNKNOWN_LAYER_ATTRIBUTE, :name do "Unknown layer attribute '#{name}' in configuration file #{semantic}" end BINDINGS_REF_NOT_STRING_OR_ARRAY = issue :BINDINGS_REF_NOT_STRING_OR_ARRAY, :kind do "Configuration file #{semantic} has bindings reference in '#{kind}' that is neither a String nor an Array." end MISSING_SCHEME = issue :MISSING_SCHEME, :uri do "Configuration file #{semantic} contains a bindings reference: '#{uri}' without scheme." end UNKNOWN_REF_SCHEME = issue :UNKNOWN_REF_SCHEME, :uri, :kind do "Configuration file #{semantic} contains a bindings reference: '#{kind}' => '#{uri}' with unknown scheme" end REF_WITHOUT_PATH = issue :REF_WITHOUT_PATH, :uri, :kind do "Configuration file #{semantic} contains a bindings reference: '#{kind}' => '#{uri}' without path" end BINDINGS_REF_INVALID_URI = issue :BINDINGS_REF_INVALID_URI, :msg do "Configuration file #{semantic} contains a bindings reference: '#{kind}' => invalid uri, msg: '#{msg}'" end LAYER_ATTRIBUTE_IS_SYMBOL = issue :LAYER_ATTRIBUTE_IS_SYMBOL, :name do "Configuration file #{semantic} contains a layer attribute '#{name}' that is a Symbol (should be String)" end LAYER_NAME_NOT_STRING = issue :LAYER_NAME_NOT_STRING, :class_name do "Configuration file #{semantic} contains a layer name that is not a String, got a: '#{class_name}'" end CONFIG_WRONG_VERSION = issue :CONFIG_WRONG_VERSION, :expected, :actual do "The configuration file '#{semantic}' has unsupported 'version', expected: #{expected}, but got: #{actual}." end EXTENSIONS_NOT_HASH = issue :EXTENSIONS_NOT_HASH, :actual do "The configuration file '#{semantic}' contains 'extensions', expected: Hash, but got: #{actual}." end EXTENSION_BINDING_NOT_HASH = issue :EXTENSION_BINDING_NOT_HASH, :extension, :actual do "The configuration file '#{semantic}' contains '#{extension}', expected: Hash, but got: #{actual}." end UNKNOWN_EXTENSION = issue :UNKNOWN_EXTENSION, :extension do "The configuration file '#{semantic}' contains the unknown extension: #{extension}." end end diff --git a/lib/puppet/pops/model/ast_tree_dumper.rb b/lib/puppet/pops/model/ast_tree_dumper.rb index 00c4ae40a..9008b1ccd 100644 --- a/lib/puppet/pops/model/ast_tree_dumper.rb +++ b/lib/puppet/pops/model/ast_tree_dumper.rb @@ -1,386 +1,386 @@ require 'puppet/parser/ast' # Dumps a Pops::Model in reverse polish notation; i.e. LISP style # The intention is to use this for debugging output # TODO: BAD NAME - A DUMP is a Ruby Serialization # class Puppet::Pops::Model::AstTreeDumper < Puppet::Pops::Model::TreeDumper AST = Puppet::Parser::AST Model = Puppet::Pops::Model def dump_LiteralFloat o o.value.to_s end def dump_LiteralInteger o case o.radix when 10 o.value.to_s when 8 "0%o" % o.value when 16 "0x%X" % o.value else "bad radix:" + o.value.to_s end end def dump_Expression(o) "(pops-expression #{Puppet::Pops::Model::ModelTreeDumper.new().dump(o.value)})" end def dump_Factory o do_dump(o.current) end def dump_ArithmeticOperator o [o.operator.to_s, do_dump(o.lval), do_dump(o.rval)] end def dump_Relationship o [o.arrow.to_s, do_dump(o.left), do_dump(o.right)] end # Hostname is tricky, it is either a bare word, a string, or default, or regular expression # Least evil, all strings except default are quoted def dump_HostName o result = do_dump o.value unless o.value.is_a? AST::Regex result = result == "default" ? ":default" : "'#{result}'" end result end # x[y] prints as (slice x y) def dump_HashOrArrayAccess o var = o.variable.is_a?(String) ? "$#{o.variable}" : do_dump(o.variable) ["slice", var, do_dump(o.key)] end # The AST Collection knows about exported or virtual query, not the query. def dump_Collection o result = ["collect", do_dump(o.type), :indent, :break] if o.form == :virtual q = ["<| |>"] else q = ["<<| |>>"] end q << do_dump(o.query) unless is_nop?(o.query) q << :indent result << q o.override do |ao| result << :break << do_dump(ao) end result += [:dedent, :dedent ] result end def dump_CollExpr o operator = case o.oper when 'and' '&&' when 'or' '||' else o.oper end [operator, do_dump(o.test1), do_dump(o.test2)] end def dump_ComparisonOperator o [o.operator.to_s, do_dump(o.lval), do_dump(o.rval)] end def dump_Boolean o o.to_s end def dump_BooleanOperator o operator = o.operator == 'and' ? '&&' : '||' [operator, do_dump(o.lval), do_dump(o.rval)] end def dump_InOperator o ["in", do_dump(o.lval), do_dump(o.rval)] end # $x = ... # $x += ... # def dump_VarDef o operator = o.append ? "+=" : "=" [operator, '$' + do_dump(o.name), do_dump(o.value)] end # Produces (name => expr) or (name +> expr) def dump_ResourceParam o operator = o.add ? "+>" : "=>" [do_dump(o.param), operator, do_dump(o.value)] end def dump_Array o o.collect {|e| do_dump(e) } end def dump_ASTArray o ["[]"] + o.children.collect {|x| do_dump(x)} end def dump_ASTHash o ["{}"] + o.value.sort_by{|k,v| k.to_s}.collect {|x| [do_dump(x[0]), do_dump(x[1])]} # ["{}"] + o.value.collect {|x| [do_dump(x[0]), do_dump(x[1])]} end def dump_MatchOperator o [o.operator.to_s, do_dump(o.lval), do_dump(o.rval)] end # Dump a Ruby String in single quotes unless it is a number. def dump_String o if o.is_a? String o # A Ruby String, not quoted elsif Puppet::Pops::Utils.to_n(o.value) o.value # AST::String that is a number without quotes else "'#{o.value}'" # AST::String that is not a number end end def dump_Lambda o result = ["lambda"] result << ["parameters"] + o.parameters.collect {|p| _dump_ParameterArray(p) } if o.parameters.size() > 0 if o.children == [] result << [] # does not have a lambda body else result << do_dump(o.children) end result end def dump_Default o ":default" end def dump_Undef o ":undef" end # Note this is Regex (the AST kind), not Ruby Regexp def dump_Regex o "/#{o.value.source}/" end def dump_Nop o ":nop" end def dump_NilClass o "()" end def dump_Not o ['!', dump(o.value)] end def dump_Variable o "$#{dump(o.value)}" end def dump_Minus o ['-', do_dump(o.value)] end def dump_BlockExpression o ["block"] + o.children.collect {|x| do_dump(x) } end # Interpolated strings are shown as (cat seg0 seg1 ... segN) def dump_Concat o ["cat"] + o.value.collect {|x| x.is_a?(AST::String) ? " "+do_dump(x) : ["str", do_dump(x)]} end def dump_Hostclass o # ok, this is kind of crazy stuff in the AST, information in a context instead of in AST, and # parameters are in a Ruby Array with each parameter being an Array... # context = o.context args = context[:arguments] parent = context[:parent] result = ["class", o.name] result << ["inherits", parent] if parent result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0 if is_nop?(o.code) result << [] else result << do_dump(o.code) end result end def dump_Name o o.value end def dump_Node o context = o.context parent = context[:parent] code = context[:code] result = ["node"] result << ["matches"] + o.names.collect {|m| do_dump(m) } result << ["parent", do_dump(parent)] if !is_nop?(parent) if is_nop?(code) result << [] else result << do_dump(code) end result end def dump_Definition o # ok, this is even crazier that Hostclass. The name of the define does not have an accessor # and some things are in the context (but not the name). Parameters are called arguments and they # are in a Ruby Array where each parameter is an array of 1 or 2 elements. # context = o.context name = o.instance_variable_get("@name") args = context[:arguments] code = context[:code] result = ["define", name] result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0 if is_nop?(code) result << [] else result << do_dump(code) end result end def dump_ResourceReference o result = ["slice", do_dump(o.type)] if o.title.children.size == 1 result << do_dump(o.title[0]) else result << do_dump(o.title.children) end result end def dump_ResourceOverride o result = ["override", do_dump(o.object), :indent] o.parameters.each do |p| result << :break << do_dump(p) end result << :dedent result end # Puppet AST encodes a parameter as a one or two slot Array. # This is not a polymorph dump method. # def _dump_ParameterArray o if o.size == 2 ["=", o[0], do_dump(o[1])] else o[0] end end def dump_IfStatement o result = ["if", do_dump(o.test), :indent, :break, ["then", :indent, do_dump(o.statements), :dedent]] result += [:break, ["else", :indent, do_dump(o.else), :dedent], :dedent] unless is_nop? o.else result end # Produces (invoke name args...) when not required to produce an rvalue, and # (call name args ... ) otherwise. # def dump_Function o # somewhat ugly as Function hides its "ftype" instance variable result = [o.instance_variable_get("@ftype") == :rvalue ? "call" : "invoke", do_dump(o.name)] o.arguments.collect {|a| result << do_dump(a) } result << do_dump(o.pblock) if o.pblock result end def dump_MethodCall o # somewhat ugly as Method call (does the same as function) and hides its "ftype" instance variable result = [o.instance_variable_get("@ftype") == :rvalue ? "call-method" : "invoke-method", [".", do_dump(o.receiver), do_dump(o.name)]] o.arguments.collect {|a| result << do_dump(a) } result << do_dump(o.lambda) if o.lambda result end def dump_CaseStatement o result = ["case", do_dump(o.test), :indent] o.options.each do |s| result << :break << do_dump(s) end result << :dedent end def dump_CaseOpt o result = ["when"] result << o.value.collect {|x| do_dump(x) } # A bit of trickery to get it into the same shape as Pops output if is_nop?(o.statements) result << ["then", []] # Puppet AST has a nop if there is no body else result << ["then", do_dump(o.statements) ] end result end def dump_ResourceInstance o result = [do_dump(o.title), :indent] o.parameters.each do |p| result << :break << do_dump(p) end result << :dedent result end def dump_ResourceDefaults o result = ["resource-defaults", do_dump(o.type), :indent] o.parameters.each do |p| result << :break << do_dump(p) end result << :dedent result end def dump_Resource o if o.exported form = 'exported-' elsif o.virtual form = 'virtual-' else form = '' end result = [form+"resource", do_dump(o.type), :indent] o.instances.each do |b| result << :break << do_dump(b) end result << :dedent result end def dump_Selector o values = o.values - values = [values] unless values.instance_of? AST::ASTArray or values.instance_of? Array + values = [values] unless values.instance_of?(AST::ASTArray) || values.instance_of?(Array) ["?", do_dump(o.param)] + values.collect {|x| do_dump(x) } end def dump_Object o ['dev-error-no-polymorph-dump-for:', o.class.to_s, o.to_s] end def is_nop? o o.nil? || o.is_a?(Model::Nop) || o.is_a?(AST::Nop) end end diff --git a/lib/puppet/pops/parser/evaluating_parser.rb b/lib/puppet/pops/parser/evaluating_parser.rb index 596f549bc..11e83163d 100644 --- a/lib/puppet/pops/parser/evaluating_parser.rb +++ b/lib/puppet/pops/parser/evaluating_parser.rb @@ -1,140 +1,140 @@ # Does not support "import" and parsing ruby files # class Puppet::Pops::Parser::EvaluatingParser attr_reader :parser def initialize() @parser = Puppet::Pops::Parser::Parser.new() end def parse_string(s, file_source = 'unknown') @file_source = file_source clear() # Handling of syntax error can be much improved (in general), now it bails out of the parser # and does not have as rich information (when parsing a string), need to update it with the file source # (ideally, a syntax error should be entered as an issue, and not just thrown - but that is a general problem # and an improvement that can be made in the eparser (rather than here). # Also a possible improvement (if the YAML parser returns positions) is to provide correct output of position. # begin assert_and_report(parser.parse_string(s)) rescue Puppet::ParseError => e # TODO: This is not quite right, why does not the exception have the correct file? e.file = @file_source unless e.file.is_a?(String) && !e.file.empty? raise e end end def parse_file(file) @file_source = file clear() assert_and_report(parser.parse_file(file)) end def evaluate_string(scope, s, file_source='unknown') evaluate(scope, parse_string(s, file_source)) end def evaluate_file(file) evaluate(parse_file(file)) end def clear() @acceptor = nil end # Create a closure that can be called in the given scope def closure(model, scope) Puppet::Pops::Evaluator::Closure.new(evaluator, model, scope) end def evaluate(scope, model) return nil unless model evaluator.evaluate(model, scope) end def evaluator @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new() @@evaluator end def validate(parse_result) resulting_acceptor = acceptor() validator(resulting_acceptor).validate(parse_result) resulting_acceptor end def acceptor() Puppet::Pops::Validation::Acceptor.new end def validator(acceptor) Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) end def assert_and_report(parse_result) return nil unless parse_result - if parse_result.source_ref.nil? or parse_result.source_ref == '' + if parse_result.source_ref.nil? || parse_result.source_ref == '' parse_result.source_ref = @file_source end validation_result = validate(parse_result) Puppet::Pops::IssueReporter.assert_and_report(validation_result, :emit_warnings => true) parse_result end def quote(x) self.class.quote(x) end # Translates an already parsed string that contains control characters, quotes # and backslashes into a quoted string where all such constructs have been escaped. # Parsing the return value of this method using the puppet parser should yield # exactly the same string as the argument passed to this method # # The method makes an exception for the two character sequences \$ and \s. They # will not be escaped since they have a special meaning in puppet syntax. # # TODO: Handle \uXXXX characters ?? # # @param x [String] The string to quote and "unparse" # @return [String] The quoted string # def self.quote(x) escaped = '"' p = nil x.each_char do |c| case p when nil # do nothing when "\t" escaped << '\\t' when "\n" escaped << '\\n' when "\f" escaped << '\\f' # TODO: \cx is a range of characters - skip for now # when "\c" # escaped << '\\c' when '"' escaped << '\\"' when '\\' escaped << if c == '$' || c == 's'; p; else '\\\\'; end # don't escape \ when followed by s or $ else escaped << p end p = c end escaped << p unless p.nil? escaped << '"' end class EvaluatingEppParser < Puppet::Pops::Parser::EvaluatingParser def initialize() @parser = Puppet::Pops::Parser::EppParser.new() end end end diff --git a/lib/puppet/pops/validation.rb b/lib/puppet/pops/validation.rb index eddd7ab2d..5af51c30c 100644 --- a/lib/puppet/pops/validation.rb +++ b/lib/puppet/pops/validation.rb @@ -1,432 +1,432 @@ # A module with base functionality for validation of a model. # # * **Factory** - an abstract factory implementation that makes it easier to create a new validation factory. # * **SeverityProducer** - produces a severity (:error, :warning, :ignore) for a given Issue # * **DiagnosticProducer** - produces a Diagnostic which binds an Issue to an occurrence of that issue # * **Acceptor** - the receiver/sink/collector of computed diagnostics # * **DiagnosticFormatter** - produces human readable output for a Diagnostic # module Puppet::Pops::Validation # This class is an abstract base implementation of a _model validation factory_ that creates a validator instance # and associates it with a fully configured DiagnosticProducer. # # A _validator_ is responsible for validating a model. There may be different versions of validation available # for one and the same model; e.g. different semantics for different puppet versions, or different types of # validation configuration depending on the context/type of validation that should be performed (static, vs. runtime, etc.). # # This class is abstract and must be subclassed. The subclass must implement the methods # {#label_provider} and {#checker}. It is also expected that the sublcass will override # the severity_producer and configure the issues that should be reported as errors (i.e. if they should be ignored, produce # a warning, or a deprecation warning). # # @abstract Subclass must implement {#checker}, and {#label_provider} # @api public # class Factory # Produces a validator with the given acceptor as the recipient of produced diagnostics. # The acceptor is where detected issues are received (and typically collected). # # @param acceptor [Acceptor] the acceptor is the receiver of all detected issues # @return [#validate] a validator responding to `validate(model)` # # @api public # def validator(acceptor) checker(diagnostic_producer(acceptor)) end # Produces the diagnostics producer to use given an acceptor of issues. # # @param acceptor [Acceptor] the acceptor is the receiver of all detected issues # @return [DiagnosticProducer] a detector of issues # # @api public # def diagnostic_producer(acceptor) Puppet::Pops::Validation::DiagnosticProducer.new(acceptor, severity_producer(), label_provider()) end # Produces the SeverityProducer to use # Subclasses should implement and add specific overrides # # @return [SeverityProducer] a severity producer producing error, warning or ignore per issue # # @api public # def severity_producer Puppet::Pops::Validation::SeverityProducer.new end # Produces the checker to use. # # @abstract # # @api public # def checker(diagnostic_producer) raise NoMethodError, "checker" end # Produces the label provider to use. # # @abstract # # @api public # def label_provider raise NoMethodError, "label_provider" end end # Decides on the severity of a given issue. # The produced severity is one of `:error`, `:warning`, or `:ignore`. # By default, a severity of `:error` is produced for all issues. To configure the severity # of an issue call `#severity=(issue, level)`. # # @return [Symbol] a symbol representing the severity `:error`, `:warning`, or `:ignore` # # @api public # class SeverityProducer @@severity_hash = {:ignore => true, :warning => true, :error => true, :deprecation => true } # Creates a new instance where all issues are diagnosed as :error unless overridden. # @api public # def initialize # If diagnose is not set, the default is returned by the block @severities = Hash.new :error end # Returns the severity of the given issue. # @return [Symbol] severity level :error, :warning, or :ignore # @api public # def severity(issue) assert_issue(issue) @severities[issue] end # @see {#severity} # @api public # def [] issue severity issue end # Override a default severity with the given severity level. # # @param issue [Puppet::Pops::Issues::Issue] the issue for which to set severity # @param level [Symbol] the severity level (:error, :warning, or :ignore). # @api public # def []=(issue, level) raise Puppet::DevError.new("Attempt to set validation severity for something that is not an Issue. (Got #{issue.class})") unless issue.is_a? Puppet::Pops::Issues::Issue raise Puppet::DevError.new("Illegal severity level: #{option}") unless @@severity_hash[level] raise Puppet::DevError.new("Attempt to demote the hard issue '#{issue.issue_code}' to #{level}") unless issue.demotable? || level == :error @severities[issue] = level end # Returns `true` if the issue should be reported or not. # @return [Boolean] this implementation returns true for errors and warnings # # @api public # def should_report? issue diagnose = @severities[issue] diagnose == :error || diagnose == :warning || diagnose == :deprecation end # Checks if the given issue is valid. # @api private # def assert_issue issue raise Puppet::DevError.new("Attempt to get validation severity for something that is not an Issue. (Got #{issue.class})") unless issue.is_a? Puppet::Pops::Issues::Issue end # Checks if the given severity level is valid. # @api private # def assert_severity level raise Puppet::DevError.new("Illegal severity level: #{option}") unless @@severity_hash[level] end end # A producer of diagnostics. # An producer of diagnostics is given each issue occurrence as they are found by a diagnostician/validator. It then produces # a Diagnostic, which it passes on to a configured Acceptor. # # This class exists to aid a diagnostician/validator which will typically first check if a particular issue # will be accepted at all (before checking for an occurrence of the issue; i.e. to perform check avoidance for expensive checks). # A validator passes an instance of Issue, the semantic object (the "culprit"), a hash with arguments, and an optional # exception. The semantic object is used to determine the location of the occurrence of the issue (file/line), and it # sets keys in the given argument hash that may be used in the formatting of the issue message. # class DiagnosticProducer # A producer of severity for a given issue # @return [SeverityProducer] # attr_reader :severity_producer # A producer of labels for objects involved in the issue # @return [LabelProvider] # attr_reader :label_provider # Initializes this producer. # # @param acceptor [Acceptor] a sink/collector of diagnostic results # @param severity_producer [SeverityProducer] the severity producer to use to determine severity of a given issue # @param label_provider [LabelProvider] a provider of model element type to human readable label # def initialize(acceptor, severity_producer, label_provider) @acceptor = acceptor @severity_producer = severity_producer @label_provider = label_provider end def accept(issue, semantic, arguments={}, except=nil) return unless will_accept? issue # Set label provider unless caller provided a special label provider arguments[:label] ||= @label_provider arguments[:semantic] ||= semantic # A detail message is always provided, but is blank by default. # TODO: this support is questionable, it requires knowledge that :detail is special arguments[:detail] ||= '' source_pos = Puppet::Pops::Utils.find_closest_positioned(semantic) file = source_pos ? source_pos.locator.file : nil severity = @severity_producer.severity(issue) @acceptor.accept(Diagnostic.new(severity, issue, file, source_pos, arguments, except)) end def will_accept? issue @severity_producer.should_report? issue end end class Diagnostic attr_reader :severity attr_reader :issue attr_reader :arguments attr_reader :exception attr_reader :file attr_reader :source_pos def initialize severity, issue, file, source_pos, arguments={}, exception=nil @severity = severity @issue = issue @file = file @source_pos = source_pos @arguments = arguments # TODO: Currently unused, the intention is to provide more information (stack backtrace, etc.) when # debugging or similar - this to catch internal problems reported as higher level issues. @exception = exception end end # Formats a diagnostic for output. # Produces a diagnostic output typical for a compiler (suitable for interpretation by tools) # The format is: # `file:line:pos: Message`, where pos, line and file are included if available. # class DiagnosticFormatter def format diagnostic "#{loc(diagnostic)} #{format_severity(diagnostic)}#{format_message(diagnostic)}" end def format_message diagnostic diagnostic.issue.format(diagnostic.arguments) end # This produces "Deprecation notice: " prefix if the diagnostic has :deprecation severity, otherwise "". # The idea is that all other diagnostics are emitted with the methods Puppet.err (or an exception), and # Puppet.warning. # @note Note that it is not a good idea to use Puppet.deprecation_warning as it is for internal deprecation. # def format_severity diagnostic diagnostic.severity == :deprecation ? "Deprecation notice: " : "" end def format_location diagnostic file = diagnostic.file file = (file.is_a?(String) && file.empty?) ? nil : file line = pos = nil if diagnostic.source_pos line = diagnostic.source_pos.line pos = diagnostic.source_pos.pos end if file && line && pos "#{file}:#{line}:#{pos}:" elsif file && line "#{file}:#{line}:" elsif file "#{file}:" else "" end end end # Produces a diagnostic output in the "puppet style", where the location is appended with an "at ..." if the # location is known. # class DiagnosticFormatterPuppetStyle < DiagnosticFormatter def format diagnostic if (location = format_location diagnostic) != "" "#{format_severity(diagnostic)}#{format_message(diagnostic)}#{location}" else format_message(diagnostic) end end # The somewhat (machine) unusable format in current use by puppet. # have to be used here for backwards compatibility. def format_location diagnostic file = diagnostic.file file = (file.is_a?(String) && file.empty?) ? nil : file line = pos = nil if diagnostic.source_pos line = diagnostic.source_pos.line pos = diagnostic.source_pos.pos end if file && line && pos " at #{file}:#{line}:#{pos}" - elsif file and line + elsif file && line " at #{file}:#{line}" elsif line && pos " at line #{line}:#{pos}" elsif line " at line #{line}" elsif file " in #{file}" else "" end end end # An acceptor of diagnostics. # An acceptor of diagnostics is given each issue as they are found by a diagnostician/validator. An # acceptor can collect all found issues, or decide to collect a few and then report, or give up as the first issue # if found. # This default implementation collects all diagnostics in the order they are produced, and can then # answer questions about what was diagnosed. # class Acceptor # All diagnostic in the order they were issued attr_reader :diagnostics # The number of :warning severity issues + number of :deprecation severity issues attr_reader :warning_count # The number of :error severity issues attr_reader :error_count # Initializes this diagnostics acceptor. # By default, the acceptor is configured with a default severity producer. # @param severity_producer [SeverityProducer] the severity producer to use to determine severity of an issue # # TODO add semantic_label_provider # def initialize() @diagnostics = [] @error_count = 0 @warning_count = 0 end # Returns true when errors have been diagnosed. def errors? @error_count > 0 end # Returns true when warnings have been diagnosed. def warnings? @warning_count > 0 end # Returns true when errors and/or warnings have been diagnosed. def errors_or_warnings? errors? || warnings? end # Returns the diagnosed errors in the order thwy were reported. def errors @diagnostics.select {|d| d.severity == :error } end # Returns the diagnosed warnings in the order thwy were reported. # (This includes :warning and :deprecation severity) def warnings @diagnostics.select {|d| d.severity == :warning || d.severity == :deprecation } end def errors_and_warnings @diagnostics.select {|d| d.severity != :ignore } end # Returns the ignored diagnostics in the order thwy were reported (if reported at all) def ignored @diagnostics.select {|d| d.severity == :ignore } end # Add a diagnostic, or all diagnostics from another acceptor to the set of diagnostics # @param diagnostic [Puppet::Pops::Validation::Diagnostic, Puppet::Pops::Validation::Acceptor] diagnostic(s) that should be accepted def accept(diagnostic) if diagnostic.is_a?(Acceptor) diagnostic.diagnostics.each {|d| self.send(d.severity, d)} else self.send(diagnostic.severity, diagnostic) end end # Prunes the contain diagnostics by removing those for which the given block returns true. # The internal statistics is updated as a consequence of removing. # @return [Array