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
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Reviewer
6
+ class Tool
7
+ # Manages timing persistence for a tool — recording, retrieving, and averaging
8
+ # execution times, plus tracking when the prepare command was last run.
9
+ class Timing
10
+ SIX_HOURS_IN_SECONDS = 60 * 60 * 6
11
+
12
+ # Creates a timing tracker for a specific tool
13
+ # @param history [History] the persistence store for timing data
14
+ # @param key [Symbol] the tool's unique key
15
+ #
16
+ # @return [Timing]
17
+ def initialize(history, key)
18
+ @history = history
19
+ @key = key
20
+ end
21
+
22
+ # Specifies when the tool last had its `prepare` command run
23
+ #
24
+ # @return [Time, nil] timestamp of when the `prepare` command was last run
25
+ def last_prepared_at
26
+ date_string = @history.get(@key, :last_prepared_at).to_s
27
+
28
+ date_string.empty? ? nil : DateTime.parse(date_string).to_time
29
+ end
30
+
31
+ # Sets the timestamp for when the tool last ran its `prepare` command
32
+ # @param timestamp [DateTime, Time] the value to record
33
+ #
34
+ # @return [void]
35
+ def last_prepared_at=(timestamp)
36
+ @history.set(@key, :last_prepared_at, timestamp.to_s)
37
+ end
38
+
39
+ # Calculates the average execution time for a command
40
+ # @param command [Command] the command to get timing for
41
+ #
42
+ # @return [Float] the average time in seconds or 0 if no history
43
+ def average_time(command)
44
+ times = get_timing(command)
45
+
46
+ times.any? ? times.sum / times.size : 0
47
+ end
48
+
49
+ # Retrieves historical timing data for a command
50
+ # @param command [Command] the command to look up
51
+ #
52
+ # @return [Array<Float>] the last few recorded execution times
53
+ def get_timing(command)
54
+ @history.get(@key, command.raw_string) || []
55
+ end
56
+
57
+ # Records the execution time for a command to calculate running averages
58
+ # @param command [Command] the command that was run
59
+ # @param time [Float, nil] the execution time in seconds
60
+ #
61
+ # @return [void]
62
+ def record_timing(command, time)
63
+ return unless time
64
+
65
+ timing = get_timing(command).take(4) << time.round(2)
66
+
67
+ @history.set(@key, command.raw_string, timing)
68
+ end
69
+
70
+ # Determines whether the `prepare` command was run recently enough
71
+ #
72
+ # @return [Boolean] true if the timestamp is nil or older than six hours
73
+ def stale?
74
+ !last_prepared_at || last_prepared_at < Time.now - SIX_HOURS_IN_SECONDS
75
+ end
76
+ end
77
+ end
78
+ end
data/lib/reviewer/tool.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'tool/conversions'
4
+ require_relative 'tool/file_resolver'
3
5
  require_relative 'tool/settings'
6
+ require_relative 'tool/test_file_mapper'
7
+ require_relative 'tool/timing'
4
8
 
5
9
  module Reviewer
6
10
  # Provides an instance of a specific tool for accessing its settings and run history
@@ -8,14 +12,9 @@ module Reviewer
8
12
  extend Forwardable
9
13
  include Comparable
10
14
 
11
- # In general, Reviewer tries to save time where it can. In the case of the "prepare" command
12
- # used by some tools to retrieve data, it only runs it occasionally in order to save time.
13
- # This is the default window that it uses to determine if the tool's preparation step should be
14
- # considered stale and needs to be rerun. Frequent enough that it shouldn't get stale, but
15
- # infrequent enough that it's not cumbersome.
16
- SIX_HOURS_IN_SECONDS = 60 * 60 * 6
15
+ SIX_HOURS_IN_SECONDS = Timing::SIX_HOURS_IN_SECONDS
17
16
 
18
- attr_reader :settings, :history
17
+ attr_reader :settings
19
18
 
20
19
  def_delegators :@settings,
21
20
  :key,
@@ -27,78 +26,86 @@ module Reviewer
27
26
  :links,
28
27
  :enabled?,
29
28
  :disabled?,
30
- :max_exit_status
29
+ :skip_in_batch?,
30
+ :max_exit_status,
31
+ :supports_files?
31
32
 
32
- alias to_sym key
33
- alias to_s name
33
+ # Returns the tool's key as a symbol
34
+ # @return [Symbol] the tool's unique identifier
35
+ def to_sym = key
36
+
37
+ # Returns the tool's name as a string
38
+ # @return [String] the tool's display name
39
+ def to_s = name
34
40
 
35
41
  # Create an instance of a tool
36
42
  # @param tool_key [Symbol] the key to the tool from the configuration file
43
+ # @param config [Hash] the tool's configuration hash
44
+ # @param history [History] the history store for timing and state persistence
37
45
  #
38
46
  # @return [Tool] an instance of tool for accessing settings information and facts about the tool
39
- def initialize(tool_key)
40
- @settings = Settings.new(tool_key)
47
+ def initialize(tool_key, config:, history:)
48
+ @settings = Settings.new(tool_key, config: config)
49
+ @history = history
50
+ @timing = Timing.new(history, key)
41
51
  end
42
52
 
43
- # For determining if the tool should run it's prepration command. It will only be run both if
44
- # the tool has a preparation command, and the command hasn't been run 6 hours
53
+ # For determining if the tool should run its preparation command. It will only be run if
54
+ # the tool has a preparation command and it hasn't been run in the last 6 hours
45
55
  #
46
56
  # @return [Boolean] true if the tool has a configured `prepare` command that hasn't been run in
47
57
  # the last 6 hours
48
- def prepare?
49
- preparable? && stale?
50
- end
58
+ def prepare? = preparable? && stale?
51
59
 
52
60
  # Determines whether a tool has a specific command type configured
53
61
  # @param command_type [Symbol] one of the available command types defined in Command::TYPES
54
62
  #
55
63
  # @return [Boolean] true if the command type is configured and not blank
56
64
  def command?(command_type)
57
- commands.key?(command_type) && !commands[command_type].nil?
65
+ commands.key?(command_type) && commands[command_type]
58
66
  end
59
67
 
60
68
  # Determines if the tool can run a `install` command
61
69
  #
62
70
  # @return [Boolean] true if there is a non-blank `install` command configured
63
- def installable?
64
- command?(:install)
71
+ def installable? = command?(:install)
72
+
73
+ # Returns the install command string for this tool
74
+ #
75
+ # @return [String, nil] the install command or nil if not configured
76
+ def install_command
77
+ commands[:install]
65
78
  end
66
79
 
67
80
  # Determines if the tool can run a `prepare` command
68
81
  #
69
82
  # @return [Boolean] true if there is a non-blank `prepare` command configured
70
- def preparable?
71
- command?(:prepare)
72
- end
83
+ def preparable? = command?(:prepare)
73
84
 
74
85
  # Determines if the tool can run a `review` command
75
86
  #
76
87
  # @return [Boolean] true if there is a non-blank `review` command configured
77
- def reviewable?
78
- command?(:review)
79
- end
88
+ def reviewable? = command?(:review)
80
89
 
81
90
  # Determines if the tool can run a `format` command
82
91
  #
83
92
  # @return [Boolean] true if there is a non-blank `format` command configured
84
- def formattable?
85
- command?(:format)
86
- end
93
+ def formattable? = command?(:format)
87
94
 
88
- # Specifies when the tool last had it's `prepare` command run
95
+ # Whether this tool matches any of the given tags and is eligible for batch runs
89
96
  #
90
- # @return [DateTime] timestamp of when the `prepare` command was last run
91
- def last_prepared_at
92
- Reviewer.history.get(key, :last_prepared_at)
97
+ # @param tag_list [Array<String, Symbol>] tags to match against
98
+ # @return [Boolean] true if the tool is batch-eligible and shares at least one tag
99
+ def matches_tags?(tag_list)
100
+ !skip_in_batch? && tag_list.intersect?(tags)
93
101
  end
94
102
 
95
- # Sets the timestamp for when the tool last ran its `prepare` command
96
- # @param last_prepared_at [DateTime] the value to record for when the `prepare` command last ran
97
- #
98
- # @return [DateTime] timestamp of when the `prepare` command was last run
99
- def last_prepared_at=(last_prepared_at)
100
- Reviewer.history.set(key, :last_prepared_at, last_prepared_at)
101
- end
103
+ def_delegators :@timing,
104
+ :last_prepared_at,
105
+ :last_prepared_at=,
106
+ :average_time,
107
+ :get_timing,
108
+ :record_timing
102
109
 
103
110
  # Determines whether the `prepare` command was run recently enough
104
111
  #
@@ -107,22 +114,18 @@ module Reviewer
107
114
  def stale?
108
115
  return false unless preparable?
109
116
 
110
- last_prepared_at.nil? || last_prepared_at < Time.now - SIX_HOURS_IN_SECONDS
117
+ @timing.stale?
111
118
  end
112
119
 
113
120
  # Convenience method for determining if a tool has a configured install link
114
121
  #
115
122
  # @return [Boolean] true if there is an `install` key under links and the value isn't blank
116
- def install_link?
117
- links.key?(:install) && !links[:install].nil?
118
- end
123
+ def install_link? = links.key?(:install) && !!links[:install]
119
124
 
120
125
  # Returns the text for the install link if available
121
126
  #
122
127
  # @return [String, nil] the link if it exists, nil otherwise
123
- def install_link
124
- install_link? ? links.fetch(:install) : nil
125
- end
128
+ def install_link = install_link? ? links.fetch(:install) : nil
126
129
 
127
130
  # Determines if two tools are equal
128
131
  # @param other [Tool] the tool to compare to the current instance
@@ -132,5 +135,43 @@ module Reviewer
132
135
  settings == other.settings
133
136
  end
134
137
  alias :== eql?
138
+
139
+ # Records the pass/fail status and failed files from a result into history
140
+ # @param result [Runner::Result] the result of running this tool
141
+ #
142
+ # @return [void]
143
+ def record_run(result)
144
+ status = result.success? ? :passed : :failed
145
+ @history.set(key, :last_status, status)
146
+
147
+ if result.success?
148
+ @history.set(key, :last_failed_files, nil)
149
+ else
150
+ files = Runner::FailedFiles.new(result.stdout, result.stderr).to_a
151
+ @history.set(key, :last_failed_files, files) if files.any?
152
+ end
153
+ end
154
+
155
+ # Resolves which files this tool should process
156
+ # @param files [Array<String>] the input files to resolve
157
+ #
158
+ # @return [Array<String>] files after mapping and filtering
159
+ def resolve_files(files)
160
+ file_resolver.resolve(files)
161
+ end
162
+
163
+ # Determines if this tool should be skipped because files were requested but none match
164
+ # @param files [Array<String>] the requested files
165
+ #
166
+ # @return [Boolean] true if files were requested but none match after resolution
167
+ def skip_files?(files)
168
+ file_resolver.skip?(files)
169
+ end
170
+
171
+ private
172
+
173
+ def file_resolver
174
+ @file_resolver ||= FileResolver.new(settings)
175
+ end
135
176
  end
136
177
  end
@@ -7,37 +7,39 @@ module Reviewer
7
7
  include Enumerable
8
8
 
9
9
  # Provides an instance to work with for knowing which tools to run in a given context.
10
- # @param tags: nil [Array] the tags to use to filter tools for a run
11
- # @param tool_names: nil [type] the explicitly provided tool names to filter tools for a run
10
+ # @param tags [Array] the tags to use to filter tools for a run
11
+ # @param tool_names [Array<String>] the explicitly provided tool names to filter tools for a run
12
+ # @param arguments [Arguments] the parsed CLI arguments
13
+ # @param history [History] the history store for status persistence
14
+ # @param config_file [Pathname, nil] path to the .reviewer.yml configuration file
12
15
  #
13
16
  # @return [Reviewer::Tools] collection of tools based on the current run context
14
- def initialize(tags: nil, tool_names: nil)
15
- @tags = tags
16
- @tool_names = tool_names
17
+ def initialize(tags: nil, tool_names: nil, arguments: nil, history: nil, config_file: nil)
18
+ @tags = tags
19
+ @tool_names = tool_names
20
+ @arguments = arguments
21
+ @history = history
22
+ @config_file = config_file
17
23
  end
18
24
 
19
25
  # The current state of all available configured tools regardless of whether they are disabled
20
26
  #
21
27
  # @return [Hash] hash representing all of the configured tools
22
- def to_h
23
- configured
24
- end
28
+ def to_h = configured
25
29
  alias inspect to_h
26
30
 
27
31
  # Provides a collection of all configured tools instantiated as Tool instances
28
32
  #
29
33
  # @return [Array<Tool>] the full collection of all Tool instances
30
34
  def all
31
- configured.keys.map { |tool_name| Tool.new(tool_name) }
35
+ configured.map { |tool_name, config| Tool.new(tool_name, config: config, history: @history) }
32
36
  end
33
37
  alias to_a all
34
38
 
35
- # Provides a collection of all enabled tools instantiated as Tool instances
39
+ # Provides a collection of all tools that run in the default batch
36
40
  #
37
- # @return [Array<Tool>] the full collection of all enabled Tool instances
38
- def enabled
39
- @enabled ||= all.keep_if(&:enabled?)
40
- end
41
+ # @return [Array<Tool>] the full collection of batch-included Tool instances
42
+ def enabled = @enabled ||= all.reject(&:skip_in_batch?)
41
43
 
42
44
  # Provides a collection of all explicitly-specified-via-command-line tools as Tool instances
43
45
  #
@@ -54,13 +56,22 @@ module Reviewer
54
56
  end
55
57
 
56
58
  # Uses the full context of a run to provide the filtered subset of tools to use. It takes into
57
- # consideration: tagged tools, explicitly-specified tools, configuration (enabled/disabled), and
58
- # any other relevant details that should influence whether a specific tool should be run as part
59
- # of the current batch being executed.
59
+ # consideration: tagged tools, explicitly-specified tools, failed tools, configuration
60
+ # (enabled/disabled), and any other relevant details that should influence whether a specific
61
+ # tool should be run as part of the current batch being executed.
60
62
  #
61
63
  # @return [Array<Tool>] the full collection of should-be-used-for-this-run tools
62
64
  def current
63
- subset? ? (specified + tagged).uniq : enabled
65
+ return enabled unless subset? || failed_keyword?
66
+
67
+ (specified + tagged + failed).uniq
68
+ end
69
+
70
+ # Returns tools that failed in the previous run based on history
71
+ #
72
+ # @return [Array<Tool>] tools with :last_status of :failed in history
73
+ def failed_from_history
74
+ all.select { |tool| @history.get(tool.key, :last_status) == :failed }
64
75
  end
65
76
 
66
77
  private
@@ -70,28 +81,31 @@ module Reviewer
70
81
  # only a subset of relevant tools.
71
82
  #
72
83
  # @return [Boolean] true if any tool names or tags are provided via the command line
73
- def subset?
74
- tool_names.any? || tags.any?
75
- end
84
+ def subset? = tool_names.any? || tags.any?
76
85
 
77
- def configured
78
- @configured ||= Loader.configuration
79
- end
86
+ def failed
87
+ return [] unless failed_keyword?
80
88
 
81
- def tags
82
- Array(@tags || Reviewer.arguments.tags)
89
+ failed_from_history
83
90
  end
84
91
 
85
- def tool_names
86
- Array(@tool_names || Reviewer.arguments.keywords.for_tool_names)
87
- end
92
+ def failed_keyword? = @arguments&.keywords&.failed? || false
93
+
94
+ def configured = @configured ||= Configuration::Loader.configuration(file: @config_file)
95
+ def tags = Array(@tags || matching_tags)
96
+ def tool_names = Array(@tool_names || matching_tool_names)
97
+ def tagged?(tool) = tool.matches_tags?(tags)
98
+ def named?(tool) = tool_names.map(&:to_s).include?(tool.key.to_s)
88
99
 
89
- def tagged?(tool)
90
- tool.enabled? && (tags & tool.tags).any?
100
+ def matching_tool_names
101
+ provided = @arguments&.keywords&.provided || []
102
+ provided & configured.keys.map(&:to_s)
91
103
  end
92
104
 
93
- def named?(tool)
94
- tool_names.map(&:to_s).include?(tool.key.to_s)
105
+ def matching_tags
106
+ provided = @arguments&.keywords&.provided || []
107
+ all_tags = enabled.flat_map(&:tags).uniq
108
+ (provided & all_tags) + Array(@arguments&.tags&.raw)
95
109
  end
96
110
  end
97
111
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reviewer
4
- VERSION = '0.1.4'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/reviewer.rb CHANGED
@@ -2,89 +2,85 @@
2
2
 
3
3
  require 'benchmark'
4
4
  require 'forwardable'
5
- require 'logger'
5
+ # require 'logger'
6
+ require 'rainbow'
6
7
 
7
- require_relative 'reviewer/conversions'
8
+ require_relative 'reviewer/configuration'
9
+ require_relative 'reviewer/configuration/loader'
10
+ require_relative 'reviewer/context'
11
+ require_relative 'reviewer/history'
12
+ require_relative 'reviewer/tool'
13
+ require_relative 'reviewer/tools'
8
14
 
9
15
  require_relative 'reviewer/arguments'
10
16
  require_relative 'reviewer/batch'
17
+ require_relative 'reviewer/capabilities'
11
18
  require_relative 'reviewer/command'
12
- require_relative 'reviewer/configuration'
13
- require_relative 'reviewer/guidance'
14
- require_relative 'reviewer/history'
15
- require_relative 'reviewer/keywords'
16
- require_relative 'reviewer/loader'
17
19
  require_relative 'reviewer/output'
18
- require_relative 'reviewer/printer'
20
+ require_relative 'reviewer/prompt'
21
+ require_relative 'reviewer/report'
19
22
  require_relative 'reviewer/runner'
23
+ require_relative 'reviewer/session'
20
24
  require_relative 'reviewer/shell'
21
- require_relative 'reviewer/tool'
22
- require_relative 'reviewer/tools'
25
+ require_relative 'reviewer/setup'
26
+ require_relative 'reviewer/doctor'
23
27
  require_relative 'reviewer/version'
24
28
 
25
29
  # Primary interface for the reviewer tools
26
30
  module Reviewer
31
+ # Base error class for all Reviewer errors
27
32
  class Error < StandardError; end
28
33
 
29
34
  class << self
30
- # Resets the loaded tools
31
- def reset!
32
- @tools = nil
33
- end
35
+ # Resets the loaded tools and arguments
36
+ def reset! = @tools = @arguments = @prompt = @output = @history = @configuration = nil
34
37
 
35
38
  # Runs the `review` command for the specified tools/files. Reviewer expects all configured
36
- # commands that are not disabled to have an entry for the `review` command.
37
- # @param clear_streen [boolean] clears the screen to reduce noise when true
39
+ # commands that are not disabled to have an entry for the `review` command.
38
40
  #
39
41
  # @return [void] Prints output to the console
40
- def review(clear_screen: false)
41
- perform(:review, clear_screen: clear_screen)
42
+ def review
43
+ handle_early_exits { exit build_session.review }
42
44
  end
43
45
 
44
46
  # Runs the `format` command for the specified tools/files for which it is configured.
45
- # @param clear_streen [boolean] clears the screen to reduce noise when true
46
47
  #
47
48
  # @return [void] Prints output to the console
48
- def format(clear_screen: false)
49
- perform(:format, clear_screen: clear_screen)
49
+ def format
50
+ handle_early_exits { exit build_session.format }
50
51
  end
51
52
 
52
53
  # The collection of arguments that were passed via the command line.
53
54
  #
54
55
  # @return [Reviewer::Arguments] exposes tags, files, and keywords from arguments
55
- def arguments
56
- @arguments ||= Arguments.new
57
- end
56
+ def arguments = @arguments ||= Arguments.new
58
57
 
59
58
  # An interface for the collection of configured tools for accessing subsets of tools
60
- # based on enabled/disabled, tags, keywords, etc.
59
+ # based on enabled/disabled, tags, keywords, etc.
61
60
  #
62
61
  # @return [Reviewer::Tools] exposes the set of tools to be run in a given context
63
- def tools
64
- @tools ||= Tools.new
65
- end
62
+ def tools = @tools ||= Tools.new(arguments: arguments, history: history, config_file: configuration.file)
66
63
 
67
64
  # The primary output method for Reviewer to consistently display success/failure details for a
68
- # unique run of each tool and the collective summary when relevant.
65
+ # unique run of each tool and the collective summary when relevant.
69
66
  #
70
67
  # @return [Reviewer::Output] prints formatted output to the console.
71
- def output
72
- @output ||= Output.new
73
- end
68
+ def output = @output ||= Output.new
74
69
 
75
70
  # A file store for sharing information across runs
76
71
  #
77
72
  # @return [Reviewer::History] a YAML::Store (or Pstore) containing data on tools
78
- def history
79
- @history ||= History.new
80
- end
73
+ def history = @history ||= History.new(file: configuration.history_file)
74
+
75
+ # An interactive prompt for yes/no questions
76
+ #
77
+ # @return [Reviewer::Prompt] prompt instance wrapping stdin/stdout
78
+ def prompt = @prompt ||= Prompt.new
81
79
 
82
80
  # Exposes the configuration options for Reviewer.
83
81
  #
84
82
  # @return [Reviewer::Configuration] configuration settings instance
85
- def configuration
86
- @configuration ||= Configuration.new
87
- end
83
+ def configuration = @configuration ||= Configuration.new
88
84
 
89
85
  # A block approach to configuring Reviewer.
90
86
  #
@@ -94,27 +90,91 @@ module Reviewer
94
90
  # end
95
91
  #
96
92
  # @return [Reviewer::Configuration] Reviewer configuration settings
97
- def configure
98
- yield(configuration)
99
- end
93
+ def configure = yield(configuration)
100
94
 
101
95
  private
102
96
 
103
- # Provides a consistent approach to running and benchmarking commmands and preventing further
104
- # execution of later tools if a command fails.
105
- # @param command_type [Symbol] the specific command to run for each tool
106
- #
107
- # @example Run the `review` command for each relevant tool
108
- # perform(:review)
109
- #
110
- # @return [Hash] the exit status (in integer format) for each command run
111
- def perform(command_type, clear_screen: false)
112
- system('clear') if clear_screen
97
+ def handle_early_exits
98
+ return show_help if arguments.help?
99
+ return show_version if arguments.version?
100
+ return Setup.run if subcommand?(:init)
101
+ return run_doctor if subcommand?(:doctor)
102
+ return run_capabilities if capabilities_flag?
103
+ return run_first_time_setup unless configuration.file.exist?
104
+
105
+ yield
106
+ end
113
107
 
114
- results = Batch.run(command_type, tools.current)
108
+ def subcommand?(name) = ARGV.first == name.to_s
109
+ def capabilities_flag? = ARGV.include?('--capabilities') || ARGV.include?('-c')
110
+
111
+ def show_help
112
+ output.help(help_text)
113
+ end
114
+
115
+ def help_text
116
+ <<~HELP
117
+ Usage: rvw [tool or tag ...] [keyword ...] [options]
118
+ fmt [tool or tag ...] [keyword ...] [options]
119
+
120
+ Commands:
121
+ rvw Run all enabled tools
122
+ rvw <tool> Run a single tool by its config key
123
+ rvw <tag> Run tools matching a tag
124
+ fmt Auto-fix with format commands
125
+ rvw init Generate .reviewer.yml from Gemfile.lock
126
+ rvw doctor Check configuration and tool health
127
+
128
+ Keywords:
129
+ staged Files staged for commit
130
+ unstaged Files with unstaged changes
131
+ modified All changed files (staged + unstaged)
132
+ untracked New files not yet tracked by git
133
+ failed Re-run only tools that failed last time
134
+
135
+ Options:
136
+ #{slop_options}
137
+
138
+ Examples:
139
+ rvw rubocop Run RuboCop only
140
+ rvw staged Review staged files across all tools
141
+ rvw -t security modified Run security-tagged tools on changed files
142
+ rvw tests -f test/user_test.rb Run tests on a specific file
143
+ rvw failed Re-run what failed last time
144
+ fmt rubocop Auto-fix with RuboCop
145
+ HELP
146
+ end
147
+
148
+ def slop_options
149
+ arguments.options.to_s.lines.drop(1).join.rstrip
150
+ end
151
+
152
+ def show_version
153
+ output.help(VERSION)
154
+ end
155
+
156
+ def run_capabilities
157
+ puts Capabilities.new(tools: tools).to_json
158
+ end
159
+
160
+ def run_doctor
161
+ report = Doctor.run(configuration: configuration, tools: tools)
162
+ Doctor::Formatter.new(output).print(report)
163
+ end
164
+
165
+ def run_first_time_setup
166
+ formatter = Setup::Formatter.new(output)
167
+ formatter.first_run_greeting
168
+ if prompt.yes?('Would you like to set it up now?')
169
+ Setup.run(configuration: configuration)
170
+ else
171
+ formatter.first_run_skip
172
+ end
173
+ end
115
174
 
116
- # Return the largest exit status
117
- exit results.values.max
175
+ def build_session
176
+ context = Context.new(arguments: arguments, output: output, history: history)
177
+ Session.new(context: context, tools: tools)
118
178
  end
119
179
  end
120
180
  end