reviewer 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.flayignore +1 -0
- data/.github/workflows/main.yml +11 -3
- data/.gitignore +5 -0
- data/.reviewer.example.yml +27 -23
- data/.reviewer.yml +58 -5
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +14 -0
- data/Gemfile +0 -3
- data/Gemfile.lock +54 -29
- data/README.md +5 -50
- data/exe/fmt +1 -1
- data/exe/rvw +1 -1
- data/lib/reviewer.rb +39 -26
- data/lib/reviewer/arguments.rb +25 -9
- data/lib/reviewer/arguments/files.rb +37 -5
- data/lib/reviewer/arguments/keywords.rb +23 -9
- data/lib/reviewer/arguments/tags.rb +26 -3
- data/lib/reviewer/batch.rb +64 -0
- data/lib/reviewer/command.rb +100 -0
- data/lib/reviewer/command/string.rb +72 -0
- data/lib/reviewer/command/string/env.rb +40 -0
- data/lib/reviewer/command/string/flags.rb +40 -0
- data/lib/reviewer/command/string/verbosity.rb +51 -0
- data/lib/reviewer/command/verbosity.rb +65 -0
- data/lib/reviewer/configuration.rb +24 -4
- data/lib/reviewer/conversions.rb +27 -0
- data/lib/reviewer/guidance.rb +73 -0
- data/lib/reviewer/history.rb +38 -0
- data/lib/reviewer/keywords.rb +9 -0
- data/lib/reviewer/keywords/git.rb +14 -0
- data/lib/reviewer/keywords/git/staged.rb +48 -0
- data/lib/reviewer/loader.rb +2 -3
- data/lib/reviewer/output.rb +92 -0
- data/lib/reviewer/printer.rb +25 -0
- data/lib/reviewer/runner.rb +43 -72
- data/lib/reviewer/runner/strategies/quiet.rb +90 -0
- data/lib/reviewer/runner/strategies/verbose.rb +63 -0
- data/lib/reviewer/shell.rb +58 -0
- data/lib/reviewer/shell/result.rb +69 -0
- data/lib/reviewer/shell/timer.rb +57 -0
- data/lib/reviewer/tool.rb +109 -40
- data/lib/reviewer/tool/settings.rb +18 -32
- data/lib/reviewer/tools.rb +38 -3
- data/lib/reviewer/version.rb +1 -1
- data/reviewer.gemspec +10 -2
- metadata +143 -16
- data/lib/reviewer/arguments/keywords/git.rb +0 -16
- data/lib/reviewer/arguments/keywords/git/staged.rb +0 -64
- data/lib/reviewer/logger.rb +0 -62
- data/lib/reviewer/tool/command.rb +0 -80
- data/lib/reviewer/tool/env.rb +0 -38
- data/lib/reviewer/tool/flags.rb +0 -38
- data/lib/reviewer/tool/verbosity.rb +0 -39
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml/store'
|
4
|
+
|
5
|
+
module Reviewer
|
6
|
+
# Handles the logic around what to display after a command has been run
|
7
|
+
class Guidance
|
8
|
+
attr_reader :command, :result, :output
|
9
|
+
|
10
|
+
# Create an instance of guidance for suggesting recovery steps after errors
|
11
|
+
# @param command: [Command] the command that was run and needs recovery guidance
|
12
|
+
# @param result: [Result] the result of the command
|
13
|
+
# @param output: Reviewer.output [Output] the output channel for displaying content
|
14
|
+
#
|
15
|
+
# @return [Guidance] the guidance class to suggest relevant recovery steps
|
16
|
+
def initialize(command:, result:, output: Reviewer.output)
|
17
|
+
@command = command
|
18
|
+
@result = result
|
19
|
+
@output = output
|
20
|
+
end
|
21
|
+
|
22
|
+
# Prints the relevant guidance based on the command and result context
|
23
|
+
#
|
24
|
+
# @return [void] prints the relevant guidance to the stream
|
25
|
+
def show
|
26
|
+
case result
|
27
|
+
when executable_not_found? then show_missing_executable_guidance
|
28
|
+
when cannot_execute? then show_unrecoverable_guidance
|
29
|
+
else show_syntax_guidance
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Conditional check for when the command result was that the executable couldn't be found
|
36
|
+
#
|
37
|
+
# @return [Boolean] true if the result indicates the command couldn't be found
|
38
|
+
def executable_not_found?
|
39
|
+
->(result) { result.executable_not_found? }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Conditional check for when the command result was that it was unable to be executed
|
43
|
+
#
|
44
|
+
# @return [Boolean] true if the result indicates the command couldn't be executed
|
45
|
+
def cannot_execute?
|
46
|
+
->(result) { result.cannot_execute? }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Shows the recovery guidance for when a command is missing
|
50
|
+
#
|
51
|
+
# @return [void] prints missing executable guidance
|
52
|
+
def show_missing_executable_guidance
|
53
|
+
output.missing_executable_guidance(command)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Shows the recovery guidance for when a command generates an unrecoverable error
|
57
|
+
#
|
58
|
+
# @return [void] prints unrecoverable error guidance
|
59
|
+
def show_unrecoverable_guidance
|
60
|
+
output.unrecoverable(result.stderr)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Shows suggestions for ignoring or disable rules when a command fails after reviewing code
|
64
|
+
#
|
65
|
+
# @return [void] prints syntax guidance
|
66
|
+
def show_syntax_guidance
|
67
|
+
output.syntax_guidance(
|
68
|
+
ignore_link: command.tool.links[:ignore_syntax],
|
69
|
+
disable_link: command.tool.links[:disable_syntax]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml/store'
|
4
|
+
|
5
|
+
module Reviewer
|
6
|
+
# Provides an instance of a storage resource for persisting data across runs
|
7
|
+
class History
|
8
|
+
attr_reader :file, :store
|
9
|
+
|
10
|
+
def initialize(file = Reviewer.configuration.history_file)
|
11
|
+
@file = file
|
12
|
+
@store = YAML::Store.new(file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def set(group, attribute, value)
|
16
|
+
store.transaction do |s|
|
17
|
+
s[group] = {} if s[group].nil?
|
18
|
+
s[group][attribute] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(group, attribute)
|
23
|
+
store.transaction do |s|
|
24
|
+
s[group].nil? ? nil : s[group][attribute]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset!
|
29
|
+
return unless File.exist?(file)
|
30
|
+
|
31
|
+
FileUtils.rm(file)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.reset!
|
35
|
+
new.reset!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
module Keywords
|
5
|
+
module Git
|
6
|
+
# Provides a convenient interface to get the list of staged files via Git
|
7
|
+
class Staged
|
8
|
+
OPTIONS = [
|
9
|
+
'diff',
|
10
|
+
'--staged',
|
11
|
+
'--name-only'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
attr_reader :stdout, :stderr, :status, :exit_status
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
stdout.strip.empty? ? [] : stdout.split("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def list
|
21
|
+
@stdout, @stderr, @status = Open3.capture3(command)
|
22
|
+
@exit_status = @status.exitstatus.to_i
|
23
|
+
|
24
|
+
@status.success? ? to_a : raise_command_line_error
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.list
|
28
|
+
new.list
|
29
|
+
end
|
30
|
+
|
31
|
+
def command
|
32
|
+
command_parts.join(' ')
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def raise_command_line_error
|
38
|
+
message = "Git Error: #{stderr} (#{command})"
|
39
|
+
raise SystemCallError.new(message, exit_status)
|
40
|
+
end
|
41
|
+
|
42
|
+
def command_parts
|
43
|
+
BASE_COMMAND + OPTIONS
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/reviewer/loader.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'yaml'
|
4
|
-
require 'active_support/core_ext/hash/indifferent_access'
|
5
4
|
|
6
5
|
module Reviewer
|
7
6
|
# Provides a collection of the configured tools
|
@@ -16,7 +15,7 @@ module Reviewer
|
|
16
15
|
|
17
16
|
def initialize(file = Reviewer.configuration.file)
|
18
17
|
@file = file
|
19
|
-
@configuration =
|
18
|
+
@configuration = configuration_hash
|
20
19
|
|
21
20
|
validate_configuration!
|
22
21
|
end
|
@@ -50,7 +49,7 @@ module Reviewer
|
|
50
49
|
end
|
51
50
|
|
52
51
|
def configuration_hash
|
53
|
-
@configuration_hash ||=
|
52
|
+
@configuration_hash ||= Psych.safe_load_file(@file, symbolize_names: true)
|
54
53
|
rescue Errno::ENOENT
|
55
54
|
raise MissingConfigurationError, "Tools configuration file couldn't be found at `#{file}`"
|
56
55
|
rescue Psych::SyntaxError => e
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
|
5
|
+
module Reviewer
|
6
|
+
# Friendly API for printing nicely-formatted output to the console
|
7
|
+
class Output
|
8
|
+
SUCCESS = 'Success'
|
9
|
+
FAILURE = 'Failure ·'
|
10
|
+
DIVIDER = ('-' * 60).to_s
|
11
|
+
|
12
|
+
attr_reader :printer
|
13
|
+
|
14
|
+
def initialize(printer: Reviewer.configuration.printer)
|
15
|
+
@printer = printer
|
16
|
+
end
|
17
|
+
|
18
|
+
def info(message)
|
19
|
+
printer.info message
|
20
|
+
end
|
21
|
+
|
22
|
+
def blank_line
|
23
|
+
printer.info
|
24
|
+
end
|
25
|
+
|
26
|
+
def divider
|
27
|
+
blank_line
|
28
|
+
printer.info DIVIDER.light_black
|
29
|
+
blank_line
|
30
|
+
end
|
31
|
+
|
32
|
+
def tool_summary(tool)
|
33
|
+
printer.info "\n#{tool.name}".bold + ' · '.light_black + tool.description
|
34
|
+
end
|
35
|
+
|
36
|
+
def current_command(command)
|
37
|
+
command = String(command)
|
38
|
+
|
39
|
+
printer.info "\nNow Running:"
|
40
|
+
printer.info command.light_black
|
41
|
+
end
|
42
|
+
|
43
|
+
def exit_status(value)
|
44
|
+
failure("Exit Status #{value}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def success(timer)
|
48
|
+
message = SUCCESS.green.bold + " #{timer.total_seconds}s".green
|
49
|
+
message += " (#{timer.prep_percent}% preparation)".yellow if timer.prepped?
|
50
|
+
|
51
|
+
printer.info message
|
52
|
+
end
|
53
|
+
|
54
|
+
def failure(details, command: nil)
|
55
|
+
printer.error "#{FAILURE} #{details}".red.bold
|
56
|
+
|
57
|
+
return if command.nil?
|
58
|
+
|
59
|
+
blank_line
|
60
|
+
printer.error 'Failed Command:'.red.bold
|
61
|
+
printer.error String(command).light_black
|
62
|
+
end
|
63
|
+
|
64
|
+
def unrecoverable(details)
|
65
|
+
printer.error 'Unrecoverable Error:'.red.bold
|
66
|
+
printer.error details
|
67
|
+
end
|
68
|
+
|
69
|
+
def guidance(summary, details)
|
70
|
+
return if details.nil?
|
71
|
+
|
72
|
+
blank_line
|
73
|
+
printer.info summary
|
74
|
+
printer.info details.to_s.light_black
|
75
|
+
end
|
76
|
+
|
77
|
+
def missing_executable_guidance(command)
|
78
|
+
tool = command.tool
|
79
|
+
installation_command = Command.new(tool, :install, :no_silence).string if tool.installable?
|
80
|
+
install_link = tool.install_link
|
81
|
+
|
82
|
+
failure("Missing executable for '#{tool}'", command: command)
|
83
|
+
guidance('Try installing the tool:', installation_command)
|
84
|
+
guidance('Read the installation guidance:', install_link)
|
85
|
+
end
|
86
|
+
|
87
|
+
def syntax_guidance(ignore_link: nil, disable_link: nil)
|
88
|
+
guidance('Selectively Ignore a Rule:', ignore_link)
|
89
|
+
guidance('Fully Disable a Rule:', disable_link)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
# Clean formatter for logging to $stdout
|
5
|
+
class StandardOutFormatter < ::Logger::Formatter
|
6
|
+
# Overrides ::Logger::Formatter `call` to present output more concisely
|
7
|
+
# @param _severity [Logger::Severity] Unused - Logger severity for etnry
|
8
|
+
# @param _time [DateTime] Unused - Timestamp for entry
|
9
|
+
# @param _progname [String] Unused - Name of the current program for entry
|
10
|
+
# @param message [String] The string to print to $stdout
|
11
|
+
#
|
12
|
+
# @return [type] [description]
|
13
|
+
def call(_severity, _time, _progname, message)
|
14
|
+
"#{message}\n"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Logger for $stdout
|
19
|
+
class Printer < ::Logger
|
20
|
+
def initialize(formatter = StandardOutFormatter.new)
|
21
|
+
super($stdout)
|
22
|
+
@formatter = formatter
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/reviewer/runner.rb
CHANGED
@@ -1,102 +1,73 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'runner/strategies/quiet'
|
4
|
+
require_relative 'runner/strategies/verbose'
|
4
5
|
|
5
6
|
module Reviewer
|
6
|
-
#
|
7
|
+
# Wrapper for executng a command and printing the results
|
7
8
|
class Runner
|
8
|
-
|
9
|
+
extend Forwardable
|
9
10
|
|
10
|
-
attr_accessor :
|
11
|
+
attr_accessor :strategy
|
11
12
|
|
12
|
-
attr_reader :
|
13
|
-
:last_command_run,
|
14
|
-
:stdout,
|
15
|
-
:stderr,
|
16
|
-
:status,
|
17
|
-
:exit_status,
|
18
|
-
:logger
|
13
|
+
attr_reader :command, :shell, :output
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
15
|
+
def_delegators :@command, :tool
|
16
|
+
def_delegators :@shell, :result, :timer
|
17
|
+
def_delegators :result, :exit_status
|
18
|
+
|
19
|
+
def initialize(tool, command_type, strategy = Strategies::Quiet, output: Reviewer.output)
|
20
|
+
@command = Command.new(tool, command_type)
|
21
|
+
@strategy = strategy
|
22
|
+
@shell = Shell.new
|
23
|
+
@output = output
|
24
24
|
end
|
25
25
|
|
26
26
|
def run
|
27
|
-
|
27
|
+
# Show which tool is about to run
|
28
|
+
output.tool_summary(tool)
|
28
29
|
|
29
|
-
|
30
|
-
|
30
|
+
# Run the provided strategy
|
31
|
+
strategy.new(self).tap do |run_strategy|
|
32
|
+
run_strategy.prepare if run_prepare_step?
|
33
|
+
run_strategy.run
|
31
34
|
end
|
32
35
|
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
+
# If it failed,
|
37
|
+
guidance.show unless success?
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
def shell_out(cmd)
|
40
|
-
@stdout, @stderr, @status = Open3.capture3(cmd)
|
41
|
-
@exit_status = status.exitstatus
|
42
|
-
@last_command_run = cmd
|
43
|
-
end
|
44
|
-
|
45
|
-
def run_review
|
46
|
-
shell_out(tool.preparation_command) if tool.prepare_command?
|
47
|
-
shell_out(tool.review_command(seed: seed))
|
48
|
-
end
|
49
|
-
|
50
|
-
def run_format
|
51
|
-
shell_out(tool.format_command) if tool.format_command?
|
52
|
-
end
|
53
|
-
|
54
|
-
def review_verbosely
|
55
|
-
cmd = tool.review_command(:no_silence, seed: seed)
|
56
|
-
logger.rerunning(tool, cmd)
|
57
|
-
system(cmd)
|
58
|
-
end
|
59
|
-
|
60
|
-
def print_result
|
61
|
-
if status.success?
|
62
|
-
logger.success(elapsed_time)
|
63
|
-
else
|
64
|
-
recovery_guidance
|
65
|
-
end
|
39
|
+
exit_status
|
66
40
|
end
|
67
41
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
42
|
+
def success?
|
43
|
+
# Some review tools return a range of non-zero exit statuses and almost never return 0.
|
44
|
+
# (`yarn audit` is a good example.) Those tools can be configured to accept a non-zero exit
|
45
|
+
# status so they aren't constantly considered to be failing over minor issues.
|
46
|
+
#
|
47
|
+
# But when other command types (prepare, install, format) are run, they either succeed or they
|
48
|
+
# fail. With no shades of gray in those cases, anything other than a 0 is a failure.
|
49
|
+
if command.type == :review
|
50
|
+
exit_status <= tool.max_exit_status
|
73
51
|
else
|
74
|
-
|
52
|
+
exit_status.zero?
|
75
53
|
end
|
76
54
|
end
|
77
55
|
|
78
|
-
def
|
79
|
-
|
80
|
-
"Missing executable for '#{tool}'"
|
81
|
-
else
|
82
|
-
"Exit Status #{exit_status}"
|
83
|
-
end
|
56
|
+
def run_prepare_step?
|
57
|
+
command.type != :prepare && tool.prepare?
|
84
58
|
end
|
85
59
|
|
86
|
-
def
|
87
|
-
|
88
|
-
logger.guidance('Read the installation guidance:', tool.settings.links[:install]) if tool.install_link?
|
60
|
+
def prepare_command
|
61
|
+
@prepare_command ||= Command.new(tool, :prepare, command.verbosity)
|
89
62
|
end
|
90
63
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
64
|
+
def update_last_prepared_at
|
65
|
+
# Touch the `last_prepared_at` timestamp for the tool so it waits before running again.
|
66
|
+
tool.last_prepared_at = Time.now
|
94
67
|
end
|
95
68
|
|
96
|
-
def
|
97
|
-
|
98
|
-
# Otherwise, re-running after the failure will change the seed and show different results.
|
99
|
-
@seed ||= Random.rand(100_000)
|
69
|
+
def guidance
|
70
|
+
@guidance ||= Reviewer::Guidance.new(command: command, result: result, output: output)
|
100
71
|
end
|
101
72
|
end
|
102
73
|
end
|