reviewer 0.1.4 → 1.0.0

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.alexignore +1 -0
  3. data/.github/FUNDING.yml +3 -0
  4. data/.github/workflows/main.yml +81 -11
  5. data/.github/workflows/release.yml +98 -0
  6. data/.gitignore +1 -1
  7. data/.inch.yml +3 -1
  8. data/.reek.yml +175 -0
  9. data/.reviewer.example.yml +27 -12
  10. data/.reviewer.future.yml +221 -0
  11. data/.reviewer.yml +191 -28
  12. data/.reviewer_stdout +0 -0
  13. data/.rubocop.yml +34 -1
  14. data/CHANGELOG.md +42 -2
  15. data/Gemfile +39 -1
  16. data/Gemfile.lock +294 -72
  17. data/README.md +315 -7
  18. data/RELEASING.md +190 -0
  19. data/Rakefile +117 -0
  20. data/dependency_decisions.yml +61 -0
  21. data/exe/fmt +1 -1
  22. data/exe/rvw +1 -1
  23. data/lib/reviewer/arguments/files.rb +60 -27
  24. data/lib/reviewer/arguments/keywords.rb +39 -43
  25. data/lib/reviewer/arguments/tags.rb +21 -14
  26. data/lib/reviewer/arguments.rb +107 -29
  27. data/lib/reviewer/batch/formatter.rb +87 -0
  28. data/lib/reviewer/batch.rb +46 -35
  29. data/lib/reviewer/capabilities.rb +81 -0
  30. data/lib/reviewer/command/string/env.rb +16 -6
  31. data/lib/reviewer/command/string/flags.rb +14 -5
  32. data/lib/reviewer/command/string.rb +53 -24
  33. data/lib/reviewer/command.rb +69 -39
  34. data/lib/reviewer/configuration/loader.rb +70 -0
  35. data/lib/reviewer/configuration.rb +14 -4
  36. data/lib/reviewer/context.rb +15 -0
  37. data/lib/reviewer/doctor/config_check.rb +46 -0
  38. data/lib/reviewer/doctor/environment_check.rb +58 -0
  39. data/lib/reviewer/doctor/formatter.rb +75 -0
  40. data/lib/reviewer/doctor/keyword_check.rb +85 -0
  41. data/lib/reviewer/doctor/opportunity_check.rb +88 -0
  42. data/lib/reviewer/doctor/report.rb +63 -0
  43. data/lib/reviewer/doctor/tool_inventory.rb +41 -0
  44. data/lib/reviewer/doctor.rb +28 -0
  45. data/lib/reviewer/history.rb +36 -12
  46. data/lib/reviewer/output/formatting.rb +40 -0
  47. data/lib/reviewer/output/printer.rb +105 -0
  48. data/lib/reviewer/output.rb +54 -65
  49. data/lib/reviewer/prompt.rb +38 -0
  50. data/lib/reviewer/report/formatter.rb +124 -0
  51. data/lib/reviewer/report.rb +100 -0
  52. data/lib/reviewer/runner/failed_files.rb +66 -0
  53. data/lib/reviewer/runner/formatter.rb +103 -0
  54. data/lib/reviewer/runner/guidance.rb +79 -0
  55. data/lib/reviewer/runner/result.rb +150 -0
  56. data/lib/reviewer/runner/strategies/captured.rb +232 -0
  57. data/lib/reviewer/runner/strategies/{verbose.rb → passthrough.rb} +15 -24
  58. data/lib/reviewer/runner.rb +179 -35
  59. data/lib/reviewer/session/formatter.rb +87 -0
  60. data/lib/reviewer/session.rb +208 -0
  61. data/lib/reviewer/setup/catalog.rb +233 -0
  62. data/lib/reviewer/setup/detector.rb +61 -0
  63. data/lib/reviewer/setup/formatter.rb +94 -0
  64. data/lib/reviewer/setup/gemfile_lock.rb +55 -0
  65. data/lib/reviewer/setup/generator.rb +54 -0
  66. data/lib/reviewer/setup/tool_block.rb +112 -0
  67. data/lib/reviewer/setup.rb +41 -0
  68. data/lib/reviewer/shell/result.rb +25 -11
  69. data/lib/reviewer/shell/timer.rb +47 -27
  70. data/lib/reviewer/shell.rb +46 -21
  71. data/lib/reviewer/tool/conversions.rb +20 -0
  72. data/lib/reviewer/tool/file_resolver.rb +54 -0
  73. data/lib/reviewer/tool/settings.rb +107 -56
  74. data/lib/reviewer/tool/test_file_mapper.rb +73 -0
  75. data/lib/reviewer/tool/timing.rb +78 -0
  76. data/lib/reviewer/tool.rb +88 -47
  77. data/lib/reviewer/tools.rb +47 -33
  78. data/lib/reviewer/version.rb +1 -1
  79. data/lib/reviewer.rb +114 -54
  80. data/reviewer.gemspec +21 -20
  81. data/structure.svg +1 -0
  82. metadata +113 -148
  83. data/.ruby-version +0 -1
  84. data/lib/reviewer/command/string/verbosity.rb +0 -51
  85. data/lib/reviewer/command/verbosity.rb +0 -65
  86. data/lib/reviewer/conversions.rb +0 -27
  87. data/lib/reviewer/guidance.rb +0 -73
  88. data/lib/reviewer/keywords/git/staged.rb +0 -48
  89. data/lib/reviewer/keywords/git.rb +0 -14
  90. data/lib/reviewer/keywords.rb +0 -9
  91. data/lib/reviewer/loader.rb +0 -59
  92. data/lib/reviewer/printer.rb +0 -25
  93. data/lib/reviewer/runner/strategies/quiet.rb +0 -90
@@ -1,64 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'batch/formatter'
4
+
3
5
  module Reviewer
4
6
  # Provides a structure for running commands for a given set of tools
5
7
  class Batch
8
+ # Raised when a tool specifies an unrecognized command type
6
9
  class UnrecognizedCommandError < ArgumentError; end
7
10
 
8
- attr_reader :command_type, :tools, :output, :results
11
+ attr_reader :command_type, :tools, :report, :context, :strategy
12
+ private :context, :strategy
9
13
 
10
- def initialize(command_type, tools, output: Reviewer.output)
14
+ # Generates an instance of Batch for running multiple tools together
15
+ # @param command_type [Symbol] the type of command to run for each tool.
16
+ # @param tools [Array<Tool>] the tools to run the commands for
17
+ # @param strategy [Class] the runner strategy class (Captured or Passthrough)
18
+ # @param context [Context] the shared runtime dependencies (arguments, output, history)
19
+ #
20
+ # @return [self]
21
+ def initialize(command_type, tools, strategy:, context:)
11
22
  @command_type = command_type
12
23
  @tools = tools
13
- @output = output
14
- @results = {}
24
+ @strategy = strategy
25
+ @context = context
26
+ @report = Report.new
15
27
  end
16
28
 
29
+ # Iterates over the tools in the batch to successfully run the commands. Also times the entire
30
+ # batch in order to provide a total execution time.
31
+ #
32
+ # @return [Report] the report containing results for all commands run
17
33
  def run
18
- benchmark_batch do
19
- tools.each do |tool|
20
- runner = Runner.new(tool, command_type, strategy)
21
-
22
- # With multiple tools, run each one quietly.
23
- # Otherwise, with just one tool
24
- runner.run
25
-
26
- # Record the exit status
27
- capture_results(runner)
28
-
29
- # If the tool fails, stop running other tools
30
- break unless runner.success?
34
+ elapsed_time = Benchmark.realtime do
35
+ clear_last_statuses
36
+ matching_tools.each do |tool|
37
+ runner = run_tool(tool)
38
+ break unless runner.success? || runner.missing?
31
39
  end
32
40
  end
33
41
 
34
- results
35
- end
36
-
37
- def self.run(*args)
38
- new(*args).run
42
+ @report.record_duration(elapsed_time)
43
+ @report
39
44
  end
40
45
 
41
46
  private
42
47
 
43
- def multiple_tools?
44
- tools.size > 1
45
- end
48
+ # Runs a single tool and records its result in the report.
49
+ # @return [Runner] the runner after execution
50
+ def run_tool(tool)
51
+ runner = Runner.new(tool, command_type, strategy, context: context)
52
+ runner.run
46
53
 
47
- def strategy
48
- multiple_tools? ? Runner::Strategies::Quiet : Runner::Strategies::Verbose
54
+ result = runner.to_result
55
+ @report.add(result)
56
+ tool.record_run(result) unless runner.missing?
57
+
58
+ runner
49
59
  end
50
60
 
51
- def capture_results(runner)
52
- @results[runner.tool.key] = runner.exit_status
61
+ def clear_last_statuses
62
+ matching_tools.each do |tool|
63
+ context.history.set(tool.key, :last_status, nil)
64
+ end
53
65
  end
54
66
 
55
- # Records and prints the total runtime of a block
56
- # @param &block [type] section of code to be timed
67
+ # Returns the set of tools matching the provided command. So when formatting, if a tool does not
68
+ # have a format command, then it will be skipped.
57
69
  #
58
- # @return [void] prints the elapsed time
59
- def benchmark_batch(&block)
60
- elapsed_time = Benchmark.realtime(&block)
61
- output.info "\nTotal Time ".white + "#{elapsed_time.round(1)}s".bold
70
+ # @return [Array<Tool>] the enabled tools that support the provided command
71
+ def matching_tools
72
+ tools.select { |tool| tool.command?(command_type) }
62
73
  end
63
74
  end
64
75
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Reviewer
6
+ # Provides machine-readable output describing available tools and usage patterns.
7
+ # Designed for AI agents and automation tools to discover and use Reviewer correctly.
8
+ #
9
+ # @example
10
+ # puts Reviewer::Capabilities.new.to_json
11
+ class Capabilities
12
+ attr_reader :tools
13
+
14
+ # Creates a capabilities report for machine-readable tool discovery
15
+ # @param tools [Tools] the tools collection to report on
16
+ #
17
+ # @return [Capabilities]
18
+ def initialize(tools:)
19
+ @tools = tools
20
+ end
21
+
22
+ KEYWORDS = {
23
+ staged: 'Files staged for commit',
24
+ unstaged: 'Files with unstaged changes',
25
+ modified: 'All changed files',
26
+ untracked: 'New files not yet tracked'
27
+ }.freeze
28
+
29
+ SCENARIOS = {
30
+ before_commit: 'rvw staged',
31
+ during_development: 'rvw modified',
32
+ full_review: 'rvw'
33
+ }.freeze
34
+
35
+ # Convert capabilities to a hash representation
36
+ #
37
+ # @return [Hash] structured capabilities data
38
+ def to_h
39
+ {
40
+ version: VERSION,
41
+ tools: tools_data,
42
+ keywords: KEYWORDS,
43
+ scenarios: SCENARIOS
44
+ }
45
+ end
46
+
47
+ # Convert capabilities to formatted JSON string
48
+ #
49
+ # @return [String] JSON representation of capabilities
50
+ def to_json(*_args)
51
+ JSON.pretty_generate(to_h)
52
+ end
53
+
54
+ private
55
+
56
+ # Build tool data from configured tools
57
+ #
58
+ # @return [Array<Hash>] array of tool capability hashes
59
+ def tools_data
60
+ tools.all.map { |tool| tool_data(tool) }
61
+ end
62
+
63
+ # Build capability data for a single tool
64
+ #
65
+ # @param tool [Tool] the tool to extract data from
66
+ # @return [Hash] tool capability hash
67
+ def tool_data(tool)
68
+ {
69
+ key: tool.key.to_s,
70
+ name: tool.name,
71
+ description: tool.description,
72
+ tags: tool.tags,
73
+ skip_in_batch: tool.skip_in_batch?,
74
+ commands: {
75
+ review: tool.reviewable?,
76
+ format: tool.formattable?
77
+ }
78
+ }
79
+ end
80
+ end
81
+ end
@@ -7,28 +7,38 @@ module Reviewer
7
7
  class Env
8
8
  attr_reader :env_pairs
9
9
 
10
+ # Creates an instance of env variables for a tool to help generate the command string
11
+ # @param env_pairs [Hash] [description]
12
+ #
13
+ # @return [self]
10
14
  def initialize(env_pairs)
11
15
  @env_pairs = env_pairs
12
16
  end
13
17
 
18
+ # Converts environment variables to a space-separated string
19
+ #
20
+ # @return [String] formatted environment variables (e.g., "KEY=value KEY2=value2")
14
21
  def to_s
15
22
  to_a.compact.join(' ')
16
23
  end
17
24
 
25
+ # Converts environment variables to an array of KEY=value strings
26
+ #
27
+ # @return [Array<String, nil>] array of formatted env vars, nil for empty values
18
28
  def to_a
19
- env = []
20
- env_pairs.each { |key, value| env << env(key, value) }
21
- env
29
+ env_pairs.map { |key, value| env(key, value) }
22
30
  end
23
31
 
24
32
  private
25
33
 
26
34
  def env(key, value)
27
- return nil if key.to_s.strip.empty? || value.to_s.strip.empty?
35
+ key_str = key.to_s.strip
36
+ value_str = value.to_s.strip
37
+ return nil if key_str.empty? || value_str.empty?
28
38
 
29
- value = needs_quotes?(value) ? "'#{value}'" : value
39
+ value_str = "'#{value_str}'" if needs_quotes?(value)
30
40
 
31
- "#{key.to_s.strip.upcase}=#{value.to_s.strip}"
41
+ "#{key_str.upcase}=#{value_str}"
32
42
  end
33
43
 
34
44
  def needs_quotes?(value)
@@ -3,22 +3,31 @@
3
3
  module Reviewer
4
4
  class Command
5
5
  class String
6
- # Assembles tool flag settings into a single string or array
6
+ # Translates tool flag settings from the tool's configuration values into a single string or
7
+ # array that can be used to generate the command string
7
8
  class Flags
8
9
  attr_reader :flag_pairs
9
10
 
11
+ # Creates an instance of command-string friendly flags
12
+ # @param flag_pairs [Hash] the flags (keys) and their values
13
+ #
14
+ # @return [self]
10
15
  def initialize(flag_pairs)
11
16
  @flag_pairs = flag_pairs
12
17
  end
13
18
 
19
+ # Creates a string-friendly format to use in a command
20
+ #
21
+ # @return [String] a string of flags that can be safely passed to a command
14
22
  def to_s
15
23
  to_a.join(' ')
16
24
  end
17
25
 
26
+ # Creates an array of all flag name/value pairs
27
+ #
28
+ # @return [Array<String>] array of all flag strings to use to when running the command
18
29
  def to_a
19
- flags = []
20
- flag_pairs.each { |key, value| flags << flag(key, value) }
21
- flags
30
+ flag_pairs.map { |key, value| flag(key, value) }
22
31
  end
23
32
 
24
33
  private
@@ -26,7 +35,7 @@ module Reviewer
26
35
  def flag(key, value)
27
36
  dash = key.to_s.size == 1 ? '-' : '--'
28
37
 
29
- value = needs_quotes?(value) ? "'#{value}'" : value
38
+ value = "'#{value}'" if needs_quotes?(value)
30
39
 
31
40
  "#{dash}#{key} #{value}".strip
32
41
  end
@@ -2,22 +2,28 @@
2
2
 
3
3
  require_relative 'string/env'
4
4
  require_relative 'string/flags'
5
- require_relative 'string/verbosity'
6
5
 
7
6
  module Reviewer
8
7
  class Command
9
- # Assembles tool tool_settings into a usable command string for the command type and verbosity
8
+ # Assembles tool tool_settings into a usable command string for the command type
10
9
  class String
11
- include Conversions
10
+ attr_reader :command_type, :tool_settings, :files
12
11
 
13
- attr_reader :command_type, :tool_settings, :verbosity
14
-
15
- def initialize(command_type, tool_settings:, verbosity: nil)
12
+ # Creates a command string builder for a tool
13
+ # @param command_type [Symbol] the command type (:install, :prepare, :review, :format)
14
+ # @param tool_settings [Tool::Settings] the tool's configuration settings
15
+ # @param files [Array<String>] files to include in the command
16
+ #
17
+ # @return [String] a command string builder instance
18
+ def initialize(command_type, tool_settings:, files: [])
16
19
  @command_type = command_type
17
20
  @tool_settings = tool_settings
18
- @verbosity = Verbosity(verbosity)
21
+ @files = Array(files)
19
22
  end
20
23
 
24
+ # Converts the command to a complete string ready for execution
25
+ #
26
+ # @return [String] the full command string
21
27
  def to_s
22
28
  to_a
23
29
  .map(&:strip) # Remove extra spaces on the components
@@ -25,48 +31,71 @@ module Reviewer
25
31
  .strip # Strip extra spaces from the end result
26
32
  end
27
33
 
34
+ # Converts the command to an array of its components
35
+ #
36
+ # @return [Array<String, nil>] env vars, body, flags, and files
28
37
  def to_a
29
38
  [
30
39
  env_variables,
31
40
  body,
32
41
  flags,
33
- verbosity_options
42
+ files_string
34
43
  ].compact
35
44
  end
36
45
 
37
- def env_variables
38
- Env.new(tool_settings.env).to_s
39
- end
46
+ # The string of environment variables built from a tool's configuration settings
47
+ #
48
+ # @return [String] the environment variable names and values concatened for the command
49
+ def env_variables = Env.new(tool_settings.env).to_s
40
50
 
51
+ # The base command string from the tool's configuration.
52
+ # Uses the file-scoped command when files are present and one is configured.
53
+ #
54
+ # @return [String] the configured command for the command type
41
55
  def body
42
- tool_settings.commands.fetch(command_type)
56
+ file_scoped_command || tool_settings.commands.fetch(command_type)
43
57
  end
44
58
 
59
+ # Gets the flags to be used in conjunction with the review command for a tool
60
+ # 1. The `review` commands are the only commands that use flags
61
+ # 2. If no flags are configured, this won't do anything
62
+ #
63
+ # @return [String] the concatenated list of flags to pass to the review command
45
64
  def flags
46
- # Flags to be used for `review` commands.
47
- # 1. The `review` commands are the only commands that use flags
48
- # 2. If no flags are configured, this won't do much
49
- #
50
- # Note: Since verbosity is handled separately, flags for 'quiet' are handled separately at a
51
- # lower level by design and excluded from this check. They are not included with the other
52
- # configured flags.
53
65
  return nil unless flags?
54
66
 
55
67
  Flags.new(tool_settings.flags).to_s
56
68
  end
57
69
 
58
- def verbosity_options
59
- Verbosity.new(tool_settings.quiet_option, level: verbosity.level).to_s
70
+ # Builds the files portion of the command string
71
+ #
72
+ # @return [String, nil] the formatted files string or nil if not applicable
73
+ def files_string
74
+ return nil unless files_applicable?
75
+
76
+ file_list = files.join(tool_settings.files_separator)
77
+ flag = tool_settings.files_flag
78
+
79
+ flag.empty? ? file_list : "#{flag} #{file_list}"
60
80
  end
61
81
 
62
82
  private
63
83
 
84
+ def file_scoped_command
85
+ return nil unless files.any?
86
+
87
+ tool_settings.files_command(command_type)
88
+ end
89
+
64
90
  # Determines whether the string needs flags added
65
91
  #
66
92
  # @return [Boolean] true if it's a review command and it has flags configured
67
- def flags?
68
- command_type == :review && tool_settings.flags.any?
69
- end
93
+ def flags? = command_type == :review && tool_settings.flags.any?
94
+
95
+ # Determines whether files should be appended to the command
96
+ #
97
+ # @return [Boolean] true if tool supports files and files were provided
98
+ def files_applicable? = tool_settings.supports_files? && files.any?
70
99
  end
71
100
  end
72
101
  end
@@ -1,28 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'command/string'
4
- require_relative 'command/verbosity'
5
4
 
6
5
  module Reviewer
7
6
  # The core funtionality to translate a tool, command type, and verbosity into a runnable command
8
7
  class Command
9
- include Conversions
8
+ include Tool::Conversions
10
9
 
11
10
  SEED_SUBSTITUTION_VALUE = '$SEED'
12
11
 
13
- attr_reader :tool, :type
12
+ attr_reader :tool, :arguments, :history
13
+ private :arguments, :history
14
+
15
+ # @!attribute type
16
+ # @return [Symbol] the command type (:install, :prepare, :review, :format)
17
+ attr_accessor :type
14
18
 
15
19
  # Creates an instance of the Command class to synthesize a command string using the tool,
16
20
  # command type, and verbosity.
17
21
  # @param tool [Tool, Symbol] a tool or tool key to use to look up the command and options
18
22
  # @param type [Symbol] the desired command type (:install, :prepare, :review, :format)
19
- # @param verbosity = Verbosity::TOTAL_SILENCE [Symbol] the desired verbosity for the command
23
+ # @param context [Context] the shared runtime dependencies (arguments, output, history)
20
24
  #
21
25
  # @return [Command] the intersection of a tool, command type, and verbosity
22
- def initialize(tool, type, verbosity = Verbosity::TOTAL_SILENCE)
26
+ def initialize(tool, type, context:)
23
27
  @tool = Tool(tool)
24
28
  @type = type.to_sym
25
- @verbosity = Verbosity(verbosity)
29
+ @seed = nil
30
+ @arguments = context.arguments
31
+ @history = context.history
26
32
  end
27
33
 
28
34
  # The final command string with all of the conditions appled
@@ -33,26 +39,6 @@ module Reviewer
33
39
  end
34
40
  alias to_s string
35
41
 
36
- # Getter for @verbosity. Since the setter is custom, the getter needs to be explicitly declared.
37
- # Otherwise, using `attr_accessor` and then overriding the setter muddies the waters.
38
- #
39
- # @return [Verbosity] the current verbosity setting for the command
40
- def verbosity # rubocop:disable Style/TrivialAccessors
41
- @verbosity
42
- end
43
-
44
- # Override verbosity assignment to clear the related memoized values when verbosity changes
45
- # @param verbosity [Verbosity, Symbol] the desired verbosity for the command
46
- #
47
- # @return [Verbosity] the updated verbosity level for the command
48
- def verbosity=(verbosity)
49
- # Unmemoize string since the verbosity has been changed
50
- @raw_string = nil
51
- @string = nil
52
-
53
- @verbosity = Verbosity(verbosity)
54
- end
55
-
56
42
  # Generates a seed that can be re-used across runs so that the results are consistent across
57
43
  # related runs for tools that would otherwise change the seed automatically every run.
58
44
  # Since not all tools will use the seed, there's no need to generate it in the initializer.
@@ -60,26 +46,72 @@ module Reviewer
60
46
  #
61
47
  # @return [Integer] a random integer to pass to tools that use seeds
62
48
  def seed
63
- @seed ||= Random.rand(100_000)
49
+ @seed ||= if arguments.keywords.failed?
50
+ history.get(tool.key, :last_seed) || Random.rand(100_000)
51
+ else
52
+ Random.rand(100_000)
53
+ end
64
54
 
65
55
  # Store the seed for reference
66
- Reviewer.history.set(tool.key, :last_seed, @seed)
56
+ history.set(tool.key, :last_seed, @seed)
67
57
 
68
58
  @seed
69
59
  end
70
60
 
71
- private
72
-
73
61
  # The raw command string before any substitutions. For example, since seeds need to remain
74
62
  # consistent from one run to the next, they're
75
63
  #
76
- # @return [type] [description]
64
+ # @return [String] the command string before seed substitution
77
65
  def raw_string
78
- @raw_string ||= String.new(
79
- type,
80
- tool_settings: tool.settings,
81
- verbosity: verbosity
82
- ).to_s
66
+ @raw_string ||= String.new(type, tool_settings: tool.settings, files: target_files).to_s # rubocop:disable Lint/RedundantTypeConversion
67
+ end
68
+
69
+ # Gets the list of files to target for this command, resolved by the tool
70
+ #
71
+ # @return [Array<String>] the list of files from arguments, resolved by tool
72
+ def target_files
73
+ @target_files ||= tool.resolve_files(requested_files)
74
+ end
75
+
76
+ # Determines if this command should be skipped because files were requested but none match
77
+ #
78
+ # @return [Boolean] true if files were requested but resolution left none for this tool
79
+ def skip?
80
+ tool.skip_files?(requested_files)
81
+ end
82
+
83
+ # Returns a summary hash for run display, or nil if this command should be skipped
84
+ #
85
+ # @return [Hash, nil] { name:, files: } or nil if skipped
86
+ def run_summary
87
+ return nil if skip?
88
+
89
+ { name: tool.name, files: target_files }
90
+ end
91
+
92
+ private
93
+
94
+ # The raw list of files from arguments before resolution.
95
+ # Falls through to stored failed files when the `failed` keyword is present
96
+ # and no explicit files were provided.
97
+ #
98
+ # @return [Array<String>] files from -f flag, keywords like 'staged', or stored failed files
99
+ def requested_files
100
+ @requested_files ||= begin
101
+ explicit = arguments.files.to_a
102
+ if explicit.empty? && arguments.keywords.failed?
103
+ stored_failed_files
104
+ else
105
+ explicit
106
+ end
107
+ end
108
+ end
109
+
110
+ # Retrieves failed files stored from the previous run for this tool
111
+ #
112
+ # @return [Array<String>] stored failed file paths, or empty array
113
+ def stored_failed_files
114
+ history.get(tool.key, :last_failed_files) || []
83
115
  end
84
116
 
85
117
  # The version of the command with the SEED_SUBSTITUTION_VALUE replaced
@@ -93,8 +125,6 @@ module Reviewer
93
125
  # Determines if the raw command string has a SEED_SUBSTITUTION_VALUE that needs replacing
94
126
  #
95
127
  # @return [Boolean] true if the raw command string contains the SEED_SUBSTITUTION_VALUE
96
- def seed_substitution?
97
- raw_string.include?(SEED_SUBSTITUTION_VALUE)
98
- end
128
+ def seed_substitution? = raw_string.include?(SEED_SUBSTITUTION_VALUE)
99
129
  end
100
130
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Reviewer
6
+ class Configuration
7
+ # Provides a collection of the configured tools
8
+ class Loader
9
+ # Raised when the .reviewer.yml configuration file cannot be found
10
+ class MissingConfigurationError < StandardError; end
11
+
12
+ # Raised when the .reviewer.yml file contains invalid YAML syntax
13
+ class InvalidConfigurationError < StandardError; end
14
+
15
+ # Raised when a configured tool is missing a required review command
16
+ class MissingReviewCommandError < StandardError; end
17
+
18
+ attr_reader :configuration, :file
19
+
20
+ # Creates a loader instance for the configuration file
21
+ # @param file [Pathname] the path to the configuration YAML file
22
+ #
23
+ # @return [Loader] a loader with parsed configuration
24
+ # @raise [MissingConfigurationError] if the file doesn't exist
25
+ # @raise [InvalidConfigurationError] if the YAML is malformed
26
+ # @raise [MissingReviewCommandError] if a tool lacks a review command
27
+ def initialize(file:)
28
+ @file = file
29
+ @configuration = configuration_hash
30
+
31
+ validate_configuration
32
+ end
33
+
34
+ # Whether all configured tools have a review command
35
+ #
36
+ # @return [Boolean] true if every tool has a review command configured
37
+ def review_commands_present?
38
+ configuration.all? { |_key, value| value[:commands]&.key?(:review) }
39
+ end
40
+
41
+ # Converts the loader to its configuration hash
42
+ #
43
+ # @return [Hash] the parsed configuration
44
+ def to_h = configuration
45
+
46
+ # Loads and returns the tools configuration hash
47
+ #
48
+ # @return [Hash] the parsed configuration from the YAML file
49
+ def self.configuration(file:) = new(file: file).configuration
50
+
51
+ private
52
+
53
+ def require_review_commands
54
+ return if review_commands_present?
55
+
56
+ missing = configuration.find { |_key, value| !value[:commands]&.key?(:review) }
57
+ raise MissingReviewCommandError, "'#{missing[0]}' does not have a 'review' key under 'commands' in `#{file}`"
58
+ end
59
+ alias validate_configuration require_review_commands
60
+
61
+ def configuration_hash
62
+ @configuration_hash ||= Psych.safe_load_file(@file, symbolize_names: true)
63
+ rescue Errno::ENOENT
64
+ raise MissingConfigurationError, "Tools configuration file couldn't be found at `#{file}`"
65
+ rescue Psych::SyntaxError => e
66
+ raise InvalidConfigurationError, "Tools configuration file (#{file}) has a syntax error: #{e.message}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -4,21 +4,31 @@ require 'pathname'
4
4
 
5
5
  module Reviewer
6
6
  # Configuration values container for Reviewer
7
+ #
8
+ # @!attribute file
9
+ # @return [Pathname] the pathname for the primary configuraton file
10
+ # @!attribute history_file
11
+ # @return [Pathname] the pathname for the history file to store data across runs
12
+ # @!attribute printer
13
+ # @return [Output::Printer] the printer instance for console output
14
+ #
15
+ # @author [garrettdimon]
16
+ #
7
17
  class Configuration
8
18
  DEFAULT_PATH = Dir.pwd.freeze
9
19
 
10
20
  DEFAULT_CONFIG_FILE_NAME = '.reviewer.yml'
11
21
  DEFAULT_HISTORY_FILE_NAME = '.reviewer_history.yml'
12
22
 
13
- DEFAULT_CONFIG_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_CONFIG_FILE_NAME}"
14
- DEFAULT_HISTORY_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_HISTORY_FILE_NAME}"
23
+ DEFAULT_CONFIG_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_CONFIG_FILE_NAME}".freeze
24
+ DEFAULT_HISTORY_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_HISTORY_FILE_NAME}".freeze
15
25
 
16
- attr_accessor :file, :history_file, :printer
26
+ attr_accessor :file, :history_file
27
+ attr_reader :printer
17
28
 
18
29
  def initialize
19
30
  @file = Pathname(DEFAULT_CONFIG_LOCATION)
20
31
  @history_file = Pathname(DEFAULT_HISTORY_LOCATION)
21
- @printer = ::Reviewer::Printer.new
22
32
 
23
33
  # Future Configuration Options:
24
34
  # - seed_substitution_value(string): Currently a constant of `$SEED` in Reviewer::Command, but