codeowner_validator 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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