reviewer 0.1.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +79 -11
- data/.github/workflows/release.yml +98 -0
- data/.gitignore +1 -1
- data/.inch.yml +3 -1
- data/.reek.yml +175 -0
- data/.reviewer.example.yml +7 -2
- data/.reviewer.yml +166 -40
- data/.rubocop.yml +34 -2
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +291 -70
- data/LICENSE.txt +20 -4
- data/README.md +310 -21
- data/RELEASING.md +190 -0
- data/Rakefile +117 -0
- data/dependency_decisions.yml +61 -0
- data/exe/fmt +1 -1
- data/exe/rvw +1 -1
- data/lib/reviewer/arguments/files.rb +47 -20
- data/lib/reviewer/arguments/keywords.rb +34 -41
- data/lib/reviewer/arguments/tags.rb +11 -11
- data/lib/reviewer/arguments.rb +100 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +32 -48
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +12 -6
- data/lib/reviewer/command/string/flags.rb +2 -4
- data/lib/reviewer/command/string.rb +47 -12
- data/lib/reviewer/command.rb +65 -10
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +6 -3
- data/lib/reviewer/context.rb +15 -0
- data/lib/reviewer/doctor/config_check.rb +46 -0
- data/lib/reviewer/doctor/environment_check.rb +58 -0
- data/lib/reviewer/doctor/formatter.rb +75 -0
- data/lib/reviewer/doctor/keyword_check.rb +85 -0
- data/lib/reviewer/doctor/opportunity_check.rb +88 -0
- data/lib/reviewer/doctor/report.rb +63 -0
- data/lib/reviewer/doctor/tool_inventory.rb +41 -0
- data/lib/reviewer/doctor.rb +28 -0
- data/lib/reviewer/history.rb +10 -17
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +70 -9
- data/lib/reviewer/output.rb +37 -78
- data/lib/reviewer/prompt.rb +38 -0
- data/lib/reviewer/report/formatter.rb +124 -0
- data/lib/reviewer/report.rb +100 -0
- data/lib/reviewer/runner/failed_files.rb +66 -0
- data/lib/reviewer/runner/formatter.rb +103 -0
- data/lib/reviewer/runner/guidance.rb +79 -0
- data/lib/reviewer/runner/result.rb +150 -0
- data/lib/reviewer/runner/strategies/captured.rb +98 -23
- data/lib/reviewer/runner/strategies/passthrough.rb +2 -11
- data/lib/reviewer/runner.rb +126 -40
- data/lib/reviewer/session/formatter.rb +87 -0
- data/lib/reviewer/session.rb +208 -0
- data/lib/reviewer/setup/catalog.rb +233 -0
- data/lib/reviewer/setup/detector.rb +61 -0
- data/lib/reviewer/setup/formatter.rb +94 -0
- data/lib/reviewer/setup/gemfile_lock.rb +55 -0
- data/lib/reviewer/setup/generator.rb +54 -0
- data/lib/reviewer/setup/tool_block.rb +112 -0
- data/lib/reviewer/setup.rb +41 -0
- data/lib/reviewer/shell/result.rb +14 -15
- data/lib/reviewer/shell/timer.rb +40 -35
- data/lib/reviewer/shell.rb +41 -12
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +88 -44
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -69
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +109 -50
- data/reviewer.gemspec +16 -19
- metadata +101 -142
- data/lib/reviewer/conversions.rb +0 -16
- data/lib/reviewer/guidance.rb +0 -77
- data/lib/reviewer/keywords/git/staged.rb +0 -64
- data/lib/reviewer/keywords/git.rb +0 -14
- data/lib/reviewer/keywords.rb +0 -9
- data/lib/reviewer/loader.rb +0 -59
- data/lib/reviewer/output/scrubber.rb +0 -48
- data/lib/reviewer/output/token.rb +0 -85
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output/formatting'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
class Session
|
|
7
|
+
# Display logic for lifecycle warnings: unrecognized keywords, no matching tools, etc.
|
|
8
|
+
class Formatter
|
|
9
|
+
include Output::Formatting
|
|
10
|
+
|
|
11
|
+
attr_reader :output, :printer
|
|
12
|
+
private :output, :printer
|
|
13
|
+
|
|
14
|
+
# Creates a formatter for session lifecycle warnings
|
|
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 warnings for keywords that don't match any tool or git scope
|
|
24
|
+
# @param unrecognized [Array<String>] the unrecognized keyword strings
|
|
25
|
+
# @param suggestions [Hash{String => String}] keyword => suggested correction
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def unrecognized_keywords(unrecognized, suggestions)
|
|
29
|
+
unrecognized.each do |keyword|
|
|
30
|
+
printer.puts(:warning, "Unrecognized: #{keyword}")
|
|
31
|
+
suggestion = suggestions[keyword]
|
|
32
|
+
printer.puts(:muted, " did you mean '#{suggestion}'?") if suggestion
|
|
33
|
+
end
|
|
34
|
+
output.newline
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Displays a warning when an unrecognized output format is requested
|
|
38
|
+
# @param value [String] the invalid format name
|
|
39
|
+
# @param known [Array<Symbol>] the valid format options
|
|
40
|
+
#
|
|
41
|
+
# @return [void]
|
|
42
|
+
def invalid_format(value, known)
|
|
43
|
+
printer.puts(:warning, "Unknown format '#{value}', using 'streaming'")
|
|
44
|
+
printer.puts(:muted, "Valid formats: #{known.join(', ')}")
|
|
45
|
+
output.newline
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Displays a git-related error with context-appropriate messaging
|
|
49
|
+
# @param message [String] the error message from the git command
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
def git_error(message)
|
|
53
|
+
if message.include?('not a git repository')
|
|
54
|
+
printer.puts(:warning, 'Not a git repository')
|
|
55
|
+
printer.puts(:muted, 'Git keywords (staged, modified, etc.) require a git repository')
|
|
56
|
+
else
|
|
57
|
+
printer.puts(:warning, 'Git command failed')
|
|
58
|
+
printer.puts(:muted, message)
|
|
59
|
+
printer.puts(:muted, 'Continuing without file filtering')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Displays a message when file-scoping keywords resolved to no files
|
|
64
|
+
# @param keywords [Array<String>] the file keywords that were requested (e.g. ['staged'])
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
def no_reviewable_files(keywords:)
|
|
68
|
+
output.newline
|
|
69
|
+
printer.puts(:muted, "No reviewable #{keywords.join(', ')} files found")
|
|
70
|
+
output.newline
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Displays a warning when no configured tools match the requested names or tags
|
|
74
|
+
# @param requested [Array<String>] tool names or tags the user asked for
|
|
75
|
+
# @param available [Array<String>] all configured tool keys
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
def no_matching_tools(requested:, available:)
|
|
79
|
+
output.newline
|
|
80
|
+
printer.puts(:warning, 'No matching tools found')
|
|
81
|
+
printer.puts(:muted, "Requested: #{requested.join(', ')}") if requested.any?
|
|
82
|
+
printer.puts(:muted, "Available: #{available.join(', ')}") if available.any?
|
|
83
|
+
output.newline
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'session/formatter'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
# Run lifecycle with full dependency injection.
|
|
7
|
+
# Owns the review/format lifecycle that was previously in Reviewer module methods.
|
|
8
|
+
class Session
|
|
9
|
+
attr_reader :context, :tools
|
|
10
|
+
private :context, :tools
|
|
11
|
+
|
|
12
|
+
# Creates a session with all dependencies injected
|
|
13
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
14
|
+
# @param tools [Tools] the collection of configured tools
|
|
15
|
+
#
|
|
16
|
+
# @return [Session]
|
|
17
|
+
def initialize(context:, tools:)
|
|
18
|
+
@context = context
|
|
19
|
+
@tools = tools
|
|
20
|
+
context.arguments.keywords.tools = tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Runs the review command for the current set of tools
|
|
24
|
+
#
|
|
25
|
+
# @return [Integer] the maximum exit status from all tools
|
|
26
|
+
def review
|
|
27
|
+
run_tools(:review)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Runs the format command for the current set of tools
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer] the maximum exit status from all tools
|
|
33
|
+
def format
|
|
34
|
+
run_tools(:format)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def arguments = context.arguments
|
|
40
|
+
def output = context.output
|
|
41
|
+
def history = context.history
|
|
42
|
+
|
|
43
|
+
def run_tools(command_type)
|
|
44
|
+
if json_output?
|
|
45
|
+
run_json(command_type)
|
|
46
|
+
else
|
|
47
|
+
run_text(command_type)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_json(command_type)
|
|
52
|
+
message = json_early_exit_message
|
|
53
|
+
return emit_json_early_exit(message) if message
|
|
54
|
+
|
|
55
|
+
current_tools = tools.current
|
|
56
|
+
return 0 if current_tools.empty?
|
|
57
|
+
|
|
58
|
+
strategy = runner_strategy(current_tools)
|
|
59
|
+
report = Batch.new(command_type, current_tools, strategy: strategy, context: context).run
|
|
60
|
+
puts report.to_json
|
|
61
|
+
report.max_exit_status
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_text(command_type)
|
|
65
|
+
return 0 if handle_failed_with_nothing_to_run?
|
|
66
|
+
return 0 if handle_file_scoping_with_no_files?
|
|
67
|
+
|
|
68
|
+
warn_unrecognized_keywords
|
|
69
|
+
|
|
70
|
+
current_tools = tools.current
|
|
71
|
+
return warn_no_matching_tools if current_tools.empty?
|
|
72
|
+
|
|
73
|
+
show_run_summary(current_tools, command_type)
|
|
74
|
+
|
|
75
|
+
strategy = runner_strategy(current_tools)
|
|
76
|
+
report = Batch.new(command_type, current_tools, strategy: strategy, context: context).run
|
|
77
|
+
display_text_report(report)
|
|
78
|
+
show_missing_tools(report, current_tools)
|
|
79
|
+
|
|
80
|
+
report.max_exit_status
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def warn_no_matching_tools
|
|
84
|
+
formatter.no_matching_tools(
|
|
85
|
+
requested: arguments.keywords.provided + arguments.tags.to_a,
|
|
86
|
+
available: tools.all.map { |tool| tool.key.to_s }
|
|
87
|
+
)
|
|
88
|
+
0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def json_output?
|
|
92
|
+
arguments.format == :json
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def json_early_exit_message
|
|
96
|
+
if failed_with_nothing_to_run?
|
|
97
|
+
'No failures to retry'
|
|
98
|
+
elsif file_scoping_with_no_files?
|
|
99
|
+
"No reviewable #{arguments.files.keywords.join(', ')} files found"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def emit_json_early_exit(message)
|
|
104
|
+
puts JSON.pretty_generate(
|
|
105
|
+
success: true,
|
|
106
|
+
message: message,
|
|
107
|
+
summary: { total: 0, passed: 0, failed: 0, missing: 0, duration: 0 },
|
|
108
|
+
tools: []
|
|
109
|
+
)
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def warn_unrecognized_keywords
|
|
114
|
+
unrecognized = arguments.keywords.unrecognized
|
|
115
|
+
return if unrecognized.empty?
|
|
116
|
+
|
|
117
|
+
suggestions = build_suggestions(unrecognized)
|
|
118
|
+
formatter.unrecognized_keywords(unrecognized, suggestions)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_suggestions(unrecognized)
|
|
122
|
+
possible = arguments.keywords.possible
|
|
123
|
+
checker = DidYouMean::SpellChecker.new(dictionary: possible)
|
|
124
|
+
|
|
125
|
+
unrecognized.each_with_object({}) do |keyword, map|
|
|
126
|
+
corrections = checker.correct(keyword)
|
|
127
|
+
map[keyword] = corrections.first if corrections.any?
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns true if failed keyword is present with nothing to re-run (caller should return early)
|
|
132
|
+
def handle_failed_with_nothing_to_run?
|
|
133
|
+
return false unless failed_with_nothing_to_run?
|
|
134
|
+
|
|
135
|
+
display_failed_empty_message
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def failed_with_nothing_to_run?
|
|
140
|
+
keywords = arguments.keywords
|
|
141
|
+
keywords.failed? &&
|
|
142
|
+
tools.failed_from_history.empty? &&
|
|
143
|
+
keywords.for_tool_names.empty? &&
|
|
144
|
+
keywords.for_tags.empty? &&
|
|
145
|
+
arguments.tags.to_a.empty?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns true if file keywords were provided but resolved to no files (caller should return early)
|
|
149
|
+
def handle_file_scoping_with_no_files?
|
|
150
|
+
return false unless file_scoping_with_no_files?
|
|
151
|
+
|
|
152
|
+
formatter.no_reviewable_files(keywords: arguments.files.keywords)
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def file_scoping_with_no_files?
|
|
157
|
+
arguments.files.keywords.any? && arguments.files.to_a.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def display_failed_empty_message
|
|
161
|
+
if tools.all.any? { |tool| history.get(tool.key, :last_status) }
|
|
162
|
+
batch_formatter.no_failures_to_retry
|
|
163
|
+
else
|
|
164
|
+
batch_formatter.no_previous_run
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def display_text_report(report)
|
|
169
|
+
if arguments.format == :summary
|
|
170
|
+
Report::Formatter.new(report, output: output).print
|
|
171
|
+
elsif report.success?
|
|
172
|
+
ran_count = report.results.count { |result| !result.missing? && !result.skipped? }
|
|
173
|
+
batch_formatter.summary(ran_count, report.duration)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def show_missing_tools(report, current_tools)
|
|
178
|
+
return unless report.missing?
|
|
179
|
+
|
|
180
|
+
batch_formatter.missing_tools(report.missing_tools, tools: current_tools)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def show_run_summary(current_tools, command_type)
|
|
184
|
+
return unless arguments.keywords.provided.any?
|
|
185
|
+
|
|
186
|
+
entries = build_run_summary(current_tools, command_type)
|
|
187
|
+
return if entries.size <= 1 && entries.none? { |entry| entry[:files].any? }
|
|
188
|
+
|
|
189
|
+
batch_formatter.run_summary(entries)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_run_summary(current_tools, command_type)
|
|
193
|
+
current_tools.filter_map do |tool|
|
|
194
|
+
Command.new(tool, command_type, context: context).run_summary
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def runner_strategy(current_tools)
|
|
199
|
+
arguments.runner_strategy(multiple_tools: current_tools.size > 1)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def formatter = @formatter ||= Session::Formatter.new(output)
|
|
203
|
+
|
|
204
|
+
def batch_formatter
|
|
205
|
+
Batch::Formatter.new(output)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Setup
|
|
5
|
+
# Frozen hash of known tool definitions with detection signals.
|
|
6
|
+
# Each entry contains the config structure needed for .reviewer.yml
|
|
7
|
+
# plus a :detect key with signals for auto-detection.
|
|
8
|
+
module Catalog
|
|
9
|
+
# Known tool definitions with detection signals and default configuration
|
|
10
|
+
TOOLS = {
|
|
11
|
+
bundle_audit: {
|
|
12
|
+
name: 'Bundle Audit',
|
|
13
|
+
description: 'Review gem dependencies for security issues',
|
|
14
|
+
tags: %w[security dependencies ruby],
|
|
15
|
+
commands: {
|
|
16
|
+
install: 'bundle exec gem install bundler-audit',
|
|
17
|
+
prepare: 'bundle exec bundle-audit update',
|
|
18
|
+
review: 'bundle exec bundle-audit check --no-update'
|
|
19
|
+
},
|
|
20
|
+
detect: {
|
|
21
|
+
gems: %w[bundler-audit]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
rubocop: {
|
|
25
|
+
name: 'RuboCop',
|
|
26
|
+
description: 'Review Ruby syntax and formatting for consistency',
|
|
27
|
+
tags: %w[ruby syntax],
|
|
28
|
+
commands: {
|
|
29
|
+
install: 'bundle exec gem install rubocop',
|
|
30
|
+
review: 'bundle exec rubocop --parallel',
|
|
31
|
+
format: 'bundle exec rubocop --auto-correct'
|
|
32
|
+
},
|
|
33
|
+
files: { flag: '', separator: ' ', pattern: '*.rb' },
|
|
34
|
+
detect: {
|
|
35
|
+
gems: %w[rubocop],
|
|
36
|
+
files: %w[.rubocop.yml]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
standard: {
|
|
40
|
+
name: 'Standard',
|
|
41
|
+
description: 'Zero-configuration Ruby linter',
|
|
42
|
+
tags: %w[ruby syntax],
|
|
43
|
+
commands: {
|
|
44
|
+
review: 'bundle exec standardrb',
|
|
45
|
+
format: 'bundle exec standardrb --fix'
|
|
46
|
+
},
|
|
47
|
+
detect: {
|
|
48
|
+
gems: %w[standard]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
reek: {
|
|
52
|
+
name: 'Reek',
|
|
53
|
+
description: 'Examine Ruby classes for code smells',
|
|
54
|
+
tags: %w[ruby quality],
|
|
55
|
+
commands: {
|
|
56
|
+
install: 'bundle exec gem install reek',
|
|
57
|
+
review: 'bundle exec reek'
|
|
58
|
+
},
|
|
59
|
+
files: { flag: '', separator: ' ', pattern: '*.rb' },
|
|
60
|
+
detect: {
|
|
61
|
+
gems: %w[reek],
|
|
62
|
+
files: %w[.reek.yml]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
flog: {
|
|
66
|
+
name: 'Flog',
|
|
67
|
+
description: 'Reports the most tortured Ruby code in a pain report',
|
|
68
|
+
tags: %w[ruby quality],
|
|
69
|
+
commands: {
|
|
70
|
+
install: 'bundle exec gem install flog',
|
|
71
|
+
review: 'bundle exec flog -g lib'
|
|
72
|
+
},
|
|
73
|
+
detect: {
|
|
74
|
+
gems: %w[flog]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
flay: {
|
|
78
|
+
name: 'Flay',
|
|
79
|
+
description: 'Review Ruby code for structural similarities',
|
|
80
|
+
tags: %w[ruby quality],
|
|
81
|
+
commands: {
|
|
82
|
+
install: 'bundle exec gem install flay',
|
|
83
|
+
review: 'bundle exec flay ./lib'
|
|
84
|
+
},
|
|
85
|
+
detect: {
|
|
86
|
+
gems: %w[flay]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
brakeman: {
|
|
90
|
+
name: 'Brakeman',
|
|
91
|
+
description: 'Static analysis security scanner for Rails',
|
|
92
|
+
tags: %w[security ruby],
|
|
93
|
+
commands: {
|
|
94
|
+
install: 'bundle exec gem install brakeman',
|
|
95
|
+
review: 'bundle exec brakeman --no-pager -q'
|
|
96
|
+
},
|
|
97
|
+
detect: {
|
|
98
|
+
gems: %w[brakeman],
|
|
99
|
+
directories: %w[app/controllers]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
fasterer: {
|
|
103
|
+
name: 'Fasterer',
|
|
104
|
+
description: 'Suggest performance improvements for Ruby code',
|
|
105
|
+
tags: %w[ruby quality performance],
|
|
106
|
+
commands: {
|
|
107
|
+
install: 'bundle exec gem install fasterer',
|
|
108
|
+
review: 'bundle exec fasterer'
|
|
109
|
+
},
|
|
110
|
+
files: { flag: '', separator: ' ', pattern: '*.rb' },
|
|
111
|
+
detect: {
|
|
112
|
+
gems: %w[fasterer]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
tests: {
|
|
116
|
+
name: 'Minitest',
|
|
117
|
+
description: 'Unit tests and coverage',
|
|
118
|
+
tags: %w[ruby tests],
|
|
119
|
+
commands: {
|
|
120
|
+
review: 'bundle exec rake test'
|
|
121
|
+
},
|
|
122
|
+
files: { review: 'bundle exec ruby -Itest', pattern: '*_test.rb', map_to_tests: 'minitest' },
|
|
123
|
+
detect: {
|
|
124
|
+
gems: %w[minitest],
|
|
125
|
+
directories: %w[test]
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
specs: {
|
|
129
|
+
name: 'RSpec',
|
|
130
|
+
description: 'Behavior-driven tests and coverage',
|
|
131
|
+
tags: %w[ruby tests],
|
|
132
|
+
commands: {
|
|
133
|
+
review: 'bundle exec rspec'
|
|
134
|
+
},
|
|
135
|
+
files: { flag: '', separator: ' ', pattern: '*_spec.rb', map_to_tests: 'rspec' },
|
|
136
|
+
detect: {
|
|
137
|
+
gems: %w[rspec],
|
|
138
|
+
directories: %w[spec]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
eslint: {
|
|
142
|
+
name: 'ESLint',
|
|
143
|
+
description: 'Lint JavaScript and TypeScript code',
|
|
144
|
+
tags: %w[javascript linting syntax],
|
|
145
|
+
commands: {
|
|
146
|
+
review: 'npx eslint .',
|
|
147
|
+
format: 'npx eslint . --fix'
|
|
148
|
+
},
|
|
149
|
+
files: { flag: '', separator: ' ', pattern: '*.js' },
|
|
150
|
+
detect: {
|
|
151
|
+
files: %w[.eslintrc .eslintrc.js .eslintrc.json .eslintrc.yml eslint.config.js eslint.config.mjs]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
prettier: {
|
|
155
|
+
name: 'Prettier',
|
|
156
|
+
description: 'Check code formatting consistency',
|
|
157
|
+
tags: %w[javascript formatting],
|
|
158
|
+
commands: {
|
|
159
|
+
review: 'npx prettier --check .',
|
|
160
|
+
format: 'npx prettier --write .'
|
|
161
|
+
},
|
|
162
|
+
detect: {
|
|
163
|
+
files: %w[.prettierrc .prettierrc.js .prettierrc.json .prettierrc.yml .prettierrc.yaml]
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
stylelint: {
|
|
167
|
+
name: 'Stylelint',
|
|
168
|
+
description: 'Lint CSS and SCSS for errors and consistency',
|
|
169
|
+
tags: %w[css linting],
|
|
170
|
+
commands: {
|
|
171
|
+
review: 'npx stylelint "**/*.css"',
|
|
172
|
+
format: 'npx stylelint "**/*.css" --fix'
|
|
173
|
+
},
|
|
174
|
+
files: { flag: '', separator: ' ', pattern: '*.css' },
|
|
175
|
+
detect: {
|
|
176
|
+
files: %w[.stylelintrc .stylelintrc.js .stylelintrc.json .stylelintrc.yml]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
typescript: {
|
|
180
|
+
name: 'TypeScript',
|
|
181
|
+
description: 'Type-check TypeScript code',
|
|
182
|
+
tags: %w[javascript typescript],
|
|
183
|
+
commands: {
|
|
184
|
+
review: 'npx tsc --noEmit'
|
|
185
|
+
},
|
|
186
|
+
detect: {
|
|
187
|
+
files: %w[tsconfig.json]
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
biome: {
|
|
191
|
+
name: 'Biome',
|
|
192
|
+
description: 'Lint and format JavaScript and TypeScript',
|
|
193
|
+
tags: %w[javascript linting formatting],
|
|
194
|
+
commands: {
|
|
195
|
+
review: 'npx @biomejs/biome check .',
|
|
196
|
+
format: 'npx @biomejs/biome check . --fix'
|
|
197
|
+
},
|
|
198
|
+
files: { flag: '', separator: ' ', pattern: '*.js' },
|
|
199
|
+
detect: {
|
|
200
|
+
files: %w[biome.json biome.jsonc]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}.freeze
|
|
204
|
+
|
|
205
|
+
# Returns the full catalog of known tools
|
|
206
|
+
#
|
|
207
|
+
# @return [Hash] frozen hash of tool definitions
|
|
208
|
+
def self.all = TOOLS
|
|
209
|
+
|
|
210
|
+
# Returns the config for a tool key without the :detect key
|
|
211
|
+
#
|
|
212
|
+
# @param key [Symbol] the tool key
|
|
213
|
+
# @return [Hash, nil] config hash without :detect, or nil if not found
|
|
214
|
+
def self.config_for(key)
|
|
215
|
+
definition = TOOLS[key]
|
|
216
|
+
return nil unless definition
|
|
217
|
+
|
|
218
|
+
definition.except(:detect)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns the detection signals for a tool key
|
|
222
|
+
#
|
|
223
|
+
# @param key [Symbol] the tool key
|
|
224
|
+
# @return [Hash, nil] detect hash, or nil if not found
|
|
225
|
+
def self.detect_for(key)
|
|
226
|
+
definition = TOOLS[key]
|
|
227
|
+
return nil unless definition
|
|
228
|
+
|
|
229
|
+
definition[:detect]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Setup
|
|
5
|
+
# Scans a project directory to detect which review tools are applicable
|
|
6
|
+
# based on Gemfile.lock contents, config files, and directory structure.
|
|
7
|
+
class Detector
|
|
8
|
+
# Value object for a single detection result (tool key + evidence).
|
|
9
|
+
# @!attribute key [rw]
|
|
10
|
+
# @return [Symbol] the tool identifier from the catalog
|
|
11
|
+
# @!attribute reasons [rw]
|
|
12
|
+
# @return [Array<String>] evidence strings explaining why the tool was detected
|
|
13
|
+
Result = Struct.new(:key, :reasons, keyword_init: true) do
|
|
14
|
+
# @return [String] human-readable tool name from the catalog, or the key as fallback
|
|
15
|
+
def name = Catalog.config_for(key)&.dig(:name) || key.to_s
|
|
16
|
+
# @return [String] formatted line for display (name + reasons)
|
|
17
|
+
def summary = " #{name.ljust(22)}#{reasons.join(', ')}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :project_dir
|
|
21
|
+
|
|
22
|
+
# Creates a detector for scanning a project directory for supported tools
|
|
23
|
+
# @param project_dir [Pathname, String] the project root to scan
|
|
24
|
+
#
|
|
25
|
+
# @return [Detector]
|
|
26
|
+
def initialize(project_dir = Pathname.pwd)
|
|
27
|
+
@project_dir = Pathname(project_dir)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Scans the project and returns detection results for matching tools
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<Result>] detected tools with evidence
|
|
33
|
+
def detect
|
|
34
|
+
gems = GemfileLock.new(project_dir.join('Gemfile.lock')).gem_names
|
|
35
|
+
|
|
36
|
+
Catalog.all.filter_map do |key, definition|
|
|
37
|
+
reasons = reasons_for(definition[:detect], gems)
|
|
38
|
+
Result.new(key: key, reasons: reasons) if reasons.any?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def reasons_for(detect, gems)
|
|
45
|
+
gem_reasons(detect, gems) + file_reasons(detect) + directory_reasons(detect)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gem_reasons(detect, gems)
|
|
49
|
+
Array(detect[:gems]).select { |name| gems.include?(name) }.map { |name| "#{name} in Gemfile.lock" }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def file_reasons(detect)
|
|
53
|
+
Array(detect[:files]).select { |name| project_dir.join(name).exist? }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def directory_reasons(detect)
|
|
57
|
+
Array(detect[:directories]).select { |name| project_dir.join(name).directory? }.map { |name| "#{name}/ directory" }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output/formatting'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
module Setup
|
|
7
|
+
# Display logic for the first-run setup flow
|
|
8
|
+
class Formatter
|
|
9
|
+
include Output::Formatting
|
|
10
|
+
|
|
11
|
+
attr_reader :output, :printer
|
|
12
|
+
private :output, :printer
|
|
13
|
+
|
|
14
|
+
# Creates a formatter for setup flow 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 the welcome message when Reviewer has no configuration file
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
26
|
+
def first_run_greeting
|
|
27
|
+
output.newline
|
|
28
|
+
printer.puts(:bold, "It looks like you're setting up Reviewer for the first time on this project.")
|
|
29
|
+
output.newline
|
|
30
|
+
printer.puts(:muted, 'This will auto-detect your tools and generate a .reviewer.yml configuration file.')
|
|
31
|
+
output.newline
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Displays a hint about `rvw init` when the user declines initial setup
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def first_run_skip
|
|
38
|
+
printer.puts(:muted, 'You can run `rvw init` any time to auto-detect and configure your tools.')
|
|
39
|
+
output.newline
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Displays a notice when `rvw init` is run but .reviewer.yml already exists
|
|
43
|
+
# @param config_file [Pathname] the existing configuration file path
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
def setup_already_exists(config_file)
|
|
47
|
+
output.newline
|
|
48
|
+
printer.puts(:bold, "Configuration already exists: #{config_file.basename}")
|
|
49
|
+
output.newline
|
|
50
|
+
printer.puts(:muted, 'Run `rvw doctor` for a diagnostic report.')
|
|
51
|
+
printer.puts(:muted, 'To regenerate, remove the file and run `rvw init` again.')
|
|
52
|
+
output.newline
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Displays a message when auto-detection finds no supported tools in the project
|
|
56
|
+
#
|
|
57
|
+
# @return [void]
|
|
58
|
+
def setup_no_tools_detected
|
|
59
|
+
output.newline
|
|
60
|
+
printer.puts(:bold, 'No supported tools detected.')
|
|
61
|
+
output.newline
|
|
62
|
+
printer.puts(:muted, 'Create .reviewer.yml manually:')
|
|
63
|
+
printer.puts(:muted, " #{Setup::CONFIG_URL}")
|
|
64
|
+
output.newline
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Displays the results of a successful setup with the detected tools
|
|
68
|
+
# @param results [Array<Detector::Result>] the tools that were detected and configured
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
def setup_success(results)
|
|
72
|
+
output.newline
|
|
73
|
+
printer.puts(:success, 'Created .reviewer.yml')
|
|
74
|
+
print_detected_tools(results)
|
|
75
|
+
print_setup_footer
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def print_detected_tools(results)
|
|
81
|
+
output.newline
|
|
82
|
+
printer.puts(:bold, 'Detected tools:')
|
|
83
|
+
results.each { |result| printer.puts(:default, result.summary) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_setup_footer
|
|
87
|
+
output.newline
|
|
88
|
+
printer.puts(:muted, "Configure further: #{Setup::CONFIG_URL}")
|
|
89
|
+
printer.puts(:muted, 'Run `rvw` to review your code.')
|
|
90
|
+
output.newline
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|