Class: Goodcheck::ConfigLoader

Inherits:
Object
  • Object
show all
Includes:
ArrayHelper
Defined in:
lib/goodcheck/config_loader.rb

Defined Under Namespace

Classes: InvalidPattern

Constant Summary collapse

Schema =
StrongJSON.new do
  def self.array_or(type)
    a = array(type)
    enum(a, type, detector: -> (value) {
      case value
      when Array
        a
      else
        type
      end
    })
  end

  let :deprecated_regexp_pattern, object(regexp: string, case_insensitive: boolean?, multiline: boolean?)
  let :deprecated_literal_pattern, object(literal: string, case_insensitive: boolean?)
  let :deprecated_token_pattern, object(token: string, case_insensitive: boolean?)

  let :encoding, enum(*Encoding.name_list.map {|name| literal(name) })
  let :glob_obj, object(pattern: string, encoding: optional(encoding),
                        exclude: enum?(string, array(string)))
  let :one_glob, enum(glob_obj,
                      string,
                      detector: -> (value) {
                        case value
                        when Hash
                          glob_obj
                        when String
                          string
                        end
                      })
  let :glob, array_or(one_glob)

  let :var_pattern, any
  let :variable_pattern, array_or(var_pattern)
  let :negated_variable_pattern, object(not: variable_pattern)

  let :where, hash(
    enum(
      variable_pattern,
      negated_variable_pattern,
      literal(true),
      detector: -> (value) {
        case
        when value.is_a?(Hash) && value.key?(:not)
          negated_variable_pattern
        when value == true
          literal(true)
        else
          variable_pattern
        end
      }
    )
  )

  let :regexp_pattern, object(regexp: string, case_sensitive: boolean?, multiline: boolean?, glob: optional(glob))
  let :literal_pattern, object(literal: string, case_sensitive: boolean?, glob: optional(glob))
  let :token_pattern, object(
    token: string,
    case_sensitive: boolean?,
    glob: optional(glob),
    where: optional(where)
  )

  let :pattern, enum(regexp_pattern,
                     literal_pattern,
                     token_pattern,
                     deprecated_regexp_pattern,
                     deprecated_literal_pattern,
                     deprecated_token_pattern,
                     string,
                     detector: -> (value) {
                       case value
                       when Hash
                         case
                         when value.key?(:regexp) && value.key?(:case_insensitive)
                           deprecated_regexp_pattern
                         when value.key?(:regexp)
                           regexp_pattern
                         when value.key?(:literal) && value.key?(:case_insensitive)
                           deprecated_literal_pattern
                         when value.key?(:literal)
                           literal_pattern
                         when value.key?(:token) && value.key?(:case_insensitive)
                           deprecated_token_pattern
                         when value.key?(:token)
                           token_pattern
                         end
                       when String
                         string
                       end
                     })

  let :positive_rule, object(
    id: string,
    pattern: array_or(pattern),
    message: string,
    justification: optional(array_or(string)),
    glob: optional(glob),
    pass: optional(array_or(string)),
    fail: optional(array_or(string)),
    severity: optional(string)
  )

  let :negative_rule, object(
    id: string,
    not: object(pattern: array_or(pattern)),
    message: string,
    justification: optional(array_or(string)),
    glob: optional(glob),
    pass: optional(array_or(string)),
    fail: optional(array_or(string)),
    severity: optional(string)
  )

  let :nopattern_rule, object(
    id: string,
    message: string,
    justification: optional(array_or(string)),
    glob: glob,
    severity: optional(string)
  )

  let :positive_trigger, object(
    pattern: array_or(pattern),
    glob: optional(glob),
    pass: optional(array_or(string)),
    fail: optional(array_or(string))
  )

  let :negative_trigger, object(
    not: object(pattern: array_or(pattern)),
    glob: optional(glob),
    pass: optional(array_or(string)),
    fail: optional(array_or(string))
  )

  let :nopattern_trigger, object(
    glob: glob_obj
  )

  let :trigger, enum(
    positive_trigger,
    negative_trigger,
    nopattern_trigger,
    detector: -> (hash) {
      if hash.is_a?(Hash)
        case
        when hash.key?(:pattern)
          positive_trigger
        when hash.key?(:not)
          negative_trigger
        else
          nopattern_trigger
        end
      end
    }
  )

  let :triggered_rule, object(
    id: string,
    message: string,
    justification: optional(array_or(string)),
    trigger: array_or(trigger),
    severity: optional(string)
  )

  let :rule, enum(positive_rule,
                  negative_rule,
                  nopattern_rule,
                  triggered_rule,
                  detector: -> (hash) {
                    if hash.is_a?(Hash)
                      case
                      when hash[:trigger]
                        triggered_rule
                      when hash[:pattern]
                        positive_rule
                      when hash[:not]
                        negative_rule
                      when hash.key?(:glob) && !hash.key?(:pattern) && !hash.key?(:not)
                        nopattern_rule
                      end
                    end
                  })

  let :rules, array(rule)

  let :severity, object(
    allow: optional(array(string)),
    required: boolean?
  )

  let :config, object(
    rules: optional(rules),
    import: optional(array(string)),
    exclude: optional(array_or(string)),
    exclude_binary: boolean?,
    severity: optional(severity)
  )
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ArrayHelper

#array

Constructor Details

#initialize(path:, content:, stderr:, import_loader:) ⇒ ConfigLoader

Returns a new instance of ConfigLoader.



214
215
216
217
218
219
220
# File 'lib/goodcheck/config_loader.rb', line 214

def initialize(path:, content:, stderr:, import_loader:)
  @path = path
  @content = content
  @stderr = stderr
  @printed_warnings = Set.new
  @import_loader = import_loader
end

Instance Attribute Details

#contentObject (readonly)

Returns the value of attribute content.



209
210
211
# File 'lib/goodcheck/config_loader.rb', line 209

def content
  @content
end

#import_loaderObject (readonly)

Returns the value of attribute import_loader.



212
213
214
# File 'lib/goodcheck/config_loader.rb', line 212

def import_loader
  @import_loader
end

#pathObject (readonly)

Returns the value of attribute path.



208
209
210
# File 'lib/goodcheck/config_loader.rb', line 208

def path
  @path
end

#printed_warningsObject (readonly)

Returns the value of attribute printed_warnings.



211
212
213
# File 'lib/goodcheck/config_loader.rb', line 211

def printed_warnings
  @printed_warnings
end

#stderrObject (readonly)

Returns the value of attribute stderr.



210
211
212
# File 'lib/goodcheck/config_loader.rb', line 210

def stderr
  @stderr
end

Instance Method Details

#case_sensitive?(pattern) ⇒ Boolean

Returns:

  • (Boolean)


445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/goodcheck/config_loader.rb', line 445

def case_sensitive?(pattern)
  return true if pattern.is_a?(String)
  case
  when pattern.key?(:case_sensitive)
    pattern[:case_sensitive]
  when pattern.key?(:case_insensitive)
    print_warning_once "👻 `case_insensitive` option is deprecated. Use `case_sensitive` option instead."
    !pattern[:case_insensitive]
  else
    true
  end
end

#loadObject



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/goodcheck/config_loader.rb', line 222

def load
  Goodcheck.logger.info "Loading configuration: #{path}"
  Schema.config.coerce(content)

  rules = []

  load_rules(rules, array(content[:rules]))

  Array(content[:import]).each do |import|
    load_import rules, import
  end

  Config.new(
    rules: rules,
    exclude_paths: Array(content[:exclude]),
    exclude_binary: content[:exclude_binary],
    severity: content[:severity]
  )
end

#load_globs(globs) ⇒ Object



353
354
355
356
357
358
359
360
361
362
# File 'lib/goodcheck/config_loader.rb', line 353

def load_globs(globs)
  globs.map do |glob|
    case glob
    when String
      Glob.new(pattern: glob, encoding: nil, exclude: nil)
    when Hash
      Glob.new(pattern: glob[:pattern], encoding: glob[:encoding], exclude: glob[:exclude])
    end
  end
end

#load_import(rules, import) ⇒ Object



250
251
252
253
254
255
256
257
258
259
# File 'lib/goodcheck/config_loader.rb', line 250

def load_import(rules, import)
  Goodcheck.logger.info "Importing rules from #{import}"

  import_loader.load(import) do |content, filename|
    json = JSON.parse(JSON.dump(YAML.safe_load(content, filename: filename)), symbolize_names: true)

    Schema.rules.coerce json
    load_rules(rules, json)
  end
end

#load_pattern(pattern) ⇒ Object



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/goodcheck/config_loader.rb', line 364

def load_pattern(pattern)
  case pattern
  when String
    case (pat = load_string_pattern(pattern))
    when String
      Pattern::Literal.new(source: pat, case_sensitive: true)
    when ::Regexp
      Pattern::Regexp.new(source: pattern,
                          regexp: pat,
                          multiline: pat.options & ::Regexp::MULTILINE == ::Regexp::MULTILINE,
                          case_sensitive: !pat.casefold?)
    end
  when Hash
    if pattern[:glob]
      print_warning_once "🌏 Pattern with glob is deprecated: globs are ignored at all."
    end

    case
    when pattern[:literal]
      cs = case_sensitive?(pattern)
      literal = pattern[:literal]
      Pattern::Literal.new(source: literal, case_sensitive: cs)
    when pattern[:regexp]
      regexp = pattern[:regexp]
      cs = case_sensitive?(pattern)
      multiline = pattern[:multiline]
      Pattern::Regexp.new(source: regexp, case_sensitive: cs, multiline: multiline)
    when pattern[:token]
      tok = pattern[:token]
      cs = case_sensitive?(pattern)
      Pattern::Token.new(source: tok, variables: load_token_vars(pattern[:where]), case_sensitive: cs)
    end
  end
end

#load_rule(hash) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/goodcheck/config_loader.rb', line 261

def load_rule(hash)
  Goodcheck.logger.debug "Loading rule: #{hash[:id]}"

  id = hash[:id]
  triggers = retrieve_triggers(hash)
  justifications = array(hash[:justification])
  message = hash[:message].chomp
  severity = hash[:severity]

  Rule.new(id: id, message: message, justifications: justifications, triggers: triggers, severity: severity)
end

#load_rules(rules, array) ⇒ Object



242
243
244
245
246
247
248
# File 'lib/goodcheck/config_loader.rb', line 242

def load_rules(rules, array)
  array.each do |hash|
    rules << load_rule(hash)
  rescue RegexpError => exn
    raise InvalidPattern, "Invalid pattern of the `#{hash.fetch(:id)}` rule in `#{path}`: #{exn.message}"
  end
end

#load_string_pattern(string) ⇒ Object



399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/goodcheck/config_loader.rb', line 399

def load_string_pattern(string)
  if string =~ /\A\/(.*)\/([im]*)\Z/
    source = $1
    opts = $2
    options = 0
    options |= ::Regexp::IGNORECASE if opts =~ /i/
    options |= ::Regexp::MULTILINE if opts =~ /m/
    ::Regexp.new(source, options)
  else
    string
  end
end

#load_token_vars(pattern) ⇒ Object



412
413
414
415
416
417
418
419
420
421
# File 'lib/goodcheck/config_loader.rb', line 412

def load_token_vars(pattern)
  case pattern
  when Hash
    pattern.each.with_object({}) do |(key, value), hash|
      hash[key.to_sym] = load_var_pattern(value)
    end
  else
    {}
  end
end

#load_var_pattern(pattern) ⇒ Object



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/goodcheck/config_loader.rb', line 423

def load_var_pattern(pattern)
  if pattern.is_a?(Hash) && pattern[:not]
    negated = true
    pattern = pattern[:not]
  else
    negated = false
  end

  pattern = [] if pattern == true

  patterns = array(pattern).map do |pat|
    case pat
    when String
      load_string_pattern(pat)
    else
      pat
    end
  end

  Pattern::Token::VarPattern.new(patterns: patterns, negated: negated)
end


458
459
460
461
462
463
# File 'lib/goodcheck/config_loader.rb', line 458

def print_warning_once(message)
  unless printed_warnings.include?(message)
    stderr.puts "[Warning] " + message
    printed_warnings << message
  end
end

#retrieve_patterns(hash) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/goodcheck/config_loader.rb', line 338

def retrieve_patterns(hash)
  if hash.is_a?(Hash) && hash.key?(:not)
    negated = true
    hash = hash[:not]
  else
    negated = false
  end

  if hash.key?(:pattern)
    [array(hash[:pattern]).map {|pat| load_pattern(pat) }, negated]
  else
    [[], false]
  end
end

#retrieve_trigger(hash) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/goodcheck/config_loader.rb', line 325

def retrieve_trigger(hash)
  patterns, negated = retrieve_patterns(hash)
  globs = load_globs(array(hash[:glob]))
  passes = array(hash[:pass])
  fails = array(hash[:fail])

  Trigger.new(patterns: patterns,
              globs: globs,
              passes: passes,
              fails: fails,
              negated: negated)
end

#retrieve_triggers(hash) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/goodcheck/config_loader.rb', line 273

def retrieve_triggers(hash)
  if hash.key?(:trigger)
    array(hash[:trigger]).map do |trigger|
      retrieve_trigger(trigger)
    end
  else
    globs = load_globs(array(hash[:glob]))
    passes = array(hash[:pass])
    fails = array(hash[:fail])

    if hash.key?(:not) || hash.key?(:pattern)
      if hash.key?(:not)
        negated = true
        patterns = array(hash[:not][:pattern])
      else
        negated = false
        patterns = array(hash[:pattern])
      end

      glob_patterns, noglob_patterns = patterns.partition {|pat|
        pat.is_a?(Hash) && pat.key?(:glob)
      }

      skip_fails = !fails.empty? && !glob_patterns.empty?

      glob_patterns.map do |pat|
        Trigger.new(
          patterns: [load_pattern(pat)],
          globs: load_globs(array(pat[:glob])),
          passes: passes,
          fails: [],
          negated: negated
        ).by_pattern!.skips_fail_examples!(skip_fails)
      end.push(
        Trigger.new(
          patterns: noglob_patterns.map {|pat| load_pattern(pat) },
          globs: globs,
          passes: passes,
          fails: glob_patterns.empty? ? fails : [],
          negated: negated
        ).by_pattern!.skips_fail_examples!(skip_fails)
      )
    else
      [Trigger.new(patterns: [],
                   globs: globs,
                   passes: passes,
                   fails: fails,
                   negated: false).by_pattern!]
    end
  end
end