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/output.rb
CHANGED
|
@@ -2,121 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
require 'io/console/size' # For determining console width/height
|
|
4
4
|
|
|
5
|
+
require_relative 'output/formatting'
|
|
5
6
|
require_relative 'output/printer'
|
|
6
|
-
require_relative 'output/scrubber'
|
|
7
|
-
require_relative 'output/token'
|
|
8
7
|
|
|
9
8
|
module Reviewer
|
|
10
|
-
#
|
|
9
|
+
# Console display infrastructure — primitives for styled terminal output.
|
|
10
|
+
# Domain-specific display logic lives in each concept's Formatter class.
|
|
11
11
|
class Output
|
|
12
12
|
DEFAULT_CONSOLE_WIDTH = 120
|
|
13
|
-
DIVIDER = '
|
|
13
|
+
DIVIDER = '─'
|
|
14
|
+
RAKE_ABORTED_TEXT = "rake aborted!\n"
|
|
15
|
+
|
|
16
|
+
# Removes unhelpful rake exit status noise from stderr
|
|
17
|
+
# @param text [String, nil] the stderr output to clean up
|
|
18
|
+
# @return [String] the cleaned text with rake noise removed
|
|
19
|
+
def self.scrub(text)
|
|
20
|
+
text = text.to_s
|
|
21
|
+
return '' if text.empty?
|
|
22
|
+
|
|
23
|
+
text.include?(RAKE_ABORTED_TEXT) ? text.split(RAKE_ABORTED_TEXT).first : text
|
|
24
|
+
end
|
|
14
25
|
|
|
15
26
|
attr_reader :printer
|
|
16
27
|
|
|
17
28
|
# Creates an instance of Output to print Reviewer activity and results to the console
|
|
29
|
+
# @param printer [Printer] the low-level printer for styled terminal output
|
|
30
|
+
#
|
|
31
|
+
# @return [Output]
|
|
18
32
|
def initialize(printer = Printer.new)
|
|
19
33
|
@printer = printer
|
|
20
34
|
end
|
|
21
35
|
|
|
36
|
+
# === Primitives ===
|
|
37
|
+
|
|
38
|
+
# Clears the terminal screen (no-op when output is not a TTY)
|
|
39
|
+
# @return [void]
|
|
22
40
|
def clear
|
|
23
|
-
system('clear')
|
|
41
|
+
system('clear') if printer.tty?
|
|
24
42
|
end
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
# Prints a blank line
|
|
45
|
+
# @return [void]
|
|
46
|
+
def newline = printer.puts(:default, '')
|
|
29
47
|
|
|
48
|
+
# Prints a horizontal rule spanning the console width
|
|
49
|
+
# @return [void]
|
|
30
50
|
def divider
|
|
31
51
|
newline
|
|
32
52
|
printer.print(:muted, DIVIDER * console_width)
|
|
33
53
|
end
|
|
34
54
|
|
|
35
|
-
# Prints
|
|
36
|
-
# @param message [String] the text to
|
|
37
|
-
#
|
|
55
|
+
# Prints an unformatted help message
|
|
56
|
+
# @param message [String] the help text to display
|
|
38
57
|
# @return [void]
|
|
39
58
|
def help(message)
|
|
40
59
|
printer.puts(:default, message)
|
|
41
60
|
end
|
|
42
61
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# @param tool_count [Integer] the number of commands run in the batch
|
|
46
|
-
# @param seconds [Float] the total number of seconds the batch ran in realtime
|
|
47
|
-
#
|
|
62
|
+
# Writes raw output directly without formatting
|
|
63
|
+
# @param value [String] the raw output to write
|
|
48
64
|
# @return [void]
|
|
49
|
-
def batch_summary(tool_count, seconds)
|
|
50
|
-
printer.print(:bold, "~#{seconds.round(1)} seconds")
|
|
51
|
-
printer.puts(:muted, " for #{tool_count} tools") if tool_count > 1
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Print a tool summary using the name and description. Used before running a command to help
|
|
55
|
-
# identify which tool is running at any given moment.
|
|
56
|
-
# @param tool [Tool] the tool to identify and describe
|
|
57
|
-
#
|
|
58
|
-
# @return [void]
|
|
59
|
-
def tool_summary(tool)
|
|
60
|
-
printer.print(:bold, tool.name)
|
|
61
|
-
printer.puts(:muted, " #{tool.description}")
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Prints the text of a command to the console to help proactively expose potentials issues with
|
|
65
|
-
# syntax if Reviewer translated thte provided options in an unexpected way
|
|
66
|
-
# @param command [String, Command] the command to identify on the console
|
|
67
|
-
#
|
|
68
|
-
# @return [void] [description]
|
|
69
|
-
def current_command(command)
|
|
70
|
-
printer.puts(:bold, 'Now Running:')
|
|
71
|
-
printer.puts(:default, String(command))
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def success(timer)
|
|
75
|
-
printer.print(:success, 'Success')
|
|
76
|
-
printer.print(:success_light, " #{timer.total_seconds}s")
|
|
77
|
-
printer.print(:warning_light, " (#{timer.prep_percent}% prep ~#{timer.prep_seconds}s)") if timer.prepped?
|
|
78
|
-
newline
|
|
79
|
-
newline
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def failure(details, command: nil)
|
|
83
|
-
printer.print(:failure, 'Failure')
|
|
84
|
-
printer.puts(:muted, " #{details}")
|
|
85
|
-
|
|
86
|
-
return if command.nil?
|
|
87
|
-
|
|
88
|
-
newline
|
|
89
|
-
printer.puts(:bold, 'Failed Command:')
|
|
90
|
-
printer.puts(:muted, String(command))
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def unrecoverable(details)
|
|
94
|
-
printer.puts(:error, 'Unrecoverable Error:')
|
|
95
|
-
printer.puts(:muted, details)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def guidance(summary, details)
|
|
99
|
-
return if details.nil?
|
|
100
|
-
|
|
101
|
-
newline
|
|
102
|
-
printer.puts(:bold, summary)
|
|
103
|
-
printer.puts(:muted, details)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
65
|
def unfiltered(value)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
printer.stream << value
|
|
66
|
+
printer.write_raw(value)
|
|
110
67
|
end
|
|
111
68
|
|
|
112
|
-
|
|
69
|
+
private
|
|
113
70
|
|
|
71
|
+
# Returns the current console width, falling back to a default
|
|
72
|
+
# @return [Integer] the console width in columns
|
|
114
73
|
def console_width
|
|
115
74
|
return DEFAULT_CONSOLE_WIDTH if IO.console.nil?
|
|
116
75
|
|
|
117
76
|
_height, width = IO.console.winsize
|
|
118
77
|
|
|
119
|
-
width
|
|
78
|
+
width.positive? ? width : DEFAULT_CONSOLE_WIDTH
|
|
120
79
|
end
|
|
121
80
|
end
|
|
122
81
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
# Simple interactive prompt for yes/no questions.
|
|
5
|
+
# Wraps input/output streams for testability.
|
|
6
|
+
class Prompt
|
|
7
|
+
attr_reader :input, :output
|
|
8
|
+
|
|
9
|
+
# Creates an interactive prompt for yes/no questions
|
|
10
|
+
# @param input [IO] the input stream (defaults to $stdin)
|
|
11
|
+
# @param output [IO] the output stream (defaults to $stdout)
|
|
12
|
+
#
|
|
13
|
+
# @return [Prompt]
|
|
14
|
+
def initialize(input: $stdin, output: $stdout)
|
|
15
|
+
@input = input
|
|
16
|
+
@output = output
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Asks a yes/no question and returns the boolean result.
|
|
20
|
+
# Returns false in non-interactive contexts (CI, pipes).
|
|
21
|
+
#
|
|
22
|
+
# @param message [String] the question to display
|
|
23
|
+
# @return [Boolean] true if the user answered yes
|
|
24
|
+
def yes?(message)
|
|
25
|
+
return false unless interactive?
|
|
26
|
+
|
|
27
|
+
@output.print "#{message} (y/n) "
|
|
28
|
+
response = @input.gets&.strip&.downcase
|
|
29
|
+
response&.start_with?('y') || false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def interactive?
|
|
35
|
+
@input.respond_to?(:tty?) && @input.tty?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Report
|
|
5
|
+
# Formats a Report for summary output to the console
|
|
6
|
+
class Formatter
|
|
7
|
+
include Output::Formatting
|
|
8
|
+
|
|
9
|
+
attr_reader :report, :output
|
|
10
|
+
|
|
11
|
+
# Creates a formatter for displaying a report
|
|
12
|
+
# @param report [Report] the report to format
|
|
13
|
+
# @param output [Output] the output handler for console display
|
|
14
|
+
#
|
|
15
|
+
# @return [Formatter] a formatter instance
|
|
16
|
+
def initialize(report, output: Output.new)
|
|
17
|
+
@report = report
|
|
18
|
+
@output = output
|
|
19
|
+
@name_width = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Prints the formatted report to the console
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def print
|
|
26
|
+
if report.results.empty?
|
|
27
|
+
output.printer.puts(:muted, 'No tools to run')
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
print_tool_lines
|
|
32
|
+
output.newline
|
|
33
|
+
print_summary
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def print_tool_lines
|
|
39
|
+
@name_width = max_name_width
|
|
40
|
+
report.results.each { |result| print_tool_line(result) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def print_tool_line(result)
|
|
44
|
+
if result.missing?
|
|
45
|
+
print_missing_tool(result)
|
|
46
|
+
else
|
|
47
|
+
print_executed_tool(result)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
output.newline
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def print_missing_tool(result)
|
|
54
|
+
output.printer.print(:warning, "- #{result.tool_name.ljust(@name_width)}")
|
|
55
|
+
output.printer.print(:muted, ' not installed')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print_executed_tool(result)
|
|
59
|
+
style = status_style(result.success?)
|
|
60
|
+
mark = status_mark(result.success?)
|
|
61
|
+
output.printer.print(style, "#{mark} #{result.tool_name.ljust(@name_width)}")
|
|
62
|
+
print_timing(result)
|
|
63
|
+
print_details(result)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def print_timing(result)
|
|
67
|
+
output.printer.print(:muted, " #{format_duration(result.duration).rjust(6)}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def max_name_width
|
|
71
|
+
report.results.map { |result| result.tool_name.length }.max || 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def print_details(result)
|
|
75
|
+
detail = result.detail_summary
|
|
76
|
+
return unless detail
|
|
77
|
+
|
|
78
|
+
output.printer.print(:muted, " #{detail}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def print_summary
|
|
82
|
+
if report.success?
|
|
83
|
+
print_success_summary
|
|
84
|
+
else
|
|
85
|
+
print_failure_summary
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def print_success_summary
|
|
90
|
+
output.printer.print(:success, 'All passed')
|
|
91
|
+
output.printer.puts(:muted, " (#{format_duration(report.duration)})")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def print_failure_summary
|
|
95
|
+
failed_results = report.results.reject(&:success?).reject(&:missing?)
|
|
96
|
+
|
|
97
|
+
failed_results.each do |result|
|
|
98
|
+
output.newline
|
|
99
|
+
output.printer.puts(:failure, "#{result.tool_name}:")
|
|
100
|
+
print_truncated_output(result.stdout)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def print_truncated_output(text)
|
|
105
|
+
content = text.to_s.strip
|
|
106
|
+
return if content.empty?
|
|
107
|
+
|
|
108
|
+
lines = content.lines
|
|
109
|
+
print_lines(lines.first(10))
|
|
110
|
+
print_truncation_notice(lines.size - 10)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def print_lines(lines)
|
|
114
|
+
lines.each { |line| output.printer.puts(:default, line.chomp) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def print_truncation_notice(remaining)
|
|
118
|
+
return unless remaining.positive?
|
|
119
|
+
|
|
120
|
+
output.printer.puts(:muted, "[#{remaining} more lines]")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'report/formatter'
|
|
5
|
+
|
|
6
|
+
module Reviewer
|
|
7
|
+
# Collects results from multiple tool runs and provides serialization
|
|
8
|
+
class Report
|
|
9
|
+
attr_reader :results, :duration
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@results = []
|
|
13
|
+
@duration = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Adds a Runner::Result to the collection
|
|
17
|
+
#
|
|
18
|
+
# @param result [Runner::Result] the result to add
|
|
19
|
+
# @return [Array<Runner::Result>] the updated results array
|
|
20
|
+
def add(result)
|
|
21
|
+
@results << result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Records the total duration for all tool runs
|
|
25
|
+
#
|
|
26
|
+
# @param seconds [Float] the total elapsed time in seconds
|
|
27
|
+
# @return [Float] the recorded duration
|
|
28
|
+
def record_duration(seconds)
|
|
29
|
+
@duration = seconds
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Whether all executed tools in the report succeeded (excludes missing and skipped)
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean] true if all executed results are successful
|
|
35
|
+
def success?
|
|
36
|
+
executed_results.all?(&:success?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns the highest exit status from executed results (excludes missing and skipped)
|
|
40
|
+
#
|
|
41
|
+
# @return [Integer] the maximum exit status, or 0 if empty
|
|
42
|
+
def max_exit_status
|
|
43
|
+
executed_results.map(&:exit_status).max || 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns results for tools whose executables were not found
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Runner::Result>] missing tool results
|
|
49
|
+
def missing_results
|
|
50
|
+
results.select(&:missing?)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether any tools were missing
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean] true if any results are missing
|
|
56
|
+
def missing?
|
|
57
|
+
missing_results.any?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns data for missing tools (name and key)
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Runner::Result>] the missing results with tool info
|
|
63
|
+
def missing_tools
|
|
64
|
+
missing_results
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Converts the report to a hash suitable for serialization
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] structured hash with summary and tool results
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
success: success?,
|
|
73
|
+
summary: {
|
|
74
|
+
total: results.size,
|
|
75
|
+
passed: results.count(&:success?),
|
|
76
|
+
failed: results.count { |result| !result.success? && !result.missing? },
|
|
77
|
+
missing: missing_results.size,
|
|
78
|
+
duration: duration
|
|
79
|
+
},
|
|
80
|
+
tools: results.map(&:to_h)
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Converts the report to formatted JSON
|
|
85
|
+
#
|
|
86
|
+
# @return [String] JSON representation of the report
|
|
87
|
+
def to_json(*_args)
|
|
88
|
+
JSON.pretty_generate(to_h)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Results for tools that actually executed (excludes skipped and missing)
|
|
94
|
+
#
|
|
95
|
+
# @return [Array<Runner::Result>] executed results only
|
|
96
|
+
def executed_results
|
|
97
|
+
results.select(&:executed?)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Runner
|
|
5
|
+
# Extracts file paths from tool output so that subsequent re-runs can be scoped
|
|
6
|
+
# to only the files that had issues.
|
|
7
|
+
#
|
|
8
|
+
# Merges stdout and stderr before scanning because linters (rubocop, reek, etc.)
|
|
9
|
+
# write findings to stdout, not stderr. No common tool splits output with
|
|
10
|
+
# "passing files" on stdout and "failing files" on stderr, so merging is safe.
|
|
11
|
+
# The regex pattern and File.exist? guard filter out any incidental matches.
|
|
12
|
+
class FailedFiles
|
|
13
|
+
# Matches relative path-like tokens at or near the start of a line, allowing
|
|
14
|
+
# leading whitespace for tools that indent output (reek groupings, rspec nesting).
|
|
15
|
+
# Rejects absolute paths (starting with /) to exclude Ruby runtime warnings and
|
|
16
|
+
# gem internals that would otherwise match. Tool findings always use relative
|
|
17
|
+
# paths from the project root. Supports any file extension so non-Ruby tools
|
|
18
|
+
# (eslint, stylelint) also work. File.exist? in to_a provides a final guard.
|
|
19
|
+
#
|
|
20
|
+
# lib/foo.rb:45:3: C: Style/... (rubocop)
|
|
21
|
+
# test/foo_test.rb:45 (minitest)
|
|
22
|
+
# lib/foo.rb -- message (reek)
|
|
23
|
+
# src/app.js:10:5: error ... (eslint)
|
|
24
|
+
FILE_PATH_PATTERN = %r{^\s*([^/\s]\S*\.\w+)(?::\d| -- )}
|
|
25
|
+
|
|
26
|
+
attr_reader :stdout, :stderr
|
|
27
|
+
|
|
28
|
+
# Creates a failed-file extractor from captured tool output
|
|
29
|
+
# @param stdout [String, nil] captured standard output from the tool
|
|
30
|
+
# @param stderr [String, nil] captured standard error from the tool
|
|
31
|
+
#
|
|
32
|
+
# @return [FailedFiles]
|
|
33
|
+
def initialize(stdout, stderr)
|
|
34
|
+
@stdout = stdout
|
|
35
|
+
@stderr = stderr
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Regex-matched paths filtered to only those that exist on disk, deduplicated.
|
|
39
|
+
# @return [Array<String>] unique file paths that exist in the working directory
|
|
40
|
+
def to_a
|
|
41
|
+
matched_paths.select { |path| File.exist?(path) }.uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# All regex-matched paths before filesystem filtering. Useful for testing
|
|
45
|
+
# pattern matching without requiring real files on disk.
|
|
46
|
+
# @return [Array<String>] raw paths extracted from combined output
|
|
47
|
+
def matched_paths
|
|
48
|
+
combined_output.scan(FILE_PATH_PATTERN).flatten
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Merges both streams and strips ANSI escape codes before scanning. Linters
|
|
54
|
+
# write diagnostic output (file paths with line numbers) to stdout, while
|
|
55
|
+
# stderr typically only contains crash or startup errors. Scanning both catches
|
|
56
|
+
# paths regardless of which stream the tool uses. ANSI codes are stripped via
|
|
57
|
+
# Rainbow::StringUtils.uncolor because tools run with --color embed escape
|
|
58
|
+
# sequences around file paths, which would otherwise become part of the
|
|
59
|
+
# captured path string.
|
|
60
|
+
# @return [String] merged stdout and stderr, stripped of ANSI codes
|
|
61
|
+
def combined_output
|
|
62
|
+
Rainbow::StringUtils.uncolor([stdout, stderr].compact.join("\n"))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output/formatting'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
class Runner
|
|
7
|
+
# Display logic for tool execution: tool identity, success, failure, skipped, guidance
|
|
8
|
+
class Formatter
|
|
9
|
+
include Output::Formatting
|
|
10
|
+
|
|
11
|
+
attr_reader :output, :printer
|
|
12
|
+
private :output, :printer
|
|
13
|
+
|
|
14
|
+
# Creates a formatter for runner-specific display
|
|
15
|
+
# @param output [Output] the console output handler
|
|
16
|
+
#
|
|
17
|
+
# @return [Formatter]
|
|
18
|
+
def initialize(output)
|
|
19
|
+
@output = output
|
|
20
|
+
@printer = output.printer
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Prints the tool name and description as a header before execution
|
|
24
|
+
# @param tool [Tool] the tool being run
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
27
|
+
def tool_summary(tool)
|
|
28
|
+
printer.print(:bold, tool.name)
|
|
29
|
+
printer.puts(:muted, " #{tool.description}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Displays the exact command string being executed for debugging and copy/paste
|
|
33
|
+
# @param command [Command, String] the command to display
|
|
34
|
+
#
|
|
35
|
+
# @return [void]
|
|
36
|
+
def current_command(command)
|
|
37
|
+
printer.print(:default, ' ↳ ')
|
|
38
|
+
printer.puts(:muted, String(command))
|
|
39
|
+
output.newline
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Displays a success message with timing breakdown
|
|
43
|
+
# @param timer [Shell::Timer] the timer with prep and main execution times
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
def success(timer)
|
|
47
|
+
printer.print(:success, 'Success')
|
|
48
|
+
printer.print(:success_light, " #{timer.total_seconds}s")
|
|
49
|
+
printer.print(:warning_light, " (#{timer.prep_percent}% prep ~#{timer.prep_seconds}s)") if timer.prepped?
|
|
50
|
+
output.newline
|
|
51
|
+
output.newline
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Displays a skip notice with the reason
|
|
55
|
+
# @param reason [String] why the tool was skipped
|
|
56
|
+
#
|
|
57
|
+
# @return [void]
|
|
58
|
+
def skipped(reason = 'no matching files')
|
|
59
|
+
printer.print(:muted, 'Skipped')
|
|
60
|
+
printer.puts(:muted, " (#{reason})")
|
|
61
|
+
output.newline
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Displays a failure message with details and optionally the failed command
|
|
65
|
+
# @param details [String] the failure summary (e.g. exit status)
|
|
66
|
+
# @param command [Command, String, nil] the command that failed, if applicable
|
|
67
|
+
#
|
|
68
|
+
# @return [void]
|
|
69
|
+
def failure(details, command: nil)
|
|
70
|
+
printer.print(:failure, 'Failure')
|
|
71
|
+
printer.puts(:muted, " #{details}")
|
|
72
|
+
|
|
73
|
+
return unless command
|
|
74
|
+
|
|
75
|
+
output.newline
|
|
76
|
+
printer.puts(:bold, 'Failed Command:')
|
|
77
|
+
printer.puts(:muted, String(command))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Displays an unrecoverable error that prevents further execution
|
|
81
|
+
# @param details [String] the error description
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
def unrecoverable(details)
|
|
85
|
+
printer.puts(:error, 'Unrecoverable Error:')
|
|
86
|
+
printer.puts(:muted, details)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Displays contextual guidance after a failure to help the user recover
|
|
90
|
+
# @param summary [String] the guidance heading
|
|
91
|
+
# @param details [String, nil] the guidance body (skipped if nil)
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
94
|
+
def guidance(summary, details)
|
|
95
|
+
return unless details
|
|
96
|
+
|
|
97
|
+
output.newline
|
|
98
|
+
printer.puts(:bold, summary)
|
|
99
|
+
printer.puts(:muted, details)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Runner
|
|
5
|
+
# Handles the logic around what to display after a command has been run
|
|
6
|
+
class Guidance
|
|
7
|
+
attr_reader :command, :result, :formatter, :context
|
|
8
|
+
private :context
|
|
9
|
+
|
|
10
|
+
# Create an instance of guidance for suggesting recovery steps after errors
|
|
11
|
+
# @param command [Command] the command that was run and needs recovery guidance
|
|
12
|
+
# @param result [Result] the result of the command
|
|
13
|
+
# @param context [Context] the shared runtime dependencies
|
|
14
|
+
#
|
|
15
|
+
# @return [Guidance] the guidance class to suggest relevant recovery steps
|
|
16
|
+
def initialize(command:, result:, context:)
|
|
17
|
+
@command = command
|
|
18
|
+
@result = result
|
|
19
|
+
@context = context
|
|
20
|
+
@formatter = Runner::Formatter.new(context.output)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Prints the relevant guidance based on the command and result context
|
|
24
|
+
#
|
|
25
|
+
# @return [void] prints the relevant guidance to the stream
|
|
26
|
+
def show
|
|
27
|
+
case result
|
|
28
|
+
when executable_not_found? then show_missing_executable_guidance
|
|
29
|
+
when cannot_execute? then show_unrecoverable_guidance
|
|
30
|
+
else show_syntax_guidance
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Conditional check for when the command result was that the executable couldn't be found
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] true if the result indicates the command couldn't be found
|
|
39
|
+
def executable_not_found?
|
|
40
|
+
lambda(&:executable_not_found?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Conditional check for when the command result was that it was unable to be executed
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] true if the result indicates the command couldn't be executed
|
|
46
|
+
def cannot_execute?
|
|
47
|
+
lambda(&:cannot_execute?)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Shows the recovery guidance for when a command is missing
|
|
51
|
+
#
|
|
52
|
+
# @return [void] prints missing executable guidance
|
|
53
|
+
def show_missing_executable_guidance
|
|
54
|
+
tool = command.tool
|
|
55
|
+
installation_command = Command.new(tool, :install, context: context).string if tool.installable?
|
|
56
|
+
install_link = tool.install_link
|
|
57
|
+
|
|
58
|
+
formatter.failure("Missing executable for '#{tool}'", command: command)
|
|
59
|
+
formatter.guidance('Try installing the tool:', installation_command)
|
|
60
|
+
formatter.guidance('Read the installation guidance:', install_link)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Shows the recovery guidance for when a command generates an unrecoverable error
|
|
64
|
+
#
|
|
65
|
+
# @return [void] prints unrecoverable error guidance
|
|
66
|
+
def show_unrecoverable_guidance
|
|
67
|
+
formatter.unrecoverable(result.stderr)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Shows suggestions for ignoring or disable rules when a command fails after reviewing code
|
|
71
|
+
#
|
|
72
|
+
# @return [void] prints syntax guidance
|
|
73
|
+
def show_syntax_guidance
|
|
74
|
+
formatter.guidance('Selectively Ignore a Rule:', command.tool.links[:ignore_syntax])
|
|
75
|
+
formatter.guidance('Fully Disable a Rule:', command.tool.links[:disable_syntax])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|