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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/workflows/main.yml +79 -11
  4. data/.github/workflows/release.yml +98 -0
  5. data/.gitignore +1 -1
  6. data/.inch.yml +3 -1
  7. data/.reek.yml +175 -0
  8. data/.reviewer.example.yml +7 -2
  9. data/.reviewer.yml +166 -40
  10. data/.rubocop.yml +34 -2
  11. data/CHANGELOG.md +42 -2
  12. data/Gemfile +39 -1
  13. data/Gemfile.lock +291 -70
  14. data/LICENSE.txt +20 -4
  15. data/README.md +310 -21
  16. data/RELEASING.md +190 -0
  17. data/Rakefile +117 -0
  18. data/dependency_decisions.yml +61 -0
  19. data/exe/fmt +1 -1
  20. data/exe/rvw +1 -1
  21. data/lib/reviewer/arguments/files.rb +47 -20
  22. data/lib/reviewer/arguments/keywords.rb +34 -41
  23. data/lib/reviewer/arguments/tags.rb +11 -11
  24. data/lib/reviewer/arguments.rb +100 -29
  25. data/lib/reviewer/batch/formatter.rb +87 -0
  26. data/lib/reviewer/batch.rb +32 -48
  27. data/lib/reviewer/capabilities.rb +81 -0
  28. data/lib/reviewer/command/string/env.rb +12 -6
  29. data/lib/reviewer/command/string/flags.rb +2 -4
  30. data/lib/reviewer/command/string.rb +47 -12
  31. data/lib/reviewer/command.rb +65 -10
  32. data/lib/reviewer/configuration/loader.rb +70 -0
  33. data/lib/reviewer/configuration.rb +6 -3
  34. data/lib/reviewer/context.rb +15 -0
  35. data/lib/reviewer/doctor/config_check.rb +46 -0
  36. data/lib/reviewer/doctor/environment_check.rb +58 -0
  37. data/lib/reviewer/doctor/formatter.rb +75 -0
  38. data/lib/reviewer/doctor/keyword_check.rb +85 -0
  39. data/lib/reviewer/doctor/opportunity_check.rb +88 -0
  40. data/lib/reviewer/doctor/report.rb +63 -0
  41. data/lib/reviewer/doctor/tool_inventory.rb +41 -0
  42. data/lib/reviewer/doctor.rb +28 -0
  43. data/lib/reviewer/history.rb +10 -17
  44. data/lib/reviewer/output/formatting.rb +40 -0
  45. data/lib/reviewer/output/printer.rb +70 -9
  46. data/lib/reviewer/output.rb +37 -78
  47. data/lib/reviewer/prompt.rb +38 -0
  48. data/lib/reviewer/report/formatter.rb +124 -0
  49. data/lib/reviewer/report.rb +100 -0
  50. data/lib/reviewer/runner/failed_files.rb +66 -0
  51. data/lib/reviewer/runner/formatter.rb +103 -0
  52. data/lib/reviewer/runner/guidance.rb +79 -0
  53. data/lib/reviewer/runner/result.rb +150 -0
  54. data/lib/reviewer/runner/strategies/captured.rb +98 -23
  55. data/lib/reviewer/runner/strategies/passthrough.rb +2 -11
  56. data/lib/reviewer/runner.rb +126 -40
  57. data/lib/reviewer/session/formatter.rb +87 -0
  58. data/lib/reviewer/session.rb +208 -0
  59. data/lib/reviewer/setup/catalog.rb +233 -0
  60. data/lib/reviewer/setup/detector.rb +61 -0
  61. data/lib/reviewer/setup/formatter.rb +94 -0
  62. data/lib/reviewer/setup/gemfile_lock.rb +55 -0
  63. data/lib/reviewer/setup/generator.rb +54 -0
  64. data/lib/reviewer/setup/tool_block.rb +112 -0
  65. data/lib/reviewer/setup.rb +41 -0
  66. data/lib/reviewer/shell/result.rb +14 -15
  67. data/lib/reviewer/shell/timer.rb +40 -35
  68. data/lib/reviewer/shell.rb +41 -12
  69. data/lib/reviewer/tool/conversions.rb +20 -0
  70. data/lib/reviewer/tool/file_resolver.rb +54 -0
  71. data/lib/reviewer/tool/settings.rb +88 -44
  72. data/lib/reviewer/tool/test_file_mapper.rb +73 -0
  73. data/lib/reviewer/tool/timing.rb +78 -0
  74. data/lib/reviewer/tool.rb +88 -69
  75. data/lib/reviewer/tools.rb +47 -33
  76. data/lib/reviewer/version.rb +1 -1
  77. data/lib/reviewer.rb +109 -50
  78. data/reviewer.gemspec +16 -19
  79. metadata +101 -142
  80. data/lib/reviewer/conversions.rb +0 -16
  81. data/lib/reviewer/guidance.rb +0 -77
  82. data/lib/reviewer/keywords/git/staged.rb +0 -64
  83. data/lib/reviewer/keywords/git.rb +0 -14
  84. data/lib/reviewer/keywords.rb +0 -9
  85. data/lib/reviewer/loader.rb +0 -59
  86. data/lib/reviewer/output/scrubber.rb +0 -48
  87. data/lib/reviewer/output/token.rb +0 -85
data/exe/fmt CHANGED
@@ -4,4 +4,4 @@
4
4
 
5
5
  require_relative '../lib/reviewer'
6
6
 
7
- Reviewer.format(clear_screen: true)
7
+ Reviewer.format
data/exe/rvw CHANGED
@@ -4,4 +4,4 @@
4
4
 
5
5
  require_relative '../lib/reviewer'
6
6
 
7
- Reviewer.review(clear_screen: true)
7
+ Reviewer.review
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'open3'
4
+
3
5
  module Reviewer
4
6
  class Arguments
5
7
  # Generates a Ruby-friendly list (Array) of files to run the command against from the provided
@@ -10,10 +12,13 @@ module Reviewer
10
12
  alias raw provided
11
13
 
12
14
  # Generates an instance of files from the provided arguments
13
- # @param provided: Reviewer.arguments.files.raw [Array, String] file arguments provided
14
- # directly via the -f or --files flag on the command line.
15
- # @param keywords: Reviewer.arguments.keywords [Array, String] keywords that can potentially
16
- # be translated to a list of files (ex. 'staged')
15
+ # @param provided [Array<String>] file arguments provided
16
+ # directly via the -f or --files flag on the command line
17
+ # @param keywords [Array<String>] reserved keywords that can potentially
18
+ # be translated to a list of files (e.g. 'staged', 'modified')
19
+ # @param output [Output] the console output handler
20
+ # @param on_git_error [Proc, nil] callback invoked with the error message
21
+ # when a git command fails (nil silently swallows the error)
17
22
  #
18
23
  # @example Using the `-f` flag: `rvw -f ./file.rb`
19
24
  # reviewer = Reviewer::Arguments::Files.new(provided: ['./file.rb'], keywords: [])
@@ -23,24 +28,22 @@ module Reviewer
23
28
  # reviewer.to_a # => ['./file.rb','./directory/file.rb']
24
29
  #
25
30
  # @return [self]
26
- def initialize(provided: Reviewer.arguments.files.raw, keywords: Reviewer.arguments.keywords)
31
+ def initialize(provided: [], keywords: [], output: Output.new, on_git_error: nil)
27
32
  @provided = Array(provided)
28
33
  @keywords = Array(keywords)
34
+ @output = output
35
+ @on_git_error = on_git_error
29
36
  end
30
37
 
31
38
  # Provides the full list of file/path values derived from the command-line arguments
32
39
  #
33
40
  # @return [Array<String>] full collection of the file arguments as a string
34
- def to_a
35
- file_list
36
- end
41
+ def to_a = file_list
37
42
 
38
43
  # Provides the full list of file/path values derived from the command-line arguments
39
44
  #
40
- # @return [String] comma-separated string of the derived tag values
41
- def to_s
42
- to_a.join(',')
43
- end
45
+ # @return [String] comma-separated string of the derived file values
46
+ def to_s = to_a.join(',')
44
47
 
45
48
  # Summary of the state of the file arguments
46
49
  #
@@ -75,19 +78,43 @@ module Reviewer
75
78
  return [] unless keywords.any?
76
79
 
77
80
  keywords.map do |keyword|
78
- next unless respond_to?(keyword.to_sym, true)
81
+ method_name = keyword.to_sym
82
+ next unless respond_to?(method_name, true)
79
83
 
80
- send(keyword.to_sym)
84
+ send(method_name)
81
85
  end.flatten.compact.uniq
82
86
  end
83
87
 
84
- # If `staged` is passed as a keyword via the command-line, this will get the list of staged
85
- # files via Git
86
- #
87
- # @return [Array] list of the currently staged files
88
88
  def staged
89
- # Use git for list of staged fields
90
- ::Reviewer::Keywords::Git::Staged.list
89
+ git_files(%w[diff --staged --name-only])
90
+ end
91
+
92
+ def unstaged
93
+ git_files(%w[diff --name-only])
94
+ end
95
+
96
+ def modified
97
+ git_files(%w[diff --name-only HEAD])
98
+ end
99
+
100
+ def untracked
101
+ git_files(%w[ls-files --others --exclude-standard])
102
+ end
103
+
104
+ # Executes a git command and returns the output as an array of file paths
105
+ # @param options [Array<String>] the git command options
106
+ #
107
+ # @return [Array<String>] the output lines from the command
108
+ def git_files(options)
109
+ command = (%w[git --no-pager] + options).join(' ')
110
+ stdout, stderr, status = Open3.capture3(command)
111
+
112
+ return stdout.split("\n").reject(&:empty?) if status.success?
113
+
114
+ raise SystemCallError.new("Git Error: #{stderr} (#{command})", status.exitstatus.to_i)
115
+ rescue SystemCallError => e
116
+ @on_git_error&.call(e.message)
117
+ []
91
118
  end
92
119
  end
93
120
  end
@@ -8,34 +8,37 @@ module Reviewer
8
8
  # @!attribute provided
9
9
  # @return [Array<String>] the keywords extracted from the command-line arguments
10
10
  class Keywords
11
- RESERVED = %w[staged].freeze
11
+ RESERVED = %w[staged unstaged modified untracked failed].freeze
12
12
 
13
- attr_accessor :provided
13
+ attr_reader :provided
14
+
15
+ # Sets the tools collection after initialization when tools become available
16
+ # @param value [Tools] the configured tools collection
17
+ # @return [Tools] the tools collection
18
+ attr_writer :tools
14
19
 
15
20
  alias raw provided
16
21
 
17
22
  # Generates an instance of parsed keywords from the provided arguments
18
- # @param *provided [Array<String>] the leftover (non-flag) arguments from the command line
23
+ # @param provided [Array<String>] the leftover (non-flag) arguments from the command line
24
+ # @param tools [Tools] the collection of configured tools for keyword recognition
19
25
  #
20
26
  # @return [self]
21
- def initialize(*provided)
27
+ def initialize(*provided, tools: nil)
22
28
  @provided = Array(provided.flatten)
29
+ @tools = tools
23
30
  end
24
31
 
25
- # Proves the full list of raw keyword arguments explicitly passed via command-line as an array
32
+ # Provides the full list of raw keyword arguments explicitly passed via command-line as an array
26
33
  #
27
- # @return [Array] full collection of the provided keyword arguments as a string
28
- def to_a
29
- provided
30
- end
34
+ # @return [Array<String>] full collection of the provided keyword arguments
35
+ def to_a = provided
31
36
 
32
37
  # Provides the full list of raw keyword arguments explicitly passed via command-line as a
33
38
  # comma-separated string
34
39
  #
35
- # @return [String] comma-separated list of the file arguments as a string
36
- def to_s
37
- to_a.join(',')
38
- end
40
+ # @return [String] comma-separated list of the keyword arguments as a string
41
+ def to_s = to_a.join(',')
39
42
 
40
43
  # Summary of the state of keyword arguments based on how Reviewer parsed them
41
44
  #
@@ -53,52 +56,47 @@ module Reviewer
53
56
  end
54
57
  alias inspect to_h
55
58
 
59
+ # Whether the `failed` keyword was provided
60
+ #
61
+ # @return [Boolean] true if the `failed` keyword is present
62
+ def failed? = provided.include?('failed')
63
+
56
64
  # Extracts reserved keywords from the provided arguments
57
65
  #
58
66
  # @return [Array<String>] intersection of provided arguments and reserved keywords
59
- def reserved
60
- intersection_with RESERVED
61
- end
67
+ def reserved = intersection_with(RESERVED)
62
68
 
63
69
  # Extracts keywords that match configured tags for enabled tools
64
70
  #
65
71
  # @return [Array<String>] intersection of provided arguments and configured tags for tools
66
- def for_tags
67
- intersection_with configured_tags
68
- end
72
+ def for_tags = intersection_with(configured_tags)
69
73
 
70
74
  # Extracts keywords that match configured tool keys
71
75
  #
72
76
  # @return [Array<String>] intersection of provided arguments and configured tool names
73
- def for_tool_names
74
- intersection_with configured_tool_names
75
- end
77
+ def for_tool_names = intersection_with(configured_tool_names)
76
78
 
77
79
  # Extracts keywords that match any possible recognized keyword values
78
80
  #
79
81
  # @return [Array<String>] intersection of provided arguments and recognizable keywords
80
- def recognized
81
- intersection_with possible
82
- end
82
+ def recognized = intersection_with(possible)
83
83
 
84
84
  # Extracts keywords that don't match any possible recognized keyword values
85
85
  #
86
86
  # @return [Array<String>] leftover keywords that weren't recognized
87
- def unrecognized
88
- (provided - recognized).uniq.sort
89
- end
87
+ def unrecognized = (provided - recognized).uniq.sort
90
88
 
91
89
  # Provides the complete list of all recognized keywords based on configuration
92
90
  #
93
- # @return [Array<String>] all keywords that Reviewer can recognized
94
- def possible
95
- (RESERVED + configured_tags + configured_tool_names).uniq.sort
96
- end
91
+ # @return [Array<String>] all keywords that Reviewer can recognize
92
+ def possible = (RESERVED + configured_tags + configured_tool_names).uniq.sort
97
93
 
98
94
  # Provides the complete list of all configured tags for enabled tools
99
95
  #
100
96
  # @return [Array<String>] all unique configured tags
101
97
  def configured_tags
98
+ return [] unless tools
99
+
102
100
  tools.enabled.map(&:tags).flatten.uniq.sort
103
101
  end
104
102
 
@@ -106,6 +104,8 @@ module Reviewer
106
104
  #
107
105
  # @return [Array<String>] all unique configured tools
108
106
  def configured_tool_names
107
+ return [] unless tools
108
+
109
109
  # We explicitly don't sort the tool names list because Reviewer uses the configuration order
110
110
  # to determine the execution order. So not sorting maintains the predicted order it will run
111
111
  # in and leaves the option to sort to the consuming code if needed
@@ -114,20 +114,13 @@ module Reviewer
114
114
 
115
115
  private
116
116
 
117
- # Provides a collection of enabled Tools for convenient access
118
- #
119
- # @return [Array<Reviewer::Tool>] collection of all currently enabled tools
120
- def tools
121
- @tools ||= Reviewer.tools
122
- end
117
+ attr_reader :tools
123
118
 
124
119
  # Syntactic sugar for finding intersections with valid keywords
125
120
  # @param values [Array<String>] the collection to use for finding intersecting values
126
121
  #
127
122
  # @return [Array<String>] the list of intersecting values
128
- def intersection_with(values)
129
- (values & provided).uniq.sort
130
- end
123
+ def intersection_with(values) = (values & provided).uniq.sort
131
124
  end
132
125
  end
133
126
  end
@@ -4,15 +4,19 @@ module Reviewer
4
4
  class Arguments
5
5
  # Handles the logic of translating tag arguments
6
6
  class Tags
7
- attr_accessor :provided, :keywords
7
+ # @!attribute provided
8
+ # @return [Array<String>] tags explicitly provided via -t or --tags flag
9
+ # @!attribute keywords
10
+ # @return [Array<String>] tags derived from keyword arguments
11
+ attr_reader :provided, :keywords
8
12
 
9
13
  alias raw provided
10
14
 
11
- # Generates an instace of parsed tags from the provided arguments by merging tag arguments
15
+ # Generates an instance of parsed tags from the provided arguments by merging tag arguments
12
16
  # that were provided via either flags or keywords
13
- # @param provided: Reviewer.arguments.tags.raw [Array<String>] tag arguments provided
17
+ # @param provided [Array<String>] tag arguments provided
14
18
  # directly via the -t or --tags flag on the command line.
15
- # @param keywords: Reviewer.arguments.keywords [Array, String] keywords that can potentially
19
+ # @param keywords [Array, String] keywords that can potentially
16
20
  # be translated to a list of tags based on the tags used in the configuration file
17
21
  #
18
22
  # @example Using keywords: `rvw ruby` (assuming a 'ruby' tag is defined)
@@ -23,7 +27,7 @@ module Reviewer
23
27
  # Reviewer::Arguments::Tags.new.to_a # => ['css', 'ruby']
24
28
  #
25
29
  # @return [self]
26
- def initialize(provided: Reviewer.arguments.tags.raw, keywords: Reviewer.arguments.keywords.for_tags)
30
+ def initialize(provided: [], keywords: [])
27
31
  @provided = Array(provided)
28
32
  @keywords = Array(keywords)
29
33
  end
@@ -31,16 +35,12 @@ module Reviewer
31
35
  # Provides the full list of tags values derived from the command-line arguments
32
36
  #
33
37
  # @return [Array<String>] full collection of the tag arguments as a string
34
- def to_a
35
- tag_list
36
- end
38
+ def to_a = tag_list
37
39
 
38
40
  # Provides the full list of tag values derived from the command-line arguments
39
41
  #
40
42
  # @return [String] comma-separated string of the derived tag values
41
- def to_s
42
- to_a.join(',')
43
- end
43
+ def to_s = to_a.join(',')
44
44
 
45
45
  # Summary of the state of the tag arguments
46
46
  #
@@ -19,13 +19,18 @@ module Reviewer
19
19
  # `rvw ruby staged`
20
20
  #
21
21
  class Arguments
22
- attr_accessor :options
22
+ # Valid output format options for the --format flag
23
+ KNOWN_FORMATS = %i[streaming summary json].freeze
24
+
25
+ # @!attribute options
26
+ # @return [Slop::Result] the parsed command-line options
27
+ attr_reader :options
23
28
 
24
29
  attr_reader :output
25
30
 
26
- # A catch all for aguments passed to reviewer via the command-line so they can be interpreted
27
- # and made available via the relevant classes.
28
- # @param options = ARGV [Hash] options to parse and extract the relevant values for a run
31
+ # Parses command-line arguments and makes them available as tags, files, and keywords.
32
+ # @param options [Array<String>] the command-line arguments to parse (defaults to ARGV)
33
+ # @param output [Output] the console output handler for displaying messages
29
34
  #
30
35
  # @example Using all options: `rvw keyword_one keyword_two --files ./example.rb,./example_test.rb --tags syntax`
31
36
  # reviewer = Reviewer::Arguments.new
@@ -34,24 +39,40 @@ module Reviewer
34
39
  # reviewer.keywords.to_a # => ['keyword_one', 'keyword_two']
35
40
  #
36
41
  # @return [self]
37
- def initialize(options = ARGV)
38
- @output = Output.new
39
- @options = Slop.parse options do |opts|
40
- opts.array '-f', '--files', 'a list of comma-separated files or paths', delimiter: ',', default: []
41
- opts.array '-t', '--tags', 'a list of comma-separated tags', delimiter: ',', default: []
42
-
43
- opts.on '-v', '--version', 'print the version' do
44
- @output.help VERSION
45
- exit
46
- end
47
-
48
- opts.on '-h', '--help', 'print the help' do
49
- @output.help opts
50
- exit
51
- end
52
- end
42
+ def initialize(options = ARGV, output: Output.new)
43
+ @output = output
44
+ @options = Slop.parse(options) { |opts| configure_options(opts) }
45
+ end
46
+
47
+ private
48
+
49
+ def configure_options(opts)
50
+ configure_input_options(opts)
51
+ configure_output_options(opts)
52
+ configure_info_options(opts)
53
+ end
54
+
55
+ def configure_input_options(opts)
56
+ opts.array '-f', '--files', 'a list of comma-separated files or paths', delimiter: ',', default: []
57
+ opts.array '-t', '--tags', 'a list of comma-separated tags', delimiter: ',', default: []
58
+ end
59
+
60
+ def configure_output_options(opts)
61
+ opts.on '-r', '--raw', 'force raw output (no capturing)'
62
+ opts.on '-j', '--json', 'output results as JSON'
63
+ opts.string '--format', 'output format (streaming, summary, json)', default: 'streaming'
53
64
  end
54
65
 
66
+ def configure_info_options(opts)
67
+ opts.on('-v', '--version', 'print the version')
68
+ opts.on('-h', '--help', 'print the help')
69
+ opts.on('-c', '--capabilities', 'output capabilities as JSON')
70
+ end
71
+
72
+ def session_formatter = @session_formatter ||= Session::Formatter.new(output)
73
+
74
+ public
75
+
55
76
  # Converts the arguments to a hash for versatility
56
77
  #
57
78
  # @return [Hash] The files, tags, and keywords collected from the command line options
@@ -66,23 +87,73 @@ module Reviewer
66
87
 
67
88
  # The tag arguments collected from the command line via the `-t` or `--tags` flag
68
89
  #
69
- # @return [Arguments::Tags] an colelction of the tag arguments collected from the command-line
70
- def tags
71
- @tags ||= Arguments::Tags.new(provided: options[:tags])
72
- end
90
+ # @return [Arguments::Tags] a collection of the tag arguments collected from the command-line
91
+ def tags = @tags ||= Arguments::Tags.new(provided: options[:tags])
73
92
 
74
93
  # The file arguments collected from the command line via the `-f` or `--files` flag
75
94
  #
76
- # @return [Arguments::Files] an collection of the file arguments collected from the command-line
95
+ # @return [Arguments::Files] a collection of the file arguments collected from the command-line
77
96
  def files
78
- @files ||= Arguments::Files.new(provided: options[:files])
97
+ @files ||= Arguments::Files.new(
98
+ provided: options[:files],
99
+ keywords: keywords.reserved,
100
+ output: output,
101
+ on_git_error: session_formatter.method(:git_error)
102
+ )
79
103
  end
80
104
 
81
105
  # The leftover arguments collected from the command line without being associated with a flag
82
106
  #
83
- # @return [Arguments::Keywords] an collection of the leftover arguments as keywords
84
- def keywords
85
- @keywords ||= Arguments::Keywords.new(options.arguments)
107
+ # @return [Arguments::Keywords] a collection of the leftover arguments as keywords
108
+ def keywords = @keywords ||= Arguments::Keywords.new(options.arguments)
109
+
110
+ # Whether the --help flag was passed
111
+ #
112
+ # @return [Boolean] true if help was requested
113
+ def help? = options[:help]
114
+
115
+ # Whether the --version flag was passed
116
+ #
117
+ # @return [Boolean] true if version was requested
118
+ def version? = options[:version]
119
+
120
+ # Whether to force raw/passthrough output regardless of tool count
121
+ #
122
+ # @return [Boolean] true if raw output mode is requested
123
+ def raw? = options[:raw]
124
+
125
+ # Whether to output results as JSON
126
+ #
127
+ # @return [Boolean] true if JSON output mode is requested
128
+ def json? = options[:json]
129
+
130
+ # The output format for results
131
+ #
132
+ # @return [Symbol] the output format (:streaming, :summary, or :json)
133
+ def format
134
+ return :json if json?
135
+
136
+ value = options[:format].to_sym
137
+ return value if KNOWN_FORMATS.include?(value)
138
+
139
+ session_formatter.invalid_format(options[:format], KNOWN_FORMATS)
140
+ :streaming
141
+ end
142
+
143
+ # Whether output should be streamed directly (not captured for later formatting)
144
+ #
145
+ # @return [Boolean] true if in streaming mode
146
+ def streaming? = format == :streaming
147
+
148
+ # Determines the appropriate runner strategy based on CLI flags
149
+ #
150
+ # @param multiple_tools [Boolean] whether multiple tools are being run
151
+ # @return [Class] the strategy class (Captured or Passthrough)
152
+ def runner_strategy(multiple_tools:)
153
+ return Runner::Strategies::Passthrough if raw?
154
+ return Runner::Strategies::Captured unless streaming?
155
+
156
+ multiple_tools ? Runner::Strategies::Captured : Runner::Strategies::Passthrough
86
157
  end
87
158
  end
88
159
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../output/formatting'
4
+
5
+ module Reviewer
6
+ class Batch
7
+ # Display logic for batch execution: summary, run preview, missing tools
8
+ class Formatter
9
+ include Output::Formatting
10
+
11
+ attr_reader :output, :printer
12
+ private :output, :printer
13
+
14
+ # Creates a formatter for batch execution 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
+ # Displays a one-line success summary with timing and tool count
24
+ # @param tool_count [Integer] the number of tools that ran
25
+ # @param seconds [Float] total elapsed time in seconds
26
+ #
27
+ # @return [void]
28
+ def summary(tool_count, seconds)
29
+ output.newline
30
+ printer.print(:success, CHECKMARK)
31
+ printer.print(:muted, " ~#{seconds.round(1)} seconds")
32
+ printer.print(:muted, " for #{tool_count} tools") if tool_count > 1
33
+ output.newline
34
+ end
35
+
36
+ # Displays a preview of which tools will run and their target files
37
+ # @param entries [Array<Hash>] each with :name and :files keys
38
+ #
39
+ # @return [void]
40
+ def run_summary(entries)
41
+ return if entries.empty?
42
+
43
+ entries.each { |entry| print_run_entry(entry) }
44
+ output.newline
45
+ end
46
+
47
+ # Displays a list of tools whose executables were not found, with install hints
48
+ # @param missing [Array<Runner::Result>] the results for missing tools
49
+ # @param tools [Array<Tool>] the tools that were in the batch
50
+ #
51
+ # @return [void]
52
+ def missing_tools(missing, tools:)
53
+ output.newline
54
+ printer.puts(:warning, "#{missing.size} not installed:")
55
+ tool_lookup = tools.to_h { |tool| [tool.key, tool] }
56
+ missing.each { |result| print_missing_hint(result.tool_name, tool_lookup[result.tool_key]) }
57
+ output.newline
58
+ end
59
+
60
+ # Displays a message when `rvw failed` is used but no tools failed in the last run
61
+ #
62
+ # @return [void]
63
+ def no_failures_to_retry
64
+ printer.puts(:muted, 'No failures to retry')
65
+ end
66
+
67
+ # Displays a message when `rvw failed` is used but no previous run exists in history
68
+ #
69
+ # @return [void]
70
+ def no_previous_run
71
+ printer.puts(:muted, 'No previous run found')
72
+ end
73
+
74
+ private
75
+
76
+ def print_missing_hint(name, tool)
77
+ hint = tool&.installable? ? tool.install_command : ''
78
+ printer.puts(:muted, " #{name.ljust(22)}#{hint}")
79
+ end
80
+
81
+ def print_run_entry(entry)
82
+ printer.puts(:muted, entry[:name])
83
+ entry[:files].each { |file| printer.puts(:muted, " #{file}") }
84
+ end
85
+ end
86
+ end
87
+ end