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.
- checksums.yaml +4 -4
- data/.alexignore +1 -0
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +81 -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 +27 -12
- data/.reviewer.future.yml +221 -0
- data/.reviewer.yml +191 -28
- data/.reviewer_stdout +0 -0
- data/.rubocop.yml +34 -1
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +294 -72
- data/README.md +315 -7
- 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 +60 -27
- data/lib/reviewer/arguments/keywords.rb +39 -43
- data/lib/reviewer/arguments/tags.rb +21 -14
- data/lib/reviewer/arguments.rb +107 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +46 -35
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +16 -6
- data/lib/reviewer/command/string/flags.rb +14 -5
- data/lib/reviewer/command/string.rb +53 -24
- data/lib/reviewer/command.rb +69 -39
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +14 -4
- 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 +36 -12
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +105 -0
- data/lib/reviewer/output.rb +54 -65
- 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 +232 -0
- data/lib/reviewer/runner/strategies/{verbose.rb → passthrough.rb} +15 -24
- data/lib/reviewer/runner.rb +179 -35
- 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 +25 -11
- data/lib/reviewer/shell/timer.rb +47 -27
- data/lib/reviewer/shell.rb +46 -21
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +107 -56
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -47
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +114 -54
- data/reviewer.gemspec +21 -20
- data/structure.svg +1 -0
- metadata +113 -148
- data/.ruby-version +0 -1
- data/lib/reviewer/command/string/verbosity.rb +0 -51
- data/lib/reviewer/command/verbosity.rb +0 -65
- data/lib/reviewer/conversions.rb +0 -27
- data/lib/reviewer/guidance.rb +0 -73
- data/lib/reviewer/keywords/git/staged.rb +0 -48
- 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/printer.rb +0 -25
- data/lib/reviewer/runner/strategies/quiet.rb +0 -90
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
# Bundles the shared runtime dependencies that flow through the review/format lifecycle.
|
|
5
|
+
# Passed from Session → Batch → Runner → Command so that no class needs to reach
|
|
6
|
+
# into module-level globals for arguments, output, or history.
|
|
7
|
+
#
|
|
8
|
+
# @!attribute [rw] arguments
|
|
9
|
+
# @return [Arguments] the parsed command-line arguments
|
|
10
|
+
# @!attribute [rw] output
|
|
11
|
+
# @return [Output] the output channel for displaying content
|
|
12
|
+
# @!attribute [rw] history
|
|
13
|
+
# @return [History] the YAML store for timing data and prepare timestamps
|
|
14
|
+
Context = Struct.new(:arguments, :output, :history, keyword_init: true)
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Validates the configuration file by delegating to Configuration::Loader
|
|
6
|
+
class ConfigCheck
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
# Creates a config check that validates the .reviewer.yml file
|
|
10
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
11
|
+
# @param configuration [Configuration] the configuration to validate
|
|
12
|
+
#
|
|
13
|
+
# @return [ConfigCheck]
|
|
14
|
+
def initialize(report, configuration:)
|
|
15
|
+
@report = report
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks for .reviewer.yml existence and validity
|
|
20
|
+
def check
|
|
21
|
+
config_file = @configuration.file
|
|
22
|
+
|
|
23
|
+
unless config_file.exist?
|
|
24
|
+
report.add(:configuration, status: :error,
|
|
25
|
+
message: 'No .reviewer.yml found', detail: 'Run `rvw init` to generate one')
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
report.add(:configuration, status: :ok, message: '.reviewer.yml found')
|
|
30
|
+
validate_via_loader
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Exercises the full Configuration::Loader pipeline (parse + validate) to surface config errors
|
|
36
|
+
def validate_via_loader
|
|
37
|
+
Configuration::Loader.configuration(file: @configuration.file)
|
|
38
|
+
report.add(:configuration, status: :ok, message: 'Configuration is valid')
|
|
39
|
+
rescue Configuration::Loader::InvalidConfigurationError => e
|
|
40
|
+
report.add(:configuration, status: :error, message: 'YAML syntax error', detail: e.message)
|
|
41
|
+
rescue Configuration::Loader::MissingReviewCommandError => e
|
|
42
|
+
report.add(:configuration, status: :error, message: 'Missing review command', detail: e.message)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
module Doctor
|
|
7
|
+
# Checks environment prerequisites (git, Ruby version)
|
|
8
|
+
class EnvironmentCheck
|
|
9
|
+
attr_reader :report
|
|
10
|
+
|
|
11
|
+
# Creates an environment checker for Ruby and git availability
|
|
12
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
13
|
+
#
|
|
14
|
+
# @return [EnvironmentCheck]
|
|
15
|
+
def initialize(report)
|
|
16
|
+
@report = report
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks Ruby version and git availability
|
|
20
|
+
def check
|
|
21
|
+
check_ruby_version
|
|
22
|
+
check_git
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def check_ruby_version
|
|
28
|
+
report.add(:environment, status: :ok, message: "Ruby #{RUBY_VERSION}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check_git
|
|
32
|
+
stdout, _stderr, status = Open3.capture3('git --version')
|
|
33
|
+
|
|
34
|
+
unless status.success?
|
|
35
|
+
report.add(:environment, status: :warning,
|
|
36
|
+
message: 'Git not available',
|
|
37
|
+
detail: 'Git keywords (staged, modified, etc.) require git')
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
report.add(:environment, status: :ok, message: stdout.strip)
|
|
42
|
+
check_git_repo
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def check_git_repo
|
|
46
|
+
_stdout, _stderr, status = Open3.capture3('git rev-parse --git-dir')
|
|
47
|
+
|
|
48
|
+
if status.success?
|
|
49
|
+
report.add(:environment, status: :ok, message: 'Inside a git repository')
|
|
50
|
+
else
|
|
51
|
+
report.add(:environment, status: :warning,
|
|
52
|
+
message: 'Not inside a git repository',
|
|
53
|
+
detail: 'Git keywords (staged, modified, etc.) will not work')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output/formatting'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
module Doctor
|
|
7
|
+
# Display logic for diagnostic reports
|
|
8
|
+
class Formatter
|
|
9
|
+
include Output::Formatting
|
|
10
|
+
|
|
11
|
+
attr_reader :output, :printer
|
|
12
|
+
private :output, :printer
|
|
13
|
+
|
|
14
|
+
SYMBOLS = { ok: "\u2713", warning: '!', error: "\u2717", info: "\u00b7", muted: "\u00b7" }.freeze
|
|
15
|
+
STYLES = { ok: :success, warning: :warning, error: :failure, info: :muted, muted: :muted }.freeze
|
|
16
|
+
|
|
17
|
+
SECTION_LABELS = {
|
|
18
|
+
configuration: 'Configuration',
|
|
19
|
+
tools: 'Tools',
|
|
20
|
+
opportunities: 'Opportunities',
|
|
21
|
+
environment: 'Environment'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Creates a formatter for diagnostic report display
|
|
25
|
+
# @param output [Output] the console output handler
|
|
26
|
+
#
|
|
27
|
+
# @return [Formatter]
|
|
28
|
+
def initialize(output)
|
|
29
|
+
@output = output
|
|
30
|
+
@printer = output.printer
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Renders a full diagnostic report
|
|
34
|
+
# @param report [Doctor::Report] the report to display
|
|
35
|
+
def print(report)
|
|
36
|
+
output.newline
|
|
37
|
+
Doctor::Report::SECTIONS.each do |section|
|
|
38
|
+
findings = report.section(section)
|
|
39
|
+
print_section(section, findings) if findings.any?
|
|
40
|
+
end
|
|
41
|
+
print_summary(report)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def print_section(section, findings)
|
|
47
|
+
printer.puts(:bold, SECTION_LABELS.fetch(section) { section.to_s.capitalize })
|
|
48
|
+
findings.each { |finding| print_finding(finding) }
|
|
49
|
+
output.newline
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def print_finding(finding)
|
|
53
|
+
status = finding.status
|
|
54
|
+
message = finding.message
|
|
55
|
+
detail = finding.detail
|
|
56
|
+
|
|
57
|
+
symbol = SYMBOLS.fetch(status) { ' ' }
|
|
58
|
+
style = STYLES.fetch(status) { :default }
|
|
59
|
+
|
|
60
|
+
printer.print(style, " #{symbol} ")
|
|
61
|
+
printer.puts(:default, message)
|
|
62
|
+
printer.puts(:muted, " #{detail}") if detail
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def print_summary(report)
|
|
66
|
+
if report.ok?
|
|
67
|
+
printer.puts(:success, 'No issues found')
|
|
68
|
+
else
|
|
69
|
+
printer.puts(:failure, pluralize(report.errors.size, 'issue found', 'issues found'))
|
|
70
|
+
end
|
|
71
|
+
output.newline
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Detects conflicts between configured tool names, tags, and reserved keywords
|
|
6
|
+
class KeywordCheck
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
RESERVED = Arguments::Keywords::RESERVED
|
|
10
|
+
|
|
11
|
+
# Creates a keyword check that scans for keyword conflicts in configuration
|
|
12
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
13
|
+
# @param configuration [Configuration] the configuration to check
|
|
14
|
+
# @param tools [Tools] the tools collection to analyze
|
|
15
|
+
#
|
|
16
|
+
# @return [KeywordCheck]
|
|
17
|
+
def initialize(report, configuration:, tools:)
|
|
18
|
+
@report = report
|
|
19
|
+
@configuration = configuration
|
|
20
|
+
@tools = tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Checks for keyword conflicts between tool names, tags, and reserved keywords
|
|
24
|
+
def check
|
|
25
|
+
return unless @configuration.file.exist?
|
|
26
|
+
|
|
27
|
+
check_tool_names_vs_reserved
|
|
28
|
+
check_tags_vs_reserved
|
|
29
|
+
check_tool_names_vs_tags
|
|
30
|
+
rescue Configuration::Loader::MissingConfigurationError
|
|
31
|
+
# Tools may reference a stale config path — nothing to check
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def all_tools = @all_tools ||= @tools.all
|
|
37
|
+
|
|
38
|
+
def tool_names = all_tools.map { |tool| tool.key.to_s }
|
|
39
|
+
|
|
40
|
+
def all_tags = all_tools.flat_map(&:tags).uniq
|
|
41
|
+
|
|
42
|
+
# Names of tools that use a given tag
|
|
43
|
+
def tools_with_tag(tag)
|
|
44
|
+
all_tools.select { |tool| tool.tags.include?(tag) }.map(&:name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Tags that belong to tools OTHER than the named tool
|
|
48
|
+
def tags_from_other_tools(tool_name)
|
|
49
|
+
all_tools.reject { |tool| tool.key.to_s == tool_name }.flat_map(&:tags).uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_tool_names_vs_reserved
|
|
53
|
+
(tool_names & RESERVED).each do |name|
|
|
54
|
+
report.add(:configuration,
|
|
55
|
+
status: :warning,
|
|
56
|
+
message: "Tool name '#{name}' shadows reserved keyword '#{name}'",
|
|
57
|
+
detail: "Reserved keywords (#{RESERVED.join(', ')}) trigger special behavior. " \
|
|
58
|
+
'Rename this tool to avoid unexpected results.')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_tags_vs_reserved
|
|
63
|
+
(all_tags & RESERVED).each do |tag|
|
|
64
|
+
report.add(:configuration,
|
|
65
|
+
status: :warning,
|
|
66
|
+
message: "Tag '#{tag}' shadows reserved keyword '#{tag}' (used by #{tools_with_tag(tag).join(', ')})",
|
|
67
|
+
detail: "Reserved keywords (#{RESERVED.join(', ')}) trigger special behavior. " \
|
|
68
|
+
'Rename this tag or use -t to pass tags explicitly.')
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_tool_names_vs_tags
|
|
73
|
+
tool_names.each do |name|
|
|
74
|
+
next unless tags_from_other_tools(name).include?(name)
|
|
75
|
+
|
|
76
|
+
report.add(:configuration,
|
|
77
|
+
status: :warning,
|
|
78
|
+
message: "Tool name '#{name}' is also a tag on: #{tools_with_tag(name).join(', ')}",
|
|
79
|
+
detail: "'rvw #{name}' will run both the '#{name}' tool and tagged tools. " \
|
|
80
|
+
'Use -t to target tags explicitly if this is unintended.')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Suggests improvements based on current configuration and project state
|
|
6
|
+
class OpportunityCheck
|
|
7
|
+
attr_reader :report, :project_dir
|
|
8
|
+
|
|
9
|
+
# Creates an opportunity checker that scans for unconfigured tools and missing features
|
|
10
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
11
|
+
# @param project_dir [Pathname] the project root for tool detection
|
|
12
|
+
# @param configuration [Configuration] the configuration to check
|
|
13
|
+
# @param tools [Tools] the tools collection to analyze
|
|
14
|
+
#
|
|
15
|
+
# @return [OpportunityCheck]
|
|
16
|
+
def initialize(report, project_dir, configuration:, tools:)
|
|
17
|
+
@report = report
|
|
18
|
+
@project_dir = project_dir
|
|
19
|
+
@configuration = configuration
|
|
20
|
+
@tools = tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Checks for unconfigured tools, missing file targeting, and missing format commands
|
|
24
|
+
def check
|
|
25
|
+
return unless @configuration.file.exist?
|
|
26
|
+
|
|
27
|
+
check_unconfigured_tools
|
|
28
|
+
check_missing_files_config
|
|
29
|
+
check_missing_format_command
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def check_unconfigured_tools
|
|
35
|
+
detected = Setup::Detector.new(project_dir).detect
|
|
36
|
+
configured_keys = @tools.all.map(&:key)
|
|
37
|
+
|
|
38
|
+
detected.each do |result|
|
|
39
|
+
next if configured_keys.include?(result.key)
|
|
40
|
+
|
|
41
|
+
report.add(:opportunities, status: :info,
|
|
42
|
+
message: "#{result.name} detected but not configured",
|
|
43
|
+
detail: result.reasons.join(', '))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_missing_files_config
|
|
48
|
+
@tools.all.each do |tool|
|
|
49
|
+
next if tool.skip_in_batch?
|
|
50
|
+
next if tool.supports_files?
|
|
51
|
+
next unless catalog_supports?(tool.key, :files)
|
|
52
|
+
|
|
53
|
+
report.add(:opportunities, status: :info,
|
|
54
|
+
message: "#{tool.name} has no file targeting configured",
|
|
55
|
+
detail: 'Add a `files` section to enable staged/modified file scoping')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_missing_format_command
|
|
60
|
+
@tools.all.each do |tool|
|
|
61
|
+
next if tool.skip_in_batch?
|
|
62
|
+
next if tool.formattable?
|
|
63
|
+
next unless catalog_supports?(tool.key, :format)
|
|
64
|
+
|
|
65
|
+
report.add(:opportunities, status: :info,
|
|
66
|
+
message: "#{tool.name} has no format command",
|
|
67
|
+
detail: 'Add a `format` command to enable `fmt` support')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns true only if the catalog knows this tool AND the catalog entry
|
|
72
|
+
# includes the given capability (:files or :format command)
|
|
73
|
+
def catalog_supports?(key, capability)
|
|
74
|
+
entry = Setup::Catalog::TOOLS[key]
|
|
75
|
+
return false unless entry
|
|
76
|
+
|
|
77
|
+
case capability
|
|
78
|
+
when :files
|
|
79
|
+
entry.key?(:files)
|
|
80
|
+
when :format
|
|
81
|
+
entry.dig(:commands, :format) ? true : false
|
|
82
|
+
else
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Structured container for diagnostic findings organized by section
|
|
6
|
+
class Report
|
|
7
|
+
# A single diagnostic finding with status, message, and optional detail.
|
|
8
|
+
# @!attribute status [rw]
|
|
9
|
+
# @return [Symbol] the severity (:ok, :warning, :error, or :info)
|
|
10
|
+
# @!attribute message [rw]
|
|
11
|
+
# @return [String] the finding summary
|
|
12
|
+
# @!attribute detail [rw]
|
|
13
|
+
# @return [String, nil] optional detail or guidance text
|
|
14
|
+
Finding = Struct.new(:status, :message, :detail, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
# Ordered list of report sections
|
|
17
|
+
SECTIONS = %i[configuration tools opportunities environment].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :findings
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@findings = Hash.new { |hash, key| hash[key] = [] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Adds a finding to a section
|
|
26
|
+
# @param section [Symbol] one of SECTIONS
|
|
27
|
+
# @param status [Symbol] :ok, :warning, :error, or :info
|
|
28
|
+
# @param message [String] the finding summary
|
|
29
|
+
# @param detail [String, nil] optional detail text
|
|
30
|
+
def add(section, status:, message:, detail: nil)
|
|
31
|
+
findings[section] << Finding.new(status: status, message: message, detail: detail)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether all findings are free of errors
|
|
35
|
+
def ok?
|
|
36
|
+
all_findings.none? { |finding| finding.status == :error }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# All error findings across sections
|
|
40
|
+
def errors
|
|
41
|
+
all_findings.select { |finding| finding.status == :error }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# All warning findings across sections
|
|
45
|
+
def warnings
|
|
46
|
+
all_findings.select { |finding| finding.status == :warning }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Findings for a specific section
|
|
50
|
+
# @param name [Symbol] the section name
|
|
51
|
+
# @return [Array<Finding>] findings for that section
|
|
52
|
+
def section(name)
|
|
53
|
+
findings[name]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def all_findings
|
|
59
|
+
findings.values.flatten
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Doctor
|
|
5
|
+
# Reports the status of each configured tool
|
|
6
|
+
class ToolInventory
|
|
7
|
+
attr_reader :report
|
|
8
|
+
|
|
9
|
+
# Creates a tool inventory check that reports batch/skip status for each tool
|
|
10
|
+
# @param report [Doctor::Report] the report to add findings to
|
|
11
|
+
# @param configuration [Configuration] the configuration to check
|
|
12
|
+
# @param tools [Tools] the tools collection to report on
|
|
13
|
+
#
|
|
14
|
+
# @return [ToolInventory]
|
|
15
|
+
def initialize(report, configuration:, tools:)
|
|
16
|
+
@report = report
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
@tools = tools
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Reports batch/skip status and available commands for each configured tool
|
|
22
|
+
def check
|
|
23
|
+
return unless @configuration.file.exist?
|
|
24
|
+
|
|
25
|
+
@tools.all.each do |tool|
|
|
26
|
+
skipped = tool.skip_in_batch?
|
|
27
|
+
|
|
28
|
+
report.add(:tools,
|
|
29
|
+
status: skipped ? :muted : :ok,
|
|
30
|
+
message: "#{tool.name} (#{tool.key}) — #{command_summary(tool)}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def command_summary(tool)
|
|
37
|
+
%i[review format install prepare].select { |cmd| tool.command?(cmd) }.join(', ')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'doctor/formatter'
|
|
4
|
+
require_relative 'doctor/report'
|
|
5
|
+
require_relative 'doctor/config_check'
|
|
6
|
+
require_relative 'doctor/keyword_check'
|
|
7
|
+
require_relative 'doctor/tool_inventory'
|
|
8
|
+
require_relative 'doctor/opportunity_check'
|
|
9
|
+
require_relative 'doctor/environment_check'
|
|
10
|
+
|
|
11
|
+
module Reviewer
|
|
12
|
+
# Diagnostic module for checking configuration, tools, and environment health
|
|
13
|
+
module Doctor
|
|
14
|
+
# Runs all diagnostic checks and returns a structured report
|
|
15
|
+
# @param project_dir [Pathname] the project root to scan
|
|
16
|
+
#
|
|
17
|
+
# @return [Doctor::Report] the complete diagnostic report
|
|
18
|
+
def self.run(configuration:, tools:, project_dir: Pathname.pwd)
|
|
19
|
+
report = Report.new
|
|
20
|
+
ConfigCheck.new(report, configuration: configuration).check
|
|
21
|
+
KeywordCheck.new(report, configuration: configuration, tools: tools).check
|
|
22
|
+
ToolInventory.new(report, configuration: configuration, tools: tools).check
|
|
23
|
+
OpportunityCheck.new(report, project_dir, configuration: configuration, tools: tools).check
|
|
24
|
+
EnvironmentCheck.new(report).check
|
|
25
|
+
report
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/reviewer/history.rb
CHANGED
|
@@ -3,36 +3,60 @@
|
|
|
3
3
|
require 'yaml/store'
|
|
4
4
|
|
|
5
5
|
module Reviewer
|
|
6
|
-
# Provides an
|
|
6
|
+
# Provides an interface to a local storage resource for persisting data across runs. For example,
|
|
7
|
+
# it enables remembering when `prepare` commands were run for reviews so they can be run less
|
|
8
|
+
# frequently and thus improve performance.
|
|
9
|
+
#
|
|
10
|
+
# It also enables remembering seeds across runs. Eventually `rvw rerun` could reuse the seeds from
|
|
11
|
+
# the immediately preceding run to more easily facilitate fixing tests that are accidentally
|
|
12
|
+
# order-dependent. Or it could automatically record a list of seeds that led to failures.
|
|
13
|
+
#
|
|
14
|
+
# Long term, it could serve to record timing details across runs to provide insight to min, max,
|
|
15
|
+
# and means. Those times could then be used for reviewer to make more informed decisions about
|
|
16
|
+
# default behavior to ensure each run remains fast.
|
|
7
17
|
class History
|
|
8
18
|
attr_reader :file, :store
|
|
9
19
|
|
|
10
|
-
|
|
20
|
+
# Creates an instance of a YAML::Store-backed history file
|
|
21
|
+
# @param file [Pathname] the history file to store data
|
|
22
|
+
#
|
|
23
|
+
# @return [History]
|
|
24
|
+
def initialize(file:)
|
|
11
25
|
@file = file
|
|
12
26
|
@store = YAML::Store.new(file)
|
|
13
27
|
end
|
|
14
28
|
|
|
29
|
+
# Saves a value to a given location in the history
|
|
30
|
+
# @param group [Symbol] the first-level key to use for saving the value--frequently a tool name
|
|
31
|
+
# @param attribute [Symbol] the second-level key to use for retrieving the value
|
|
32
|
+
# @param value [Primitive] any value that can be cleanly stored in YAML
|
|
33
|
+
#
|
|
34
|
+
# @return [Primitive] the value being stored
|
|
15
35
|
def set(group, attribute, value)
|
|
16
|
-
store.transaction do
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
store.transaction do
|
|
37
|
+
store[group] ||= {}
|
|
38
|
+
store[group][attribute] = value
|
|
19
39
|
end
|
|
20
40
|
end
|
|
21
41
|
|
|
42
|
+
# Retrieves a stored value from the history file
|
|
43
|
+
# @param group [Symbol] the first-level key to use for retrieving the value
|
|
44
|
+
# @param attribute [Symbol] the second-level key to use for retrieving the value
|
|
45
|
+
#
|
|
46
|
+
# @return [Primitive] the value being stored
|
|
22
47
|
def get(group, attribute)
|
|
23
|
-
store.transaction do
|
|
24
|
-
|
|
48
|
+
store.transaction(true) do
|
|
49
|
+
store[group]&.[](attribute)
|
|
25
50
|
end
|
|
26
51
|
end
|
|
27
52
|
|
|
28
|
-
|
|
53
|
+
# Removes the existing history file.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def clear
|
|
29
57
|
return unless File.exist?(file)
|
|
30
58
|
|
|
31
59
|
FileUtils.rm(file)
|
|
32
60
|
end
|
|
33
|
-
|
|
34
|
-
def self.reset!
|
|
35
|
-
new.reset!
|
|
36
|
-
end
|
|
37
61
|
end
|
|
38
62
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Output
|
|
5
|
+
# Shared display vocabulary included by all domain formatters.
|
|
6
|
+
# Provides common constants and helper methods for formatting output.
|
|
7
|
+
module Formatting
|
|
8
|
+
CHECKMARK = "\u2713"
|
|
9
|
+
XMARK = "\u2717"
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Formats a duration in seconds for display
|
|
14
|
+
# @param seconds [Float, nil] the duration to format
|
|
15
|
+
# @return [String] formatted duration (e.g., "1.23s")
|
|
16
|
+
def format_duration(seconds)
|
|
17
|
+
"#{seconds.to_f.round(2)}s"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the appropriate status mark
|
|
21
|
+
# @param success [Boolean] whether the operation succeeded
|
|
22
|
+
# @return [String] checkmark or x mark
|
|
23
|
+
def status_mark(success) = success ? CHECKMARK : XMARK
|
|
24
|
+
|
|
25
|
+
# Returns the appropriate style key for a status
|
|
26
|
+
# @param success [Boolean] whether the operation succeeded
|
|
27
|
+
# @return [Symbol] :success or :failure
|
|
28
|
+
def status_style(success) = success ? :success : :failure
|
|
29
|
+
|
|
30
|
+
# Pluralizes a word based on count
|
|
31
|
+
# @param count [Integer] the count
|
|
32
|
+
# @param singular [String] the singular form
|
|
33
|
+
# @param plural [String] the plural form (defaults to singular + "s")
|
|
34
|
+
# @return [String] formatted count with word (e.g., "1 issue" or "3 issues")
|
|
35
|
+
def pluralize(count, singular, plural = "#{singular}s")
|
|
36
|
+
count == 1 ? "1 #{singular}" : "#{count} #{plural}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|