codeowner_validator 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +6 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
  5. data/.github/pull_request_template.md +32 -0
  6. data/.github/workflows/cd.yml +53 -0
  7. data/.github/workflows/ci.yml +39 -0
  8. data/.gitignore +23 -0
  9. data/.rspec +4 -0
  10. data/.rubocop.yml +926 -0
  11. data/.ruby-gemset +1 -0
  12. data/.ruby-version +1 -0
  13. data/.vscode/launch.json +53 -0
  14. data/CHANGELOG.md +5 -0
  15. data/CODE_OF_CONDUCT.md +74 -0
  16. data/CONTRIBUTING.md +40 -0
  17. data/CONTRIBUTORS.md +3 -0
  18. data/Gemfile +20 -0
  19. data/LICENSE +205 -0
  20. data/NOTICE +13 -0
  21. data/README.md +160 -0
  22. data/Rakefile +8 -0
  23. data/bin/bundle-audit +29 -0
  24. data/bin/bundler-audit +29 -0
  25. data/bin/codeowner_validator +8 -0
  26. data/bin/rake +29 -0
  27. data/bin/rspec +29 -0
  28. data/bin/rubocop +29 -0
  29. data/codeowner_validator.gemspec +41 -0
  30. data/lib/codeowner_validator/cli/validator_cli.rb +64 -0
  31. data/lib/codeowner_validator/code_owners.rb +308 -0
  32. data/lib/codeowner_validator/common/command.rb +40 -0
  33. data/lib/codeowner_validator/common/logging.rb +86 -0
  34. data/lib/codeowner_validator/common/tasks/base.rb +62 -0
  35. data/lib/codeowner_validator/group/comment/error.rb +21 -0
  36. data/lib/codeowner_validator/group/comment/info.rb +21 -0
  37. data/lib/codeowner_validator/group/comment/verbose.rb +21 -0
  38. data/lib/codeowner_validator/group/comment/warn.rb +21 -0
  39. data/lib/codeowner_validator/group/comment.rb +55 -0
  40. data/lib/codeowner_validator/helpers/config_helper.rb +73 -0
  41. data/lib/codeowner_validator/helpers/utility_helper.rb +30 -0
  42. data/lib/codeowner_validator/lists/whitelist.rb +145 -0
  43. data/lib/codeowner_validator/tasks/duplicate_checker.rb +32 -0
  44. data/lib/codeowner_validator/tasks/file_exists_checker.rb +35 -0
  45. data/lib/codeowner_validator/tasks/missing_assignment_checker.rb +32 -0
  46. data/lib/codeowner_validator/tasks/syntax_checker.rb +32 -0
  47. data/lib/codeowner_validator/validator.rb +76 -0
  48. data/lib/codeowner_validator/version.rb +11 -0
  49. data/lib/codeowner_validator.rb +32 -0
  50. data/lib/codeowners/checker/group/line.rb +12 -0
  51. metadata +204 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/group/comment'
4
+
5
+ module CodeownerValidator
6
+ module Group
7
+ module Comment
8
+ # Public: An error comment response
9
+ class Error
10
+ include Comment
11
+
12
+ class << self
13
+ # @see CodeownerValidator::Group::Comment.match?
14
+ def match?(type)
15
+ Comment::TYPE_ERROR == type
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/group/comment'
4
+
5
+ module CodeownerValidator
6
+ module Group
7
+ module Comment
8
+ # Public: An info comment response
9
+ class Info
10
+ include Comment
11
+
12
+ class << self
13
+ # @see CodeownerValidator::Group::Comment.match?
14
+ def match?(type)
15
+ Comment::TYPE_INFO == type
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/group/comment'
4
+
5
+ module CodeownerValidator
6
+ module Group
7
+ module Comment
8
+ # Public: An info comment response
9
+ class Verbose
10
+ include Comment
11
+
12
+ class << self
13
+ # @see CodeownerValidator::Group::Comment.match?
14
+ def match?(type)
15
+ Comment::TYPE_VERBOSE == type
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/group/comment'
4
+
5
+ module CodeownerValidator
6
+ module Group
7
+ module Comment
8
+ # Public: A warn comment response
9
+ class Warn
10
+ include Comment
11
+
12
+ class << self
13
+ # @see CodeownerValidator::Group::Comment.match?
14
+ def match?(type)
15
+ Comment::TYPE_WARN == type
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodeownerValidator
4
+ module Group
5
+ # Public: Object for storing base comment information that can be rendered appropriately by the consumer
6
+ module Comment
7
+ TYPE_VERBOSE = 1
8
+ TYPE_INFO = 2
9
+ TYPE_WARN = 3
10
+ TYPE_ERROR = 4
11
+
12
+ # to keep track of hierarchical structure of comments and allow grouping
13
+ attr_accessor :parent
14
+ attr_reader :comment
15
+
16
+ class << self
17
+ # Public: Creates an instance of the comment for usage.
18
+ #
19
+ # @param [Hash] _args The hash of arguments accepted
20
+ # @option _args [String] :comment The comment to create
21
+ # @option _args [Integer] :type The comment type (info, warn, error, verbose)
22
+ #
23
+ # @return [Info|]
24
+ def build(comment:, type: TYPE_INFO, **_args)
25
+ subclasses.each do |klass|
26
+ return klass.new(comment) if klass.match?(type)
27
+ end
28
+ raise "Type '#{type}' not supported" # rubocop:disable Style/ImplicitRuntimeError
29
+ end
30
+
31
+ # Public: Returns <true> if the type of object requested is supported by the object; otherwise, <false>
32
+ #
33
+ # @param [Integer] type The type of comment to match on a subclass
34
+ def match?(type); end
35
+
36
+ private
37
+
38
+ # returns the available subclasses to the base object
39
+ def subclasses
40
+ [Info, Warn, Error, Verbose]
41
+ end
42
+ end
43
+
44
+ # Public: Creates an instance of the comment
45
+ #
46
+ # @param [String] comment The string text of the comment
47
+ def initialize(comment)
48
+ @comment = comment
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # require all subclasses
55
+ Dir.glob(File.join(File.dirname(__FILE__), 'comment', '**/*.rb'), &method(:require))
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require 'tty-spinner'
5
+
6
+ module CodeownerValidator
7
+ # Public: A configuration helper designed to assist in simple abstraction of TTY:Prompt requests
8
+ # and assign those responses to the base module's configuration
9
+ class ConfigHelper
10
+ class << self
11
+ # Public: An abstraction onto the TTY:Prompt.ask to allow requesting information from the user's input
12
+ # and saving that input within the base module's configuration
13
+ #
14
+ # @param [Hash] args The arguments in which to accept
15
+ # @option args [String] :ident The identifier to the config to store
16
+ # @option args [String] :prompt The prompt to display to the user
17
+ # @option args [String] :default The default value to display
18
+ # @option args [Boolean] :required Indicates if the requested ask required input from the user
19
+ # @option args [Boolean] :force_ask Indicates to ignore previous ask and perform again
20
+ # @option args [Boolean] :mask Indicates that the response should be hidden
21
+ def ask(ident:, prompt:, required: false, force_ask: false, mask: false, **args)
22
+ opts =
23
+ {}.tap do |h|
24
+ h[:required] = required
25
+ h[:default] = args[:default] if args[:default]
26
+ end
27
+
28
+ # return if either not being forced to ask or the information has been previously captured
29
+ return if !force_ask && ::CodeownerValidator.respond_to?(ident)
30
+
31
+ tty_prompt = ::TTY::Prompt.new
32
+ response =
33
+ tty_prompt.collect do
34
+ if mask
35
+ key(ident.to_sym).mask(
36
+ prompt,
37
+ opts
38
+ )
39
+ else
40
+ key(ident.to_sym).ask(
41
+ prompt,
42
+ opts
43
+ )
44
+ end
45
+ end
46
+
47
+ ::CodeownerValidator.configure! response
48
+ end
49
+
50
+ # Public: An abstraction onto the TTY:Prompt.select to allow requesting information from the user's input
51
+ # and saving that input within the base module's configuration
52
+ #
53
+ # @param [Hash] args The arguments in which to accept
54
+ # @option args [String] :ident The identifier to the config to store
55
+ # @option args [String] :prompt The prompt to display to the user
56
+ # @option args [Boolean] :force_ask Indicates to ignore previous ask and perform again
57
+ # @option args [Boolean] :choices The array of choices available for selection by the user
58
+ def select(ident:, prompt:, force_ask: false, choices:, **args)
59
+ # return if either not being forced to ask or the information has been previously captured
60
+ return if !force_ask && ::CodeownerValidator.respond_to?(ident)
61
+
62
+ tty_prompt = ::TTY::Prompt.new
63
+ response = tty_prompt.select(
64
+ prompt,
65
+ choices,
66
+ args
67
+ )
68
+
69
+ ::CodeownerValidator.configure! ident => response
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/ImplicitRuntimeError
4
+ module CodeownerValidator
5
+ # Public: A utility helper to provide common methods for reuse across multiple
6
+ # classes
7
+ module UtilityHelper
8
+ # Provides a way to change the current working directory to a different folder location.
9
+ # This ability can ease the reference of file references when working with multiple
10
+ # repository locations.
11
+ #
12
+ # @raise [RuntimeError] if the folder location does not exist.
13
+ def in_folder(folder)
14
+ raise "The folder location '#{folder}' does not exists" unless File.directory?(folder)
15
+
16
+ if defined?(Bundler)
17
+ Bundler.with_clean_env do
18
+ Dir.chdir folder do
19
+ yield
20
+ end
21
+ end
22
+ else
23
+ Dir.chdir folder do
24
+ yield
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ # rubocop:enable Style/ImplicitRuntimeError
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodeownerValidator
4
+ module Lists
5
+ # Manage whitelist file reading
6
+ class Whitelist
7
+ attr_reader :repo_path
8
+
9
+ # Public: Initialized with a provided file
10
+ #
11
+ # @param [String] filename The filename to initialize the list with
12
+ # @param [String] repo_path The repository base path utilized for evaluation of a .gitignore
13
+ def initialize(filename: nil, repo_path: nil)
14
+ @filename = filename
15
+ @repo_path = repo_path
16
+
17
+ # to avoid instance variable not initialized warnings
18
+ @whitelist_file = nil
19
+ @whitelist_file_paths = nil
20
+ @git_ignore_file = nil
21
+ end
22
+
23
+ # Public: Checks for existence configuration for the whitelist
24
+ #
25
+ # @return <true> if found; otherwise, <false>
26
+ def exist?
27
+ !pathspec.empty?
28
+ end
29
+
30
+ # Public: Returns <true> if the file supplied has been whitelisted; otherwise, <false>
31
+ #
32
+ # @param [String] filename The file to evaluate if has been whitelisted
33
+ # @return <true> if the file supplied has been whitelisted; otherwise, <false>
34
+ def whitelisted?(filename)
35
+ # if no items in the whitelist, assume all items are whitelisted
36
+ return true if pathspec.empty?
37
+
38
+ listed?(filename)
39
+ end
40
+
41
+ # add a `to_proc` method that allows instances of this class to be passed as a block
42
+ # for easier chaining executions
43
+ def to_proc
44
+ proc { |item| whitelisted?(item) }
45
+ end
46
+
47
+ private
48
+
49
+ # search for the .gitignore file if provided a repo path
50
+ def git_ignore_file
51
+ # if no repo, there can be no discovery of a file
52
+ return unless repo_path
53
+
54
+ return @git_ignore_file if @git_ignore_file
55
+
56
+ file = File.join(repo_path, '.gitignore')
57
+ @git_ignore_file = file if File.exist?(file)
58
+ end
59
+
60
+ # search for an actual file within the repository named 'CODEOWNERS_WHITELIST'
61
+ def whitelist_file
62
+ # if no repo, there can be no discovery of a file
63
+ return unless repo_path
64
+
65
+ return @whitelist_file if @whitelist_file
66
+
67
+ whitelist_file_paths.each do |path|
68
+ current_file_path = File.join(repo_path, path)
69
+ return current_file_path if File.exist?(current_file_path)
70
+ end
71
+
72
+ nil
73
+ end
74
+
75
+ # locations to search for the repository driven codeowners whitelist file
76
+ def whitelist_file_paths
77
+ return @whitelist_file_paths if @whitelist_file_paths
78
+
79
+ @whitelist_file_paths = %w[CODEOWNERS_WHITELIST .github/CODEOWNERS_WHITELIST]
80
+
81
+ # allow customization of the locations to search for the file
82
+ if ENV['CODEOWNER_WHITELIST_FILE_PATHS']
83
+ ENV['CODEOWNER_WHITELIST_FILE_PATHS']&.split(',')&.each(&:strip!)&.each do |str|
84
+ @whitelist_file_paths << str
85
+ end
86
+ end
87
+
88
+ @whitelist_file_paths
89
+ end
90
+
91
+ # checks for the match of the filename
92
+ def listed?(filename)
93
+ pathspec.match(filename)
94
+ end
95
+
96
+ # the pathspec is a combination of files and variables:
97
+ # * CODEOWNERS_WHITELIST - if provided a repo_path, file that exists in either the root repo path
98
+ # or alongside the CODEOWNERS file
99
+ # * CODEOWNERS_WHITELIST - env variable of comma separated
100
+ # * .gitignore - if provided a repo_path, this file is automatically added
101
+ def pathspec
102
+ # to avoid instance variable @pathspec not initialized must check if defined prior
103
+ return @pathspec if defined?(@pathspec) && @pathspec
104
+
105
+ @pathspec = PathSpec.new([])
106
+
107
+ # first, add repo driven codeowners whitelist file
108
+ @pathspec.add ::File.readlines(whitelist_file).map(&:chomp) if whitelist_file
109
+
110
+ # second, add provided file
111
+ @pathspec.add ::File.readlines(@filename).map(&:chomp) if @filename && File.exist?(@filename)
112
+
113
+ # third, add items from the CODEOWNERS_WHITELIST
114
+ if ENV['CODEOWNERS_WHITELIST']
115
+ new_items =
116
+ [].tap do |ar|
117
+ ENV['CODEOWNERS_WHITELIST']&.split(',')&.each(&:strip!)&.each do |str|
118
+ ar << str
119
+ end
120
+ end
121
+
122
+ @pathspec.add new_items
123
+ end
124
+
125
+ # last, add items from the .gitignore. this must always be last because
126
+ # of the check for empty specs at this point in which will require inclusion of
127
+ # all files prior to adding items to exclude.
128
+
129
+ # if a gitignore exists, add each reference
130
+ if git_ignore_file
131
+ # first, check if existing pathspec evaluation is empty, if yes, need to add all files '**'
132
+ # because the pathspec evaluation will only make evaluations based on the inclusive=false so need
133
+ # to first include all files to provide an entry to inclusive=true
134
+ @pathspec.add ['**'] if @pathspec.empty?
135
+
136
+ @pathspec.add ::File.readlines(git_ignore_file).map do |line|
137
+ "!#{line.chomp}"
138
+ end
139
+ end
140
+
141
+ @pathspec
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/common/tasks/base'
4
+ require 'codeowner_validator/group/comment'
5
+
6
+ module CodeownerValidator
7
+ module Tasks
8
+ # Public: The duplicate checker executes an evaluation on the code owners file looking for duplicate
9
+ # pattern references
10
+ class DuplicateChecker < Base
11
+ include ::CodeownerValidator::Group
12
+
13
+ # @see ::CodeownerValidator::Tasks::Base.summary
14
+ def summary
15
+ 'Executing Duplicated Pattern Checker'
16
+ end
17
+
18
+ # @see ::CodeownerValidator::Tasks::Base.comments
19
+ def comments
20
+ comments = []
21
+
22
+ codeowners.duplicated_patterns.each do |key, value|
23
+ msg = "Pattern '#{key}' is defined #{value.size} times on lines " \
24
+ "#{value.map(&:line_number).join(', ')}"
25
+ comments << Comment.build(comment: msg, type: Comment::TYPE_ERROR)
26
+ end
27
+
28
+ comments
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/common/tasks/base'
4
+ require 'codeowner_validator/group/comment'
5
+
6
+ module CodeownerValidator
7
+ module Tasks
8
+ # Public: The file existence checker executes an evaluation on the code owners file looking for references
9
+ # to non-existent files within the repository
10
+ class FileExistsChecker < Base
11
+ include ::CodeownerValidator::Group
12
+
13
+ # @see ::CodeownerValidator::Tasks::Base.summary
14
+ def summary
15
+ 'Executing File Exists Checker'
16
+ end
17
+
18
+ # @see ::CodeownerValidator::Tasks::Base.comments
19
+ def comments
20
+ comments = []
21
+
22
+ codeowners.invalid_reference_lines.each do |line|
23
+ file_name = line.pattern? ? line.pattern : line
24
+ msg = "line #{line.line_number}: '#{file_name}' does not match any files in the repository"
25
+ comments << Comment.build(
26
+ comment: msg,
27
+ type: Comment::TYPE_ERROR
28
+ )
29
+ end
30
+
31
+ comments
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/common/tasks/base'
4
+
5
+ module CodeownerValidator
6
+ module Tasks
7
+ # Public: The missing assignment checker executes an evaluation on the code owners file looking for files
8
+ # within the repository that are not noted as been assigned by the codeowners file
9
+ class MissingAssignmentChecker < Base
10
+ include ::CodeownerValidator::Group
11
+
12
+ # @see ::CodeownerValidator::Tasks::Base.summary
13
+ def summary
14
+ 'Executing Missing Assignment Checker'
15
+ end
16
+
17
+ # @see ::CodeownerValidator::Tasks::Base.comments
18
+ def comments
19
+ comments = []
20
+
21
+ codeowners.missing_assignments.each do |file|
22
+ comments << Comment.build(
23
+ comment: "File '#{file}' is missing from the code owners file",
24
+ type: Comment::TYPE_ERROR
25
+ )
26
+ end
27
+
28
+ comments
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/common/tasks/base'
4
+
5
+ module CodeownerValidator
6
+ module Tasks
7
+ # Public: The syntax checker executes an evaluation on the code owners file looking for missing assignment
8
+ # within the file itself
9
+ class SyntaxChecker < Base
10
+ include ::CodeownerValidator::Group
11
+
12
+ # @see ::CodeownerValidator::Tasks::Base.summary
13
+ def summary
14
+ 'Executing Valid Syntax Checker'
15
+ end
16
+
17
+ # @see ::CodeownerValidator::Tasks::Base.comments
18
+ def comments
19
+ comments = []
20
+
21
+ codeowners.unrecognized_assignments.each do |line|
22
+ comments << Comment.build(
23
+ comment: "line #{line.line_number}: Missing owner, at least one owner is required",
24
+ type: Comment::TYPE_ERROR
25
+ )
26
+ end
27
+
28
+ comments
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codeowner_validator/common/tasks/base'
4
+ Dir.glob(File.join(File.dirname(__FILE__), 'tasks', '**/*.rb'), &method(:require))
5
+ require 'codeowner_validator/group/comment'
6
+
7
+ module CodeownerValidator
8
+ # Public: The validator is utilized for execution of the tasks associated to the code owner validation tasks.
9
+ # It has the option to either execute a validation and automatically output the stdout or return an [Array]
10
+ # of comments for the consumer to do as they please. The CLI execution will route all comments to stdout.
11
+ class Validator < ::CodeownerValidator::Tasks::Base
12
+ include ::CodeownerValidator::Group
13
+
14
+ # Public: Creates an instance of the executor with provided tasks
15
+ #
16
+ # @param [Hash] options The user provided options from the command line
17
+ # @options options [Array] :tasks Array of tasks that should be specifically executed by the merger
18
+ def initialize(tasks: [], **options)
19
+ super
20
+ @options = options
21
+
22
+ # allow defining what tasks the merger should execute
23
+ @tasks = tasks.map { |task_class| task_class.new(**@options) } unless tasks.empty?
24
+ end
25
+
26
+ # Public: Returns an array of summaries associated to all tasks that are to be executed
27
+ #
28
+ # @return [Array] An array of strings describing the tasks that are to be executed
29
+ def summary
30
+ tasks.map { |t| " * #{t.summary}" }
31
+ end
32
+
33
+ # Public: Performs the execution of all tasks
34
+ def validate
35
+ log_verbose(%w[Started:] + summary)
36
+
37
+ in_repo_folder do
38
+ tasks&.each(&:execute)
39
+ end
40
+
41
+ log_info 'VALIDATION complete! 🌟'
42
+ end
43
+
44
+ # Public: Performs the execution of all tasks and returns the comments to be interpreted
45
+ def comments
46
+ comments = []
47
+
48
+ in_repo_folder do
49
+ tasks&.each do |task|
50
+ parent = Comment.build(
51
+ comment: task.summary,
52
+ type: Comment::TYPE_VERBOSE
53
+ )
54
+ task.comments.each do |comment|
55
+ comment.parent = parent
56
+ comments << comment
57
+ end
58
+ end
59
+ end
60
+
61
+ comments.group_by(&:parent)
62
+ end
63
+
64
+ private
65
+
66
+ def tasks
67
+ @tasks ||=
68
+ [
69
+ Tasks::DuplicateChecker,
70
+ Tasks::SyntaxChecker,
71
+ Tasks::FileExistsChecker,
72
+ Tasks::MissingAssignmentChecker
73
+ ].compact.map { |task_class| task_class.new(@options) } # rubocop:disable Performance/ChainArrayAllocation
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodeownerValidator
4
+ VERSION = '0.1.1'
5
+
6
+ # version module
7
+ module Version
8
+ MAJOR, MINOR, PATCH, *BUILD = VERSION.split '.'
9
+ NUMBERS = [MAJOR, MINOR, PATCH, *BUILD].freeze
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'codeowners/checker'
5
+ # pull in monkeypatch for codeowners-checker
6
+ require_relative 'codeowners/checker/group/line'
7
+ Dir.glob(File.join(File.dirname(__FILE__), 'codeowner_validator', '**/*.rb'), &method(:require))
8
+
9
+ # Public: The code owner validator space is utilized for validations against
10
+ # the code owner file for a given repository.
11
+ module CodeownerValidator
12
+ class << self
13
+ # Public: Provides the ability to configure instance variables within the module. If the
14
+ # method already exists, the value provide will be overwritten.
15
+ #
16
+ # @params [Hash] attrs The key/value paired items to be configured on the module.
17
+ def configure!(attrs = {})
18
+ attrs.each do |name, value|
19
+ name = name.to_s.to_sym
20
+ # protect against multiple executions
21
+ singleton_class.instance_eval { attr_accessor name } unless self.class.method_defined?(name)
22
+ send("#{name}=", value)
23
+ end
24
+ end
25
+ end
26
+
27
+ # Mail CLI
28
+ class CLI < Thor
29
+ desc 'validate', 'validates the codeowners file'
30
+ subcommand 'validate', ValidatorCLI
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # monkeypatch class to include line number
4
+ module Codeowners
5
+ class Checker
6
+ class Group
7
+ class Line
8
+ attr_accessor :line_number
9
+ end
10
+ end
11
+ end
12
+ end