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))
  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))
  )

  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))
  )

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

  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)
  )

  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 :import_target, string
  let :imports, array(import_target)
  let :exclude, array_or(string)

  let :config, object(
    rules: optional(rules),
    import: optional(imports),
    exclude: optional(exclude)
  )
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.



206
207
208
209
210
211
212
# File 'lib/goodcheck/config_loader.rb', line 206

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

#content ⇒ Object (readonly)

Returns the value of attribute content.



201
202
203
# File 'lib/goodcheck/config_loader.rb', line 201

def content
  @content
end

#import_loader ⇒ Object (readonly)

Returns the value of attribute import_loader.



204
205
206
# File 'lib/goodcheck/config_loader.rb', line 204

def import_loader
  @import_loader
end

#path ⇒ Object (readonly)

Returns the value of attribute path.



200
201
202
# File 'lib/goodcheck/config_loader.rb', line 200

def path
  @path
end

#printed_warnings ⇒ Object (readonly)

Returns the value of attribute printed_warnings.



203
204
205
# File 'lib/goodcheck/config_loader.rb', line 203

def printed_warnings
  @printed_warnings
end

#stderr ⇒ Object (readonly)

Returns the value of attribute stderr.



202
203
204
# File 'lib/goodcheck/config_loader.rb', line 202

def stderr
  @stderr
end

Instance Method Details

#case_sensitive?(pattern) ⇒ Boolean

Returns:

  • (Boolean)


435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/goodcheck/config_loader.rb', line 435

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

#load ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/goodcheck/config_loader.rb', line 214

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

    rules = []

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

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

    exclude_paths = Array(content[:exclude])

    Config.new(rules: rules, exclude_paths: exclude_paths)
  end
end

#load_globs(globs) ⇒ Object



343
344
345
346
347
348
349
350
351
352
# File 'lib/goodcheck/config_loader.rb', line 343

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

#load_import(rules, import) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/goodcheck/config_loader.rb', line 239

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

  Goodcheck.logger.tagged import do
    import_loader.load(import) do |content|
      json = JSON.parse(JSON.dump(YAML.load(content, import)), symbolize_names: true)

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

#load_pattern(pattern) ⇒ Object



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/goodcheck/config_loader.rb', line 354

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.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



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

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

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

#load_rules(rules, array) ⇒ Object



233
234
235
236
237
# File 'lib/goodcheck/config_loader.rb', line 233

def load_rules(rules, array)
  array.each do |hash|
    rules << load_rule(hash)
  end
end

#load_string_pattern(string) ⇒ Object



389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/goodcheck/config_loader.rb', line 389

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



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

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



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/goodcheck/config_loader.rb', line 413

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


448
449
450
451
452
453
# File 'lib/goodcheck/config_loader.rb', line 448

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

#retrieve_patterns(hash) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/goodcheck/config_loader.rb', line 328

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



315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/goodcheck/config_loader.rb', line 315

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



263
264
265
266
267
268
269
270
271
272
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
# File 'lib/goodcheck/config_loader.rb', line 263

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