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
@@ -8,81 +8,125 @@ module Reviewer
8
8
 
9
9
  alias key tool_key
10
10
 
11
- # Creates an instance of settings for retrieving values from the configuration file.
11
+ # Creates an instance of settings for retrieving values from the configuration file
12
12
  # @param tool_key [Symbol] the unique identifier for the tool in the config file
13
- # @param config: nil [Hash] the configuration values to examine for the settings
13
+ # @param config [Hash] the configuration values to examine for the settings
14
14
  #
15
- # @return [self]
16
- def initialize(tool_key, config: nil)
15
+ # @return [Settings]
16
+ def initialize(tool_key, config:)
17
17
  @tool_key = tool_key.to_sym
18
- @config = config || load_config
18
+ @config = config
19
19
  end
20
20
 
21
- def hash
22
- state.hash
23
- end
21
+ # Returns a hash code for comparing settings instances
22
+ #
23
+ # @return [Integer] hash code based on configuration state
24
+ def hash = state.hash
24
25
 
26
+ # Compares two settings instances for equality based on their configuration
27
+ # @param other [Settings] the settings to compare against
28
+ # @return [Boolean] true if both have the same configuration
25
29
  def eql?(other)
26
30
  self.class == other.class &&
27
31
  state == other.state
28
32
  end
29
33
  alias :== eql?
30
34
 
31
- def disabled?
32
- config.fetch(:disabled, false)
35
+ def skip_in_batch?
36
+ if config.key?(:skip_in_batch)
37
+ config.fetch(:skip_in_batch) { false }
38
+ else
39
+ config.fetch(:disabled) { false }
40
+ end
33
41
  end
34
42
 
35
- def enabled?
36
- !disabled?
37
- end
43
+ def disabled? = skip_in_batch?
44
+ def enabled? = !skip_in_batch?
38
45
 
39
- def name
40
- config.fetch(:name) { tool_key.to_s.capitalize }
41
- end
46
+ # The human-readable name of the tool
47
+ #
48
+ # @return [String] the configured name or capitalized tool key
49
+ def name = config.fetch(:name) { tool_key.to_s.capitalize }
42
50
 
43
- def description
44
- config.fetch(:description) { "(No description provided for '#{name}')" }
45
- end
51
+ # The human-readable description of what the tool does
52
+ #
53
+ # @return [String] the configured description or a default placeholder
54
+ def description = config.fetch(:description) { "(No description provided for '#{name}')" }
46
55
 
47
- def tags
48
- config.fetch(:tags) { [] }
49
- end
56
+ # The tags used to categorize and filter the tool
57
+ #
58
+ # @return [Array<String>] configured tags or empty array
59
+ def tags = config.fetch(:tags) { [] }
50
60
 
51
- def links
52
- config.fetch(:links) { {} }
53
- end
61
+ # The collection of reference links for the tool (home, install, usage, etc.)
62
+ #
63
+ # @return [Hash] configured links or empty hash if none
64
+ def links = config.fetch(:links) { {} }
54
65
 
55
- def env
56
- config.fetch(:env) { {} }
57
- end
66
+ # The environment variables to set when running the tool
67
+ #
68
+ # @return [Hash] configured env vars or empty hash
69
+ def env = config.fetch(:env) { {} }
58
70
 
59
- def flags
60
- config.fetch(:flags) { {} }
61
- end
71
+ # The CLI flags to pass to the tool's review command
72
+ #
73
+ # @return [Hash] configured flags or empty hash
74
+ def flags = config.fetch(:flags) { {} }
75
+
76
+ # The CLI flag used to pass files to the tool (e.g., '--files')
77
+ #
78
+ # @return [String] the configured flag or empty string if files are passed directly
79
+ def files_flag = config.dig(:files, :flag) || ''
80
+
81
+ # The separator used to join multiple file paths in the command
82
+ #
83
+ # @return [String] the configured separator or a space by default
84
+ def files_separator = config.dig(:files, :separator) || ' '
85
+
86
+ # The glob pattern used to filter which files this tool should process
87
+ #
88
+ # @return [String, nil] the pattern (e.g., '*.rb') or nil if not configured
89
+ def files_pattern = config.dig(:files, :pattern)
90
+
91
+ # The test framework to use for mapping source files to test files
92
+ #
93
+ # @return [String, nil] the framework name ('minitest' or 'rspec') or nil if not configured
94
+ def map_to_tests = config.dig(:files, :map_to_tests)
95
+
96
+ def supports_files? = config.key?(:files)
97
+
98
+ # The regex pattern for extracting a summary detail from tool output
99
+ #
100
+ # @return [String, nil] the configured pattern or nil
101
+ def summary_pattern = config.dig(:summary, :pattern)
102
+
103
+ # The label template for displaying the extracted summary detail
104
+ #
105
+ # @return [String, nil] the configured label or nil
106
+ def summary_label = config.dig(:summary, :label)
107
+
108
+ # Returns the file-scoped command override for a given command type.
109
+ # When configured, this command replaces the standard command when files are passed.
110
+ #
111
+ # @param command_type [Symbol] the command type (:review, :format)
112
+ # @return [String, nil] the file-scoped command or nil if not configured
113
+ def files_command(command_type) = config.dig(:files, command_type)
62
114
 
63
115
  # The collection of configured commands for the tool
64
116
  #
65
117
  # @return [Hash] all of the commands configured for the tool
66
- def commands
67
- config.fetch(:commands) { {} }
68
- end
118
+ def commands = config.fetch(:commands) { {} }
69
119
 
70
120
  # The largest exit status that can still be considered a success for the command
71
121
  #
72
122
  # @return [Integer] the configured `max_exit_status` for the tool or 0 if one isn't configured
73
- def max_exit_status
74
- commands.fetch(:max_exit_status, 0)
75
- end
123
+ def max_exit_status = commands.fetch(:max_exit_status) { 0 }
76
124
 
77
125
  protected
78
126
 
79
- def state
80
- config.to_hash
81
- end
82
-
83
- def load_config
84
- Reviewer.tools.to_h.fetch(key) { {} }
85
- end
127
+ # Returns the configuration as a plain hash for comparison
128
+ # @return [Hash] the configuration state
129
+ def state = config.to_hash
86
130
  end
87
131
  end
88
132
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ class Tool
5
+ # Maps source files to their corresponding test files based on framework conventions.
6
+ class TestFileMapper
7
+ FRAMEWORKS = {
8
+ minitest: { dir: 'test', suffix: '_test.rb', source_dirs: %w[app lib] },
9
+ rspec: { dir: 'spec', suffix: '_spec.rb', source_dirs: %w[app lib] }
10
+ }.freeze
11
+
12
+ # Creates a mapper for the specified test framework
13
+ # @param framework [Symbol, String, nil] the test framework (:minitest or :rspec)
14
+ #
15
+ # @return [TestFileMapper] a mapper instance for the framework
16
+ def initialize(framework)
17
+ @framework = framework&.to_sym
18
+ end
19
+
20
+ # Maps source files to their corresponding test files
21
+ # @param files [Array<String>] source files to map
22
+ #
23
+ # @return [Array<String>] mapped test files (only those that exist on disk)
24
+ def map(files)
25
+ return files unless supported?
26
+
27
+ files.map { |file| map_file(file) }.compact.uniq
28
+ end
29
+
30
+ # Checks if the framework is supported for mapping
31
+ #
32
+ # @return [Boolean] true if the framework is :minitest or :rspec
33
+ def supported?
34
+ @framework && FRAMEWORKS.key?(@framework)
35
+ end
36
+
37
+ private
38
+
39
+ def map_file(file)
40
+ return file if test_file?(file)
41
+
42
+ mapped = source_to_test(file)
43
+ mapped && File.exist?(mapped) ? mapped : nil
44
+ end
45
+
46
+ def test_file?(file)
47
+ file.end_with?(config[:suffix])
48
+ end
49
+
50
+ def source_to_test(file)
51
+ return nil unless file.end_with?('.rb')
52
+
53
+ replace_source_dir_with_test_dir(file).sub(/\.rb$/, config[:suffix])
54
+ end
55
+
56
+ def replace_source_dir_with_test_dir(path)
57
+ config[:source_dirs].each do |dir|
58
+ return path.sub(%r{^#{dir}/}, "#{config[:dir]}/") if path.start_with?("#{dir}/")
59
+ end
60
+ prepend_test_dir(path)
61
+ end
62
+
63
+ def prepend_test_dir(path)
64
+ dir = config[:dir]
65
+ path.start_with?(dir) ? path : "#{dir}/#{path}"
66
+ end
67
+
68
+ def config
69
+ FRAMEWORKS[@framework]
70
+ end
71
+ end
72
+ end
73
+ end
@@ -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,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
-
3
+ require_relative 'tool/conversions'
4
+ require_relative 'tool/file_resolver'
5
5
  require_relative 'tool/settings'
6
+ require_relative 'tool/test_file_mapper'
7
+ require_relative 'tool/timing'
6
8
 
7
9
  module Reviewer
8
10
  # Provides an instance of a specific tool for accessing its settings and run history
@@ -10,14 +12,9 @@ module Reviewer
10
12
  extend Forwardable
11
13
  include Comparable
12
14
 
13
- # In general, Reviewer tries to save time where it can. In the case of the "prepare" command
14
- # used by some tools to retrieve data, it only runs it occasionally in order to save time.
15
- # This is the default window that it uses to determine if the tool's preparation step should be
16
- # considered stale and needs to be rerun. Frequent enough that it shouldn't get stale, but
17
- # infrequent enough that it's not cumbersome.
18
- SIX_HOURS_IN_SECONDS = 60 * 60 * 6
15
+ SIX_HOURS_IN_SECONDS = Timing::SIX_HOURS_IN_SECONDS
19
16
 
20
- attr_reader :settings, :history
17
+ attr_reader :settings
21
18
 
22
19
  def_delegators :@settings,
23
20
  :key,
@@ -29,98 +26,86 @@ module Reviewer
29
26
  :links,
30
27
  :enabled?,
31
28
  :disabled?,
32
- :max_exit_status
29
+ :skip_in_batch?,
30
+ :max_exit_status,
31
+ :supports_files?
32
+
33
+ # Returns the tool's key as a symbol
34
+ # @return [Symbol] the tool's unique identifier
35
+ def to_sym = key
33
36
 
34
- alias to_sym key
35
- alias to_s name
37
+ # Returns the tool's name as a string
38
+ # @return [String] the tool's display name
39
+ def to_s = name
36
40
 
37
41
  # Create an instance of a tool
38
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
39
45
  #
40
46
  # @return [Tool] an instance of tool for accessing settings information and facts about the tool
41
- def initialize(tool_key)
42
- @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)
43
51
  end
44
52
 
45
- # For determining if the tool should run it's prepration command. It will only be run both if
46
- # 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
47
55
  #
48
56
  # @return [Boolean] true if the tool has a configured `prepare` command that hasn't been run in
49
57
  # the last 6 hours
50
- def prepare?
51
- preparable? && stale?
52
- end
58
+ def prepare? = preparable? && stale?
53
59
 
54
60
  # Determines whether a tool has a specific command type configured
55
61
  # @param command_type [Symbol] one of the available command types defined in Command::TYPES
56
62
  #
57
63
  # @return [Boolean] true if the command type is configured and not blank
58
64
  def command?(command_type)
59
- commands.key?(command_type) && !commands[command_type].nil?
65
+ commands.key?(command_type) && commands[command_type]
60
66
  end
61
67
 
62
68
  # Determines if the tool can run a `install` command
63
69
  #
64
70
  # @return [Boolean] true if there is a non-blank `install` command configured
65
- def installable?
66
- 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]
67
78
  end
68
79
 
69
80
  # Determines if the tool can run a `prepare` command
70
81
  #
71
82
  # @return [Boolean] true if there is a non-blank `prepare` command configured
72
- def preparable?
73
- command?(:prepare)
74
- end
83
+ def preparable? = command?(:prepare)
75
84
 
76
85
  # Determines if the tool can run a `review` command
77
86
  #
78
87
  # @return [Boolean] true if there is a non-blank `review` command configured
79
- def reviewable?
80
- command?(:review)
81
- end
88
+ def reviewable? = command?(:review)
82
89
 
83
90
  # Determines if the tool can run a `format` command
84
91
  #
85
92
  # @return [Boolean] true if there is a non-blank `format` command configured
86
- def formattable?
87
- command?(:format)
88
- end
89
-
90
- # Specifies when the tool last had it's `prepare` command run
91
- #
92
- # @return [Time] timestamp of when the `prepare` command was last run
93
- def last_prepared_at
94
- date_string = Reviewer.history.get(key, :last_prepared_at)
95
-
96
- date_string == '' || date_string.nil? ? nil : DateTime.parse(date_string).to_time
97
- end
93
+ def formattable? = command?(:format)
98
94
 
99
- # Sets the timestamp for when the tool last ran its `prepare` command
100
- # @param last_prepared_at [DateTime] the value to record for when the `prepare` command last ran
95
+ # Whether this tool matches any of the given tags and is eligible for batch runs
101
96
  #
102
- # @return [DateTime] timestamp of when the `prepare` command was last run
103
- def last_prepared_at=(last_prepared_at)
104
- Reviewer.history.set(key, :last_prepared_at, last_prepared_at.to_s)
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)
105
101
  end
106
102
 
107
- def average_time(command)
108
- times = get_timing(command)
109
-
110
- times.any? ? times.sum / times.size : 0
111
- end
112
-
113
- def get_timing(command)
114
- Reviewer.history.get(key, command.raw_string) || []
115
- end
116
-
117
- def record_timing(command, time)
118
- return if time.nil?
119
-
120
- timing = get_timing(command).take(4) << time.round(2)
121
-
122
- Reviewer.history.set(key, command.raw_string, timing)
123
- end
103
+ def_delegators :@timing,
104
+ :last_prepared_at,
105
+ :last_prepared_at=,
106
+ :average_time,
107
+ :get_timing,
108
+ :record_timing
124
109
 
125
110
  # Determines whether the `prepare` command was run recently enough
126
111
  #
@@ -129,22 +114,18 @@ module Reviewer
129
114
  def stale?
130
115
  return false unless preparable?
131
116
 
132
- last_prepared_at.nil? || last_prepared_at < Time.now - SIX_HOURS_IN_SECONDS
117
+ @timing.stale?
133
118
  end
134
119
 
135
120
  # Convenience method for determining if a tool has a configured install link
136
121
  #
137
122
  # @return [Boolean] true if there is an `install` key under links and the value isn't blank
138
- def install_link?
139
- links.key?(:install) && !links[:install].nil?
140
- end
123
+ def install_link? = links.key?(:install) && !!links[:install]
141
124
 
142
125
  # Returns the text for the install link if available
143
126
  #
144
127
  # @return [String, nil] the link if it exists, nil otherwise
145
- def install_link
146
- install_link? ? links.fetch(:install) : nil
147
- end
128
+ def install_link = install_link? ? links.fetch(:install) : nil
148
129
 
149
130
  # Determines if two tools are equal
150
131
  # @param other [Tool] the tool to compare to the current instance
@@ -154,5 +135,43 @@ module Reviewer
154
135
  settings == other.settings
155
136
  end
156
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
157
176
  end
158
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