|
| 1 | +require 'puppet' |
| 2 | + |
| 3 | +# Example of a module setting everything up to perform custom |
| 4 | +# validation of an AST model produced by parsing puppet source. |
| 5 | +# |
| 6 | +module MyValidation |
| 7 | + |
| 8 | + # A module for the new issues that the this new kind of validation will generate |
| 9 | + # |
| 10 | + module Issues |
| 11 | + # (see Puppet::Pops::Issues#issue) |
| 12 | + # This is boiler plate code |
| 13 | + def self.issue (issue_code, *args, &block) |
| 14 | + Puppet::Pops::Issues.issue(issue_code, *args, &block) |
| 15 | + end |
| 16 | + |
| 17 | + INVALID_WORD = issue :INVALID_WORD, :text do |
| 18 | + "The word '#{text}' is not a real word." |
| 19 | + end |
| 20 | + end |
| 21 | + |
| 22 | + # This is the class that performs the actual validation by checking input |
| 23 | + # and sending issues to an acceptor. |
| 24 | + # |
| 25 | + class MyChecker |
| 26 | + attr_reader :acceptor |
| 27 | + def initialize(diagnostics_producer) |
| 28 | + @@bad_word_visitor ||= Puppet::Pops::Visitor.new(nil, "badword", 0, 0) |
| 29 | + # add more polymorphic checkers here |
| 30 | + |
| 31 | + # remember the acceptor where the issues should be sent |
| 32 | + @acceptor = diagnostics_producer |
| 33 | + end |
| 34 | + |
| 35 | + # Validates the entire model by visiting each model element and calling the various checkers |
| 36 | + # (here just the example 'check_bad_word'), but a series of things could be checked. |
| 37 | + # |
| 38 | + # The result is collected by the configured diagnostic provider/acceptor |
| 39 | + # given when creating this Checker. |
| 40 | + # |
| 41 | + # Returns the @acceptor for convenient chaining of operations |
| 42 | + # |
| 43 | + def validate(model) |
| 44 | + # tree iterate the model, and call the checks for each element |
| 45 | + |
| 46 | + # While not strictly needed, here a check is made of the root (the "Program" AST object) |
| 47 | + check_bad_word(model) |
| 48 | + |
| 49 | + # Then check all of its content |
| 50 | + model.eAllContents.each {|m| check_bad_word(m) } |
| 51 | + @acceptor |
| 52 | + end |
| 53 | + |
| 54 | + # perform the bad_word check on one AST element |
| 55 | + # (this is done using a polymorphic visitor) |
| 56 | + # |
| 57 | + def check_bad_word(o) |
| 58 | + @@bad_word_visitor.visit_this_0(self, o) |
| 59 | + end |
| 60 | + |
| 61 | + protected |
| 62 | + |
| 63 | + def badword_Object(o) |
| 64 | + # ignore all not covered by an explicit badword_xxx method |
| 65 | + end |
| 66 | + |
| 67 | + # A bare word is a QualifiedName |
| 68 | + # |
| 69 | + def badword_QualifiedName(o) |
| 70 | + if o.value == 'bigly' |
| 71 | + acceptor.accept(Issues::INVALID_WORD, o, :text => o.value) |
| 72 | + end |
| 73 | + end |
| 74 | + end |
| 75 | + |
| 76 | + class MyFactory < Puppet::Pops::Validation::Factory |
| 77 | + # Produces the checker to use |
| 78 | + def checker(diagnostic_producer) |
| 79 | + MyChecker.new(diagnostic_producer) |
| 80 | + end |
| 81 | + |
| 82 | + # Produces the label provider to use. |
| 83 | + # |
| 84 | + def label_provider |
| 85 | + # We are dealing with AST, so the existing one will do fine. |
| 86 | + # This is what translates objects into a meaningful description of what that thing is |
| 87 | + # |
| 88 | + Puppet::Pops::Model::ModelLabelProvider.new() |
| 89 | + end |
| 90 | + |
| 91 | + # Produces the severity producer to use. Here it is configured what severity issues have |
| 92 | + # if they are not all errors. (If they are all errors this method is not needed at all). |
| 93 | + # |
| 94 | + def severity_producer |
| 95 | + # Gets a default severity producer that is then configured below |
| 96 | + p = super |
| 97 | + |
| 98 | + # Configure each issue that should **not** be an error |
| 99 | + # |
| 100 | + p[Issues::INVALID_WORD] = :warning |
| 101 | + |
| 102 | + # examples of what may be done here |
| 103 | + # p[Issues::SOME_ISSUE] = <some condition> ? :ignore : :warning |
| 104 | + # p[Issues::A_DEPRECATION] = :deprecation |
| 105 | + |
| 106 | + # return the configured producer |
| 107 | + p |
| 108 | + end |
| 109 | + |
| 110 | + # Allow simpler call when not caring about getting the actual acceptor |
| 111 | + def diagnostic_producer(acceptor=nil) |
| 112 | + acceptor.nil? ? super(Puppet::Pops::Validation::Acceptor.new) : super(acceptor) |
| 113 | + end |
| 114 | + end |
| 115 | + |
| 116 | + # We create a diagnostic formatter that outputs the error with a simple predefined |
| 117 | + # format for location, severity, and the message. This format is a typical output from |
| 118 | + # something like a linter or compiler. |
| 119 | + # (We do this because there is a bug in the DiagnosticFormatter's `format` method prior to |
| 120 | + # Puppet 4.9.0. It could otherwise have been used directly. |
| 121 | + # |
| 122 | + class Formatter < Puppet::Pops::Validation::DiagnosticFormatter |
| 123 | + def format(diagnostic) |
| 124 | + "#{format_location(diagnostic)} #{format_severity(diagnostic)}#{format_message(diagnostic)}" |
| 125 | + end |
| 126 | + end |
| 127 | +end |
| 128 | + |
| 129 | +# -- Example usage of the new validator |
| 130 | + |
| 131 | +# Get a parser |
| 132 | +parser = Puppet::Pops::Parser::EvaluatingParser.singleton |
| 133 | + |
| 134 | +# parse without validation |
| 135 | +result = parser.parser.parse_string('$x = if 1 < 2 { smaller } else { bigly }', 'testing.pp') |
| 136 | +result = result.model |
| 137 | + |
| 138 | +# validate using the default validator and get hold of the acceptor containing the result |
| 139 | +acceptor = parser.validate(result) |
| 140 | + |
| 141 | +# -- At this point, we have done everything `puppet parser validate` does except report the errors |
| 142 | +# and raise an exception if there were errors. |
| 143 | + |
| 144 | +# The acceptor may now contain errors and warnings as found by the standard puppet validation. |
| 145 | +# We could look at the amount of errors/warnings produced and decide it is too much already |
| 146 | +# or we could simply continue. Here, some feedback is printed: |
| 147 | +# |
| 148 | +puts "Standard validation errors found: #{acceptor.error_count}" |
| 149 | +puts "Standard validation warnings found: #{acceptor.warning_count}" |
| 150 | + |
| 151 | +# Validate using the 'MyValidation' defined above |
| 152 | +# |
| 153 | +validator = MyValidation::MyFactory.new().validator(acceptor) |
| 154 | + |
| 155 | +# Perform the validation - this adds the produced errors and warnings into the same acceptor |
| 156 | +# as was used for the standard validation |
| 157 | +# |
| 158 | +validator.validate(result) |
| 159 | + |
| 160 | +# We can print total statistics |
| 161 | +# (If we wanted to generated the extra validation separately we would have had to |
| 162 | +# use a separate acceptor, and then add everything in that acceptor to the main one.) |
| 163 | +# |
| 164 | +puts "Total validation errors found: #{acceptor.error_count}" |
| 165 | +puts "Total validation warnings found: #{acceptor.warning_count}" |
| 166 | + |
| 167 | +# Output the errors and warnings using a provided simple starter formatter |
| 168 | +formatter = MyValidation::Formatter.new |
| 169 | + |
| 170 | +puts "\nErrors and warnings found:" |
| 171 | +acceptor.errors_and_warnings.each do |diagnostic| |
| 172 | + puts formatter.format(diagnostic) |
| 173 | +end |
0 commit comments