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,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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Setup
|
|
5
|
+
# Parses a Gemfile.lock to extract gem names from the specs section
|
|
6
|
+
class GemfileLock
|
|
7
|
+
# Spec lines are indented with 4 spaces: " gem-name (version)"
|
|
8
|
+
SPEC_LINE = /\A {4}(\S+)\s/
|
|
9
|
+
|
|
10
|
+
attr_reader :path
|
|
11
|
+
|
|
12
|
+
# Creates a parser for extracting gem names from a Gemfile.lock
|
|
13
|
+
# @param path [Pathname] the path to the Gemfile.lock file
|
|
14
|
+
#
|
|
15
|
+
# @return [GemfileLock]
|
|
16
|
+
def initialize(path)
|
|
17
|
+
@path = path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the set of gem names found in the specs section
|
|
21
|
+
#
|
|
22
|
+
# @return [Set<String>] gem names
|
|
23
|
+
def gem_names
|
|
24
|
+
return Set.new unless path.exist?
|
|
25
|
+
|
|
26
|
+
parse_specs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def parse_specs
|
|
32
|
+
in_specs = false
|
|
33
|
+
gems = Set.new
|
|
34
|
+
|
|
35
|
+
path.each_line do |line|
|
|
36
|
+
in_specs, gem_name = process_line(line, in_specs)
|
|
37
|
+
gems.add(gem_name) if gem_name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
gems
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def process_line(line, in_specs)
|
|
44
|
+
return [true, nil] if line.strip == 'specs:'
|
|
45
|
+
return [in_specs, nil] unless in_specs
|
|
46
|
+
|
|
47
|
+
match = line.match(SPEC_LINE)
|
|
48
|
+
return [true, match[1]] if match
|
|
49
|
+
|
|
50
|
+
still_in_specs = line.start_with?(' ') || line.strip.empty?
|
|
51
|
+
[still_in_specs, nil]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
require_relative 'tool_block'
|
|
6
|
+
|
|
7
|
+
module Reviewer
|
|
8
|
+
module Setup
|
|
9
|
+
# Produces .reviewer.yml YAML content from a list of detected tool keys.
|
|
10
|
+
# Orchestrates which tools to include; delegates per-tool rendering to ToolBlock.
|
|
11
|
+
class Generator
|
|
12
|
+
attr_reader :tool_keys, :project_dir
|
|
13
|
+
|
|
14
|
+
# Creates a generator for producing .reviewer.yml configuration
|
|
15
|
+
# @param tool_keys [Array<Symbol>] catalog tool keys to include in config
|
|
16
|
+
# @param project_dir [Pathname] the project root (used for package manager detection)
|
|
17
|
+
#
|
|
18
|
+
# @return [Generator]
|
|
19
|
+
def initialize(tool_keys, project_dir: Pathname.pwd)
|
|
20
|
+
@tool_keys = tool_keys
|
|
21
|
+
@project_dir = Pathname(project_dir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Generates YAML configuration string for the detected tools
|
|
25
|
+
#
|
|
26
|
+
# @return [String] valid YAML for .reviewer.yml
|
|
27
|
+
def generate
|
|
28
|
+
return "--- {}\n" if tool_keys.empty?
|
|
29
|
+
|
|
30
|
+
blocks = tool_keys.filter_map do |key|
|
|
31
|
+
definition = Catalog.config_for(key)
|
|
32
|
+
next unless definition
|
|
33
|
+
|
|
34
|
+
ToolBlock.new(key, definition, js_runner: js_runner).to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
"---\n#{blocks.join("\n")}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Detects the JS package manager based on lockfile presence
|
|
43
|
+
def js_runner
|
|
44
|
+
@js_runner ||= if project_dir.join('yarn.lock').exist?
|
|
45
|
+
'yarn'
|
|
46
|
+
elsif project_dir.join('pnpm-lock.yaml').exist?
|
|
47
|
+
'pnpm exec'
|
|
48
|
+
else
|
|
49
|
+
'npx'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Setup
|
|
5
|
+
# Renders the YAML configuration block for a single tool definition.
|
|
6
|
+
# Owns the definition data so rendering methods reference self's state
|
|
7
|
+
# rather than reaching into parameters.
|
|
8
|
+
class ToolBlock
|
|
9
|
+
YAML_BARE_WORDS = %w[true false yes no on off null ~].freeze
|
|
10
|
+
|
|
11
|
+
# Creates a renderer for a single tool's YAML configuration block
|
|
12
|
+
# @param key [Symbol] the tool key (e.g., :rubocop)
|
|
13
|
+
# @param definition [Hash] the catalog definition for this tool
|
|
14
|
+
# @param js_runner [String] the JS package runner to substitute for npx
|
|
15
|
+
#
|
|
16
|
+
# @return [ToolBlock]
|
|
17
|
+
def initialize(key, definition, js_runner:)
|
|
18
|
+
@key = key
|
|
19
|
+
@definition = definition
|
|
20
|
+
@js_runner = js_runner
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders the full YAML block for this tool
|
|
24
|
+
#
|
|
25
|
+
# @return [String] the YAML text for this tool's configuration
|
|
26
|
+
def to_s
|
|
27
|
+
lines = header_lines
|
|
28
|
+
lines.concat(commands_block)
|
|
29
|
+
lines.concat(files_block) if @definition[:files]
|
|
30
|
+
lines << ''
|
|
31
|
+
lines.join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def header_lines
|
|
37
|
+
lines = []
|
|
38
|
+
lines << "# #{@definition[:description]}"
|
|
39
|
+
lines << "#{@key}:"
|
|
40
|
+
lines << " name: #{quote(@definition[:name])}"
|
|
41
|
+
lines << " description: #{quote(@definition[:description])}"
|
|
42
|
+
lines << tags_line if @definition[:tags]&.any?
|
|
43
|
+
lines
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tags_line
|
|
47
|
+
" tags: [#{@definition[:tags].join(', ')}]"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def commands_block
|
|
51
|
+
commands = @definition[:commands]
|
|
52
|
+
lines = [' commands:']
|
|
53
|
+
%i[install prepare review format].each do |type|
|
|
54
|
+
next unless commands[type]
|
|
55
|
+
|
|
56
|
+
lines << " #{type}: #{quote(apply_js_runner(commands[type].to_s))}"
|
|
57
|
+
end
|
|
58
|
+
lines
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def files_block
|
|
62
|
+
lines = [' files:']
|
|
63
|
+
lines.concat(files_command_lines)
|
|
64
|
+
lines.concat(files_targeting_lines)
|
|
65
|
+
lines
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def files_command_lines
|
|
69
|
+
%i[review format].filter_map do |type|
|
|
70
|
+
" #{type}: #{quote(apply_js_runner(tool_files[type].to_s))}" if tool_files[type]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def files_targeting_lines
|
|
75
|
+
[
|
|
76
|
+
(file_setting_line(:flag) if tool_files.key?(:flag)),
|
|
77
|
+
(file_setting_line(:separator) if tool_files.key?(:separator)),
|
|
78
|
+
(file_setting_line(:pattern) if tool_files[:pattern]),
|
|
79
|
+
(" map_to_tests: #{tool_files[:map_to_tests]}" if tool_files[:map_to_tests])
|
|
80
|
+
].compact
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def file_setting_line(key)
|
|
84
|
+
" #{key}: #{quote(tool_files[key])}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tool_files
|
|
88
|
+
@definition[:files]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def apply_js_runner(command)
|
|
92
|
+
return command unless command.start_with?('npx ')
|
|
93
|
+
|
|
94
|
+
command.sub('npx ', "#{@js_runner} ")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def quote(value)
|
|
98
|
+
str = value.to_s
|
|
99
|
+
return "''" if str.empty?
|
|
100
|
+
|
|
101
|
+
needs_quoting?(str) ? "'#{str.gsub("'", "''")}'" : str
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def needs_quoting?(str)
|
|
105
|
+
str.match?(/[:#\[\]{}&*!|>'"@`,]/) ||
|
|
106
|
+
str.strip != str ||
|
|
107
|
+
str.empty? ||
|
|
108
|
+
YAML_BARE_WORDS.include?(str.downcase)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'setup/catalog'
|
|
4
|
+
require_relative 'setup/detector'
|
|
5
|
+
require_relative 'setup/formatter'
|
|
6
|
+
require_relative 'setup/gemfile_lock'
|
|
7
|
+
require_relative 'setup/generator'
|
|
8
|
+
|
|
9
|
+
module Reviewer
|
|
10
|
+
# Handles first-run setup: detecting tools and generating .reviewer.yml
|
|
11
|
+
module Setup
|
|
12
|
+
# URL to the configuration documentation for setup output messages
|
|
13
|
+
CONFIG_URL = 'https://github.com/garrettdimon/reviewer#configuration'
|
|
14
|
+
|
|
15
|
+
# Runs the full setup flow: detect tools, generate config, display results
|
|
16
|
+
# @param project_dir [Pathname, String] the project root to scan (defaults to pwd)
|
|
17
|
+
# @param output [Output] the console output handler
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def self.run(configuration:, project_dir: Pathname.pwd, output: Output.new)
|
|
21
|
+
config_file = configuration.file
|
|
22
|
+
formatter = Formatter.new(output)
|
|
23
|
+
|
|
24
|
+
if config_file.exist?
|
|
25
|
+
formatter.setup_already_exists(config_file)
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
results = Detector.new(project_dir).detect
|
|
30
|
+
|
|
31
|
+
if results.empty?
|
|
32
|
+
formatter.setup_no_tools_detected
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
yaml = Generator.new(results.map(&:key), project_dir: project_dir).generate
|
|
37
|
+
config_file.write(yaml)
|
|
38
|
+
formatter.setup_success(results)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|