reviewer 0.1.5 → 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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +79 -11
- data/.github/workflows/release.yml +98 -0
- data/.gitignore +1 -1
- data/.inch.yml +3 -1
- data/.reek.yml +175 -0
- data/.reviewer.example.yml +7 -2
- data/.reviewer.yml +166 -40
- data/.rubocop.yml +34 -2
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +291 -70
- data/LICENSE.txt +20 -4
- data/README.md +310 -21
- data/RELEASING.md +190 -0
- data/Rakefile +117 -0
- data/dependency_decisions.yml +61 -0
- data/exe/fmt +1 -1
- data/exe/rvw +1 -1
- data/lib/reviewer/arguments/files.rb +47 -20
- data/lib/reviewer/arguments/keywords.rb +34 -41
- data/lib/reviewer/arguments/tags.rb +11 -11
- data/lib/reviewer/arguments.rb +100 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +32 -48
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +12 -6
- data/lib/reviewer/command/string/flags.rb +2 -4
- data/lib/reviewer/command/string.rb +47 -12
- data/lib/reviewer/command.rb +65 -10
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +6 -3
- data/lib/reviewer/context.rb +15 -0
- data/lib/reviewer/doctor/config_check.rb +46 -0
- data/lib/reviewer/doctor/environment_check.rb +58 -0
- data/lib/reviewer/doctor/formatter.rb +75 -0
- data/lib/reviewer/doctor/keyword_check.rb +85 -0
- data/lib/reviewer/doctor/opportunity_check.rb +88 -0
- data/lib/reviewer/doctor/report.rb +63 -0
- data/lib/reviewer/doctor/tool_inventory.rb +41 -0
- data/lib/reviewer/doctor.rb +28 -0
- data/lib/reviewer/history.rb +10 -17
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +70 -9
- data/lib/reviewer/output.rb +37 -78
- data/lib/reviewer/prompt.rb +38 -0
- data/lib/reviewer/report/formatter.rb +124 -0
- data/lib/reviewer/report.rb +100 -0
- data/lib/reviewer/runner/failed_files.rb +66 -0
- data/lib/reviewer/runner/formatter.rb +103 -0
- data/lib/reviewer/runner/guidance.rb +79 -0
- data/lib/reviewer/runner/result.rb +150 -0
- data/lib/reviewer/runner/strategies/captured.rb +98 -23
- data/lib/reviewer/runner/strategies/passthrough.rb +2 -11
- data/lib/reviewer/runner.rb +126 -40
- data/lib/reviewer/session/formatter.rb +87 -0
- data/lib/reviewer/session.rb +208 -0
- data/lib/reviewer/setup/catalog.rb +233 -0
- data/lib/reviewer/setup/detector.rb +61 -0
- data/lib/reviewer/setup/formatter.rb +94 -0
- data/lib/reviewer/setup/gemfile_lock.rb +55 -0
- data/lib/reviewer/setup/generator.rb +54 -0
- data/lib/reviewer/setup/tool_block.rb +112 -0
- data/lib/reviewer/setup.rb +41 -0
- data/lib/reviewer/shell/result.rb +14 -15
- data/lib/reviewer/shell/timer.rb +40 -35
- data/lib/reviewer/shell.rb +41 -12
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +88 -44
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -69
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +109 -50
- data/reviewer.gemspec +16 -19
- metadata +101 -142
- data/lib/reviewer/conversions.rb +0 -16
- data/lib/reviewer/guidance.rb +0 -77
- data/lib/reviewer/keywords/git/staged.rb +0 -64
- data/lib/reviewer/keywords/git.rb +0 -14
- data/lib/reviewer/keywords.rb +0 -9
- data/lib/reviewer/loader.rb +0 -59
- data/lib/reviewer/output/scrubber.rb +0 -48
- data/lib/reviewer/output/token.rb +0 -85
data/lib/reviewer/batch.rb
CHANGED
|
@@ -1,83 +1,67 @@
|
|
|
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, :
|
|
11
|
+
attr_reader :command_type, :tools, :report, :context, :strategy
|
|
12
|
+
private :context, :strategy
|
|
9
13
|
|
|
10
14
|
# Generates an instance of Batch for running multiple tools together
|
|
11
15
|
# @param command_type [Symbol] the type of command to run for each tool.
|
|
12
16
|
# @param tools [Array<Tool>] the tools to run the commands for
|
|
13
|
-
# @param
|
|
17
|
+
# @param strategy [Class] the runner strategy class (Captured or Passthrough)
|
|
18
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
14
19
|
#
|
|
15
20
|
# @return [self]
|
|
16
|
-
def initialize(command_type, tools,
|
|
21
|
+
def initialize(command_type, tools, strategy:, context:)
|
|
17
22
|
@command_type = command_type
|
|
18
23
|
@tools = tools
|
|
19
|
-
@
|
|
20
|
-
@
|
|
24
|
+
@strategy = strategy
|
|
25
|
+
@context = context
|
|
26
|
+
@report = Report.new
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
# Iterates over the tools in the batch to successfully run the commands. Also times the entire
|
|
24
30
|
# batch in order to provide a total execution time.
|
|
25
31
|
#
|
|
26
|
-
# @return [
|
|
32
|
+
# @return [Report] the report containing results for all commands run
|
|
27
33
|
def run
|
|
28
|
-
|
|
34
|
+
elapsed_time = Benchmark.realtime do
|
|
35
|
+
clear_last_statuses
|
|
29
36
|
matching_tools.each do |tool|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
runner.run
|
|
33
|
-
|
|
34
|
-
# Record the exit status for this tool
|
|
35
|
-
record_exit_status(runner)
|
|
36
|
-
|
|
37
|
-
# If the tool fails, stop running other tools
|
|
38
|
-
break unless runner.success?
|
|
37
|
+
runner = run_tool(tool)
|
|
38
|
+
break unless runner.success? || runner.missing?
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
@report.record_duration(elapsed_time)
|
|
43
|
+
@report
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
private
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
result = runner.to_result
|
|
55
|
+
@report.add(result)
|
|
56
|
+
tool.record_run(result) unless runner.missing?
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
# not based on the configured `max_exit_status` for the tool. For example, some tools use exit
|
|
57
|
-
# status to convey significance. So even though it returns a non-zero exit status like 2, it
|
|
58
|
-
# can still be successful.
|
|
59
|
-
# @param runner [Runner] the instance of the runner that's being inspected
|
|
60
|
-
#
|
|
61
|
-
# @return [Integer] the adjusted exit status for the runner
|
|
62
|
-
def record_exit_status(runner)
|
|
63
|
-
# Since some tools can "succeed" with a positive exit status, the overall batch is only
|
|
64
|
-
# interested in subjective failure. So if the runner succeeded according to the tool's max
|
|
65
|
-
# exit status, it should record the tool's run as a success for the purposes of the larger
|
|
66
|
-
# batch success/failure
|
|
67
|
-
@results[runner.tool.key] = runner.success? ? 0 : runner.exit_status
|
|
58
|
+
runner
|
|
68
59
|
end
|
|
69
60
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def benchmark_batch(&block)
|
|
75
|
-
elapsed_time = Benchmark.realtime(&block)
|
|
76
|
-
|
|
77
|
-
# If there's failures, skip showing the total time to focus on the issues
|
|
78
|
-
return if @results.values.sum.positive?
|
|
79
|
-
|
|
80
|
-
output.batch_summary(results.size, elapsed_time)
|
|
61
|
+
def clear_last_statuses
|
|
62
|
+
matching_tools.each do |tool|
|
|
63
|
+
context.history.set(tool.key, :last_status, nil)
|
|
64
|
+
end
|
|
81
65
|
end
|
|
82
66
|
|
|
83
67
|
# Returns the set of tools matching the provided command. So when formatting, if a tool does not
|
|
@@ -85,7 +69,7 @@ module Reviewer
|
|
|
85
69
|
#
|
|
86
70
|
# @return [Array<Tool>] the enabled tools that support the provided command
|
|
87
71
|
def matching_tools
|
|
88
|
-
tools.select { |tool| tool.
|
|
72
|
+
tools.select { |tool| tool.command?(command_type) }
|
|
89
73
|
end
|
|
90
74
|
end
|
|
91
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
|
|
@@ -15,24 +15,30 @@ module Reviewer
|
|
|
15
15
|
@env_pairs = env_pairs
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Converts environment variables to a space-separated string
|
|
19
|
+
#
|
|
20
|
+
# @return [String] formatted environment variables (e.g., "KEY=value KEY2=value2")
|
|
18
21
|
def to_s
|
|
19
22
|
to_a.compact.join(' ')
|
|
20
23
|
end
|
|
21
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
|
|
22
28
|
def to_a
|
|
23
|
-
env
|
|
24
|
-
env_pairs.each { |key, value| env << env(key, value) }
|
|
25
|
-
env
|
|
29
|
+
env_pairs.map { |key, value| env(key, value) }
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
private
|
|
29
33
|
|
|
30
34
|
def env(key, value)
|
|
31
|
-
|
|
35
|
+
key_str = key.to_s.strip
|
|
36
|
+
value_str = value.to_s.strip
|
|
37
|
+
return nil if key_str.empty? || value_str.empty?
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
value_str = "'#{value_str}'" if needs_quotes?(value)
|
|
34
40
|
|
|
35
|
-
"#{
|
|
41
|
+
"#{key_str.upcase}=#{value_str}"
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def needs_quotes?(value)
|
|
@@ -27,9 +27,7 @@ module Reviewer
|
|
|
27
27
|
#
|
|
28
28
|
# @return [Array<String>] array of all flag strings to use to when running the command
|
|
29
29
|
def to_a
|
|
30
|
-
|
|
31
|
-
flag_pairs.each { |key, value| flags << flag(key, value) }
|
|
32
|
-
flags
|
|
30
|
+
flag_pairs.map { |key, value| flag(key, value) }
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
private
|
|
@@ -37,7 +35,7 @@ module Reviewer
|
|
|
37
35
|
def flag(key, value)
|
|
38
36
|
dash = key.to_s.size == 1 ? '-' : '--'
|
|
39
37
|
|
|
40
|
-
value =
|
|
38
|
+
value = "'#{value}'" if needs_quotes?(value)
|
|
41
39
|
|
|
42
40
|
"#{dash}#{key} #{value}".strip
|
|
43
41
|
end
|
|
@@ -7,15 +7,23 @@ module Reviewer
|
|
|
7
7
|
class Command
|
|
8
8
|
# Assembles tool tool_settings into a usable command string for the command type
|
|
9
9
|
class String
|
|
10
|
-
|
|
10
|
+
attr_reader :command_type, :tool_settings, :files
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: [])
|
|
15
19
|
@command_type = command_type
|
|
16
20
|
@tool_settings = tool_settings
|
|
21
|
+
@files = Array(files)
|
|
17
22
|
end
|
|
18
23
|
|
|
24
|
+
# Converts the command to a complete string ready for execution
|
|
25
|
+
#
|
|
26
|
+
# @return [String] the full command string
|
|
19
27
|
def to_s
|
|
20
28
|
to_a
|
|
21
29
|
.map(&:strip) # Remove extra spaces on the components
|
|
@@ -23,23 +31,29 @@ module Reviewer
|
|
|
23
31
|
.strip # Strip extra spaces from the end result
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
# Converts the command to an array of its components
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<String, nil>] env vars, body, flags, and files
|
|
26
37
|
def to_a
|
|
27
38
|
[
|
|
28
39
|
env_variables,
|
|
29
40
|
body,
|
|
30
|
-
flags
|
|
41
|
+
flags,
|
|
42
|
+
files_string
|
|
31
43
|
].compact
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
# The string of environment variables built from a tool's configuration settings
|
|
35
47
|
#
|
|
36
48
|
# @return [String] the environment variable names and values concatened for the command
|
|
37
|
-
def env_variables
|
|
38
|
-
Env.new(tool_settings.env).to_s
|
|
39
|
-
end
|
|
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
|
|
|
45
59
|
# Gets the flags to be used in conjunction with the review command for a tool
|
|
@@ -53,14 +67,35 @@ module Reviewer
|
|
|
53
67
|
Flags.new(tool_settings.flags).to_s
|
|
54
68
|
end
|
|
55
69
|
|
|
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}"
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
private
|
|
57
83
|
|
|
84
|
+
def file_scoped_command
|
|
85
|
+
return nil unless files.any?
|
|
86
|
+
|
|
87
|
+
tool_settings.files_command(command_type)
|
|
88
|
+
end
|
|
89
|
+
|
|
58
90
|
# Determines whether the string needs flags added
|
|
59
91
|
#
|
|
60
92
|
# @return [Boolean] true if it's a review command and it has flags configured
|
|
61
|
-
def flags?
|
|
62
|
-
|
|
63
|
-
|
|
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?
|
|
64
99
|
end
|
|
65
100
|
end
|
|
66
101
|
end
|
data/lib/reviewer/command.rb
CHANGED
|
@@ -5,23 +5,30 @@ require_relative 'command/string'
|
|
|
5
5
|
module Reviewer
|
|
6
6
|
# The core funtionality to translate a tool, command type, and verbosity into a runnable command
|
|
7
7
|
class Command
|
|
8
|
-
include Conversions
|
|
8
|
+
include Tool::Conversions
|
|
9
9
|
|
|
10
10
|
SEED_SUBSTITUTION_VALUE = '$SEED'
|
|
11
11
|
|
|
12
|
-
attr_reader :tool
|
|
12
|
+
attr_reader :tool, :arguments, :history
|
|
13
|
+
private :arguments, :history
|
|
13
14
|
|
|
15
|
+
# @!attribute type
|
|
16
|
+
# @return [Symbol] the command type (:install, :prepare, :review, :format)
|
|
14
17
|
attr_accessor :type
|
|
15
18
|
|
|
16
19
|
# Creates an instance of the Command class to synthesize a command string using the tool,
|
|
17
20
|
# command type, and verbosity.
|
|
18
21
|
# @param tool [Tool, Symbol] a tool or tool key to use to look up the command and options
|
|
19
22
|
# @param type [Symbol] the desired command type (:install, :prepare, :review, :format)
|
|
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)
|
|
26
|
+
def initialize(tool, type, context:)
|
|
23
27
|
@tool = Tool(tool)
|
|
24
28
|
@type = type.to_sym
|
|
29
|
+
@seed = nil
|
|
30
|
+
@arguments = context.arguments
|
|
31
|
+
@history = context.history
|
|
25
32
|
end
|
|
26
33
|
|
|
27
34
|
# The final command string with all of the conditions appled
|
|
@@ -39,10 +46,14 @@ module Reviewer
|
|
|
39
46
|
#
|
|
40
47
|
# @return [Integer] a random integer to pass to tools that use seeds
|
|
41
48
|
def seed
|
|
42
|
-
@seed ||=
|
|
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
|
|
43
54
|
|
|
44
55
|
# Store the seed for reference
|
|
45
|
-
|
|
56
|
+
history.set(tool.key, :last_seed, @seed)
|
|
46
57
|
|
|
47
58
|
@seed
|
|
48
59
|
end
|
|
@@ -50,13 +61,59 @@ module Reviewer
|
|
|
50
61
|
# The raw command string before any substitutions. For example, since seeds need to remain
|
|
51
62
|
# consistent from one run to the next, they're
|
|
52
63
|
#
|
|
53
|
-
# @return [
|
|
64
|
+
# @return [String] the command string before seed substitution
|
|
54
65
|
def raw_string
|
|
55
|
-
@raw_string ||= String.new(type, tool_settings: tool.settings).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 }
|
|
56
90
|
end
|
|
57
91
|
|
|
58
92
|
private
|
|
59
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) || []
|
|
115
|
+
end
|
|
116
|
+
|
|
60
117
|
# The version of the command with the SEED_SUBSTITUTION_VALUE replaced
|
|
61
118
|
#
|
|
62
119
|
# @return [String] the command string with the SEED_SUBSTITUTION_VALUE replaced
|
|
@@ -68,8 +125,6 @@ module Reviewer
|
|
|
68
125
|
# Determines if the raw command string has a SEED_SUBSTITUTION_VALUE that needs replacing
|
|
69
126
|
#
|
|
70
127
|
# @return [Boolean] true if the raw command string contains the SEED_SUBSTITUTION_VALUE
|
|
71
|
-
def seed_substitution?
|
|
72
|
-
raw_string.include?(SEED_SUBSTITUTION_VALUE)
|
|
73
|
-
end
|
|
128
|
+
def seed_substitution? = raw_string.include?(SEED_SUBSTITUTION_VALUE)
|
|
74
129
|
end
|
|
75
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
|
|
@@ -9,6 +9,8 @@ module Reviewer
|
|
|
9
9
|
# @return [Pathname] the pathname for the primary configuraton file
|
|
10
10
|
# @!attribute history_file
|
|
11
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
|
|
12
14
|
#
|
|
13
15
|
# @author [garrettdimon]
|
|
14
16
|
#
|
|
@@ -18,10 +20,11 @@ module Reviewer
|
|
|
18
20
|
DEFAULT_CONFIG_FILE_NAME = '.reviewer.yml'
|
|
19
21
|
DEFAULT_HISTORY_FILE_NAME = '.reviewer_history.yml'
|
|
20
22
|
|
|
21
|
-
DEFAULT_CONFIG_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_CONFIG_FILE_NAME}"
|
|
22
|
-
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
|
|
23
25
|
|
|
24
|
-
attr_accessor :file, :history_file
|
|
26
|
+
attr_accessor :file, :history_file
|
|
27
|
+
attr_reader :printer
|
|
25
28
|
|
|
26
29
|
def initialize
|
|
27
30
|
@file = Pathname(DEFAULT_CONFIG_LOCATION)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
# Bundles the shared runtime dependencies that flow through the review/format lifecycle.
|
|
5
|
+
# Passed from Session → Batch → Runner → Command so that no class needs to reach
|
|
6
|
+
# into module-level globals for arguments, output, or history.
|
|
7
|
+
#
|
|
8
|
+
# @!attribute [rw] arguments
|
|
9
|
+
# @return [Arguments] the parsed command-line arguments
|
|
10
|
+
# @!attribute [rw] output
|
|
11
|
+
# @return [Output] the output channel for displaying content
|
|
12
|
+
# @!attribute [rw] history
|
|
13
|
+
# @return [History] the YAML store for timing data and prepare timestamps
|
|
14
|
+
Context = Struct.new(:arguments, :output, :history, keyword_init: true)
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Validates the configuration file by delegating to Configuration::Loader
|
|
6
|
+
class ConfigCheck
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
# Creates a config check that validates the .reviewer.yml file
|
|
10
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
11
|
+
# @param configuration [Configuration] the configuration to validate
|
|
12
|
+
#
|
|
13
|
+
# @return [ConfigCheck]
|
|
14
|
+
def initialize(report, configuration:)
|
|
15
|
+
@report = report
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks for .reviewer.yml existence and validity
|
|
20
|
+
def check
|
|
21
|
+
config_file = @configuration.file
|
|
22
|
+
|
|
23
|
+
unless config_file.exist?
|
|
24
|
+
report.add(:configuration, status: :error,
|
|
25
|
+
message: 'No .reviewer.yml found', detail: 'Run `rvw init` to generate one')
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
report.add(:configuration, status: :ok, message: '.reviewer.yml found')
|
|
30
|
+
validate_via_loader
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Exercises the full Configuration::Loader pipeline (parse + validate) to surface config errors
|
|
36
|
+
def validate_via_loader
|
|
37
|
+
Configuration::Loader.configuration(file: @configuration.file)
|
|
38
|
+
report.add(:configuration, status: :ok, message: 'Configuration is valid')
|
|
39
|
+
rescue Configuration::Loader::InvalidConfigurationError => e
|
|
40
|
+
report.add(:configuration, status: :error, message: 'YAML syntax error', detail: e.message)
|
|
41
|
+
rescue Configuration::Loader::MissingReviewCommandError => e
|
|
42
|
+
report.add(:configuration, status: :error, message: 'Missing review command', detail: e.message)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|