File: puppet-lint.rb

package info (click to toggle)
puppet-lint 4.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 932 kB
  • sloc: ruby: 11,769; sh: 16; makefile: 11
file content (266 lines) | stat: -rw-r--r-- 7,813 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# rubocop:disable Naming/FileName

# encoding: utf-8

require 'set'
require 'json'
require 'puppet-lint/version'
require 'puppet-lint/lexer'
require 'puppet-lint/configuration'
require 'puppet-lint/data'
require 'puppet-lint/checks'
require 'puppet-lint/report/github'
require 'puppet-lint/bin'
require 'puppet-lint/monkeypatches'

class PuppetLint::NoCodeError < StandardError; end

class PuppetLint::NoFix < StandardError; end

# Parser Syntax Errors
class PuppetLint::SyntaxError < StandardError
  attr_reader :token

  def initialize(token)
    @token = token
    super
  end
end

# Public: The public interface to puppet-lint.
class PuppetLint
  # Public: Gets/Sets the String manifest code to be checked.
  attr_accessor :code

  # Public: Gets the String manifest with the errors fixed.
  attr_reader :manifest

  # Public: Returns an Array of Hashes describing the problems found in the
  # manifest.
  #
  # Each Hash will contain *at least*:
  #   :check   - The Symbol name of the check that generated the problem.
  #   :kind    - The Symbol kind of the problem (:error, :warning, or
  #              :fixed).
  #   :line    - The Integer line number of the location of the problem in
  #              the manifest.
  #   :column  - The Integer column number of the location of the problem in
  #              the manifest.
  #   :message - The String message describing the problem that was found.
  attr_reader :problems

  # Public: Gets/Sets the String path to the manifest to be checked.
  attr_accessor :path

  # Public: Returns a Hash of linter statistics
  #
  #   :error   - An Integer count of errors found in the manifest.
  #   :warning - An Integer count of warnings found in the manifest.
  #   :fixed   - An Integer count of problems found in the manifest that were
  #              automatically fixed.
  attr_reader :statistics

  # Public: Initialise a new PuppetLint object.
  def initialize
    @code = nil
    @statistics = { error: 0, warning: 0, fixed: 0, ignored: 0 }
    @manifest = ''
  end

  # Public: Access PuppetLint's configuration from outside the class.
  #
  # Returns a PuppetLint::Configuration object.
  def self.configuration
    @configuration ||= PuppetLint::Configuration.new
  end

  # Public: Access PuppetLint's configuration from inside the class.
  #
  # Returns a PuppetLint::Configuration object.
  def configuration
    self.class.configuration
  end

  # Public: Set the path of the manifest file to be tested and read the
  # contents of the file.
  #
  # Returns nothing.
  def file=(path)
    return unless File.exist?(path)

    @path = path
    File.open(path, 'rb:UTF-8') do |f|
      @code = f.read
    end

    # Check if the input is an SE Linux policy package file (which also use
    # the .pp extension), which all have the first 4 bytes 0xf97cff8f.
    @code = '' if @code[0..3].unpack1('V') == 0xf97cff8f
  end

  # Internal: Retrieve the format string to be used when writing problems to
  # STDOUT.  If the user has not specified a custom log format, build one for
  # them.
  #
  # Returns a format String to be used with String#%.
  def log_format
    if configuration.log_format.nil? || configuration.log_format.empty?
      format = '%{KIND}: %{message} on line %{line}'
      format.prepend('%{path} - ') if configuration.with_filename
      format.concat(' (check: %{check})')
      configuration.log_format = format
    end

    configuration.log_format
  end

  # Internal: Format a problem message and print it to STDOUT.
  #
  # message - A Hash containing all the information about a problem.
  #
  # Returns nothing.
  def format_message(message)
    format = log_format
    puts format % message

    puts "  #{message[:reason]}" if message[:kind] == :ignored && !message[:reason].nil?
    print_context(message)
  end

  # Internal: Format a problem message and print it to STDOUT so GitHub Actions
  # recognizes it as an annotation.
  #
  # message - A Hash containing all the information about a problem.
  #
  # Returns nothing.
  def print_github_annotation(message)
    puts PuppetLint::Report::GitHubActionsReporter.format_problem(path, message)
  end

  # Internal: Get the line of the manifest on which the problem was found
  #
  # message - A Hash containing all the information about a problem.
  #
  # Returns the problematic line as a string.
  def get_context(message)
    PuppetLint::Data.manifest_lines[message[:line] - 1].strip
  end

  # Internal: Print out the line of the manifest on which the problem was found
  # as well as a marker pointing to the location on the line.
  #
  # message - A Hash containing all the information about a problem.
  #
  # Returns nothing.
  def print_context(message)
    return if message[:check] == 'documentation'
    return if message[:kind] == :fixed

    line = message[:context]
    return unless line

    offset = line.index(%r{\S}) || 1
    puts "\n  #{line.strip}"
    printf("%#{message[:column] + 2 - offset}s\n\n", '^')
  end

  # Internal: Print the reported problems with a manifest to stdout.
  #
  # problems - An Array of problem Hashes as returned by
  #            PuppetLint::Checks#run.
  #
  # Returns array of problem.
  def report(problems)
    json = []
    print_stdout = !(configuration.json || configuration.sarif)

    problems.each do |message|
      next if message[:kind] == :ignored && !PuppetLint.configuration.show_ignored

      message[:KIND] = message[:kind].to_s.upcase

      next unless message[:kind] == :fixed || [message[:kind], :all].include?(configuration.error_level)

      message[:context] = get_context(message) if configuration.with_context

      json << message

      if print_stdout
        format_message(message)
        print_github_annotation(message) if configuration.github_actions
      end
    end
    warn 'Try running `puppet parser validate <file>`' if problems.any? { |p| p[:check] == :syntax }
    json
  end

  # Public: Determine if PuppetLint found any errors in the manifest.
  #
  # Returns true if errors were found, otherwise returns false.
  def errors?
    @statistics[:error] != 0
  end

  # Public: Determine if PuppetLint found any warnings in the manifest.
  #
  # Returns true if warnings were found, otherwise returns false.
  def warnings?
    @statistics[:warning] != 0
  end

  # Public: Run the loaded manifest code through the lint checks and print the
  # results of the checks to stdout.
  #
  # Returns nothing.
  # Raises PuppetLint::NoCodeError if no manifest code has been loaded.
  def run
    raise PuppetLint::NoCodeError if @code.nil?

    if @code.empty?
      @problems = []
      @manifest = ''
      return
    end

    linter = PuppetLint::Checks.new
    @problems = linter.run(@path, @code)
    @problems.each { |problem| @statistics[problem[:kind]] += 1 }

    @manifest = linter.manifest if PuppetLint.configuration.fix
  end

  # Public: Print any problems that were found out to stdout.
  #
  # Returns an array of problems.
  def print_problems
    report(@problems)
  end

  # Public: Define a new check.
  #
  # name  - A unique name for the check as a Symbol.
  # block - The check logic. This must contain a `check` method and optionally
  #         a `fix` method.
  #
  # Returns nothing.
  #
  # Examples
  #
  #   PuppetLint.new_check(:foo) do
  #     def check
  #     end
  #   end
  def self.new_check(name, &block)
    class_name = name.to_s.split('_').map(&:capitalize).join
    klass = PuppetLint.const_set(:"Check#{class_name}", Class.new(PuppetLint::CheckPlugin))
    klass.const_set(:NAME, name)
    klass.class_exec(&block)
    PuppetLint.configuration.add_check(name, klass)
    PuppetLint::Data.ignore_overrides[name] ||= {}
  end
end

# Default configuration options
PuppetLint.configuration.defaults

require 'puppet-lint/plugins'