quiet_quality 0.1.0 → 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/.quiet_quality.yml +6 -0
- data/bin/qq +1 -33
- data/lib/quiet_quality/cli/arg_parser.rb +135 -0
- data/lib/quiet_quality/cli/entrypoint.rb +95 -0
- data/lib/quiet_quality/config/builder.rb +123 -0
- data/lib/quiet_quality/config/finder.rb +45 -0
- data/lib/quiet_quality/config/options.rb +14 -0
- data/lib/quiet_quality/config/parsed_options.rb +36 -0
- data/lib/quiet_quality/config/parser.rb +134 -0
- data/lib/quiet_quality/config/tool_options.rb +34 -0
- data/lib/quiet_quality/config.rb +8 -0
- data/lib/quiet_quality/tools/brakeman/parser.rb +45 -0
- data/lib/quiet_quality/tools/brakeman/runner.rb +36 -0
- data/lib/quiet_quality/tools/brakeman.rb +13 -0
- data/lib/quiet_quality/tools/haml_lint/parser.rb +45 -0
- data/lib/quiet_quality/tools/haml_lint/runner.rb +63 -0
- data/lib/quiet_quality/tools/haml_lint.rb +11 -0
- data/lib/quiet_quality/tools/rspec/runner.rb +1 -1
- data/lib/quiet_quality/tools.rb +2 -0
- data/lib/quiet_quality/version.rb +1 -1
- data/lib/quiet_quality.rb +1 -0
- metadata +18 -6
- data/lib/quiet_quality/cli/option_parser.rb +0 -103
- data/lib/quiet_quality/cli/options.rb +0 -27
- data/lib/quiet_quality/cli/options_builder.rb +0 -49
- data/lib/quiet_quality/tool_options.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92fb13c8040117b5c0117af41e7253d793c2818a05c4d69b8f522677f20b96a7
|
4
|
+
data.tar.gz: 27da41d1430f7a09a22444fa117904f231b8feec6006c48393bd9d0cf9859707
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 020bb9aa46fc7a1d5989dbf7f03538533b6f54006e23003f90209a6baacbb03c015cee81f00893047bc7c39260788d62a4380157ee95b610acd08acd64746569
|
7
|
+
data.tar.gz: 68d20464b4f83e819ef8e8128420e84cadb857183238167aa4a9b4947890d571ec636836656bc728dc3b459b75668152e1c510f034a8ddc8d49e5403150ec3b3
|
data/.quiet_quality.yml
ADDED
data/bin/qq
CHANGED
@@ -1,35 +1,3 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require_relative "../lib/quiet_quality"
|
3
|
-
|
4
|
-
opt_parser = QuietQuality::Cli::OptionParser.new(ARGV)
|
5
|
-
tool_names, global_options, tool_options = opt_parser.parse!
|
6
|
-
options = QuietQuality::Cli::OptionsBuilder.new(tool_names: tool_names, global_options: global_options, tool_options: tool_options).options
|
7
|
-
|
8
|
-
executor = options.executor.new(tools: options.tools)
|
9
|
-
executor.execute!
|
10
|
-
|
11
|
-
executor.outcomes.each do |outcome|
|
12
|
-
result = outcome.success? ? "Passed" : "Failed"
|
13
|
-
warn "--- #{result}: #{outcome.tool}"
|
14
|
-
end
|
15
|
-
|
16
|
-
messages = executor.messages
|
17
|
-
if messages.any?
|
18
|
-
warn "\n\n#{messages.count} messages:"
|
19
|
-
messages.each do |msg|
|
20
|
-
line_range = msg.start_line == msg.stop_line ? msg.start_line.to_s : "#{msg.start_line}-#{msg.stop_line}"
|
21
|
-
body = msg.body.gsub(/ *\n */, "\\n").slice(0, 120)
|
22
|
-
warn " #{msg.path}:#{line_range} #{msg.rule}"
|
23
|
-
warn " #{body}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
if options.annotator
|
28
|
-
warn "\n\n"
|
29
|
-
options.annotator.new.annotate!(messages)
|
30
|
-
end
|
31
|
-
|
32
|
-
if executor.any_failure?
|
33
|
-
warn "failures detected in one or more tools"
|
34
|
-
exit(1)
|
35
|
-
end
|
3
|
+
exit(1) unless QuietQuality::Cli::Entrypoint.new(argv: ARGV).execute.successful?
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module QuietQuality
|
4
|
+
module Cli
|
5
|
+
class ArgParser
|
6
|
+
def initialize(args)
|
7
|
+
@args = args
|
8
|
+
@parsed_options = Config::ParsedOptions.new
|
9
|
+
@parsed = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def parsed_options
|
13
|
+
unless @parsed
|
14
|
+
parser.parse!(@args)
|
15
|
+
@parsed_options.tools = validated_tool_names(@args.dup).map(&:to_sym)
|
16
|
+
@parsed = true
|
17
|
+
end
|
18
|
+
@parsed_options
|
19
|
+
end
|
20
|
+
|
21
|
+
def help_text
|
22
|
+
@_help_text ||= parser.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def set_global_option(name, value)
|
28
|
+
@parsed_options.set_global_option(name, value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_tool_option(tool, name, value)
|
32
|
+
@parsed_options.set_tool_option(tool, name, value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_value_from(name, value, allowed)
|
36
|
+
return if allowed.include?(value.to_sym)
|
37
|
+
fail(UsageError, "Unrecognized #{name}: #{value}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def validated_tool_names(names)
|
41
|
+
names.each { |name| validate_value_from("tool", name, Tools::AVAILABLE) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# There are several flags that _may_ take a 'tool' argument - if they do, they are tool
|
45
|
+
# options; if they don't, they are global options. (optparse allows an optional argument
|
46
|
+
# to a flag if the string representing it is not a 'string in all caps'. So `[FOO]` or `foo`
|
47
|
+
# would be optional, but `FOO` would be required. This helper simplifies handling those.
|
48
|
+
def read_tool_or_global_option(name, tool, value)
|
49
|
+
if tool
|
50
|
+
validate_value_from("tool", tool, Tools::AVAILABLE)
|
51
|
+
set_tool_option(tool, name, value)
|
52
|
+
else
|
53
|
+
set_global_option(name, value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# -- Set up the option parser itself -------------------------
|
58
|
+
|
59
|
+
def parser
|
60
|
+
@_parser ||= ::OptionParser.new do |parser|
|
61
|
+
setup_banner(parser)
|
62
|
+
setup_help_output(parser)
|
63
|
+
setup_config_options(parser)
|
64
|
+
setup_executor_options(parser)
|
65
|
+
setup_annotation_options(parser)
|
66
|
+
setup_file_target_options(parser)
|
67
|
+
setup_filter_messages_options(parser)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def setup_banner(parser)
|
72
|
+
parser.banner = "Usage: qq [TOOLS] [GLOBAL_OPTIONS] [TOOL_OPTIONS]"
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_help_output(parser)
|
76
|
+
parser.on("-h", "--help", "Prints this help") do
|
77
|
+
@parsed_options.helping = true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup_config_options(parser)
|
82
|
+
parser.on("-C", "--config PATH", "Load a config file from this path") do |path|
|
83
|
+
set_global_option(:config_path, path)
|
84
|
+
end
|
85
|
+
|
86
|
+
parser.on("-N", "--no-config", "Do not load a config file, even if present") do
|
87
|
+
set_global_option(:no_config, true)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def setup_executor_options(parser)
|
92
|
+
parser.on("-E", "--executor EXECUTOR", "Which executor to use") do |name|
|
93
|
+
validate_value_from("executor", name, Executors::AVAILABLE)
|
94
|
+
set_global_option(:executor, name.to_sym)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def setup_annotation_options(parser)
|
99
|
+
parser.on("-A", "--annotate ANNOTATOR", "Annotate with this annotator") do |name|
|
100
|
+
validate_value_from("annotator", name, Annotators::ANNOTATOR_TYPES)
|
101
|
+
set_global_option(:annotator, name.to_sym)
|
102
|
+
end
|
103
|
+
|
104
|
+
# shortcut option
|
105
|
+
parser.on("-G", "--annotate-github-stdout", "Annotate with GitHub Workflow commands") do
|
106
|
+
set_global_option(:annotator, :github_stdout)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def setup_file_target_options(parser)
|
111
|
+
parser.on("-a", "--all-files [tool]", "Use the tool(s) on all files") do |tool|
|
112
|
+
read_tool_or_global_option(:all_files, tool, true)
|
113
|
+
end
|
114
|
+
|
115
|
+
parser.on("-c", "--changed-files [tool]", "Use the tool(s) only on changed files") do |tool|
|
116
|
+
read_tool_or_global_option(:all_files, tool, false)
|
117
|
+
end
|
118
|
+
|
119
|
+
parser.on("-B", "--comparison-branch BRANCH", "Specify the branch to compare against") do |branch|
|
120
|
+
set_global_option(:comparison_branch, branch)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def setup_filter_messages_options(parser)
|
125
|
+
parser.on("-f", "--filter-messages [tool]", "Filter messages from tool(s) based on changed lines") do |tool|
|
126
|
+
read_tool_or_global_option(:filter_messages, tool, true)
|
127
|
+
end
|
128
|
+
|
129
|
+
parser.on("-u", "--unfiltered [tool]", "Don't filter messages from tool(s)") do |tool|
|
130
|
+
read_tool_or_global_option(:filter_messages, tool, false)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Cli
|
3
|
+
class Entrypoint
|
4
|
+
def initialize(argv:, output_stream: $stdout, error_stream: $stderr)
|
5
|
+
@argv = argv
|
6
|
+
@output_stream = output_stream
|
7
|
+
@error_stream = error_stream
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute
|
11
|
+
if helping?
|
12
|
+
log_help_text
|
13
|
+
else
|
14
|
+
executed
|
15
|
+
log_outcomes
|
16
|
+
log_messages
|
17
|
+
annotate_messages
|
18
|
+
end
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def successful?
|
24
|
+
helping? || !executed.any_failure?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :argv, :output_stream, :error_stream
|
30
|
+
|
31
|
+
def arg_parser
|
32
|
+
@_arg_parser ||= ArgParser.new(argv.dup)
|
33
|
+
end
|
34
|
+
|
35
|
+
def parsed_options
|
36
|
+
@_parsed_options ||= arg_parser.parsed_options
|
37
|
+
end
|
38
|
+
|
39
|
+
def helping?
|
40
|
+
parsed_options.helping?
|
41
|
+
end
|
42
|
+
|
43
|
+
def log_help_text
|
44
|
+
error_stream.puts(arg_parser.help_text)
|
45
|
+
end
|
46
|
+
|
47
|
+
def options
|
48
|
+
return @_options if defined?(@_options)
|
49
|
+
builder = Config::Builder.new(parsed_cli_options: parsed_options)
|
50
|
+
@_options = builder.options
|
51
|
+
end
|
52
|
+
|
53
|
+
def executor
|
54
|
+
@_executor ||= options.executor.new(tools: options.tools)
|
55
|
+
end
|
56
|
+
|
57
|
+
def executed
|
58
|
+
return @_executed if defined?(@_executed)
|
59
|
+
executor.execute!
|
60
|
+
@_executed = executor
|
61
|
+
end
|
62
|
+
|
63
|
+
def log_outcomes
|
64
|
+
executed.outcomes.each do |outcome|
|
65
|
+
result = outcome.success? ? "Passed" : "Failed"
|
66
|
+
error_stream.puts "--- #{result}: #{outcome.tool}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def log_message(msg)
|
71
|
+
line_range =
|
72
|
+
if msg.start_line == msg.stop_line
|
73
|
+
msg.start_line.to_s
|
74
|
+
else
|
75
|
+
"#{msg.start_line}-#{msg.stop_line}"
|
76
|
+
end
|
77
|
+
rule_string = msg.rule ? " [#{msg.rule}]" : ""
|
78
|
+
truncated_body = msg.body.gsub(/ *\n */, "\\n").slice(0, 120)
|
79
|
+
error_stream.puts " #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def log_messages
|
83
|
+
return unless executed.messages.any?
|
84
|
+
error_stream.puts "\n\n#{executed.messages.count} messages:"
|
85
|
+
executed.messages.each { |msg| log_message(msg) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def annotate_messages
|
89
|
+
return unless options.annotator
|
90
|
+
annotator = options.annotator.new(output_stream: output_stream)
|
91
|
+
annotator.annotate!(executed.messages)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Config
|
3
|
+
class Builder
|
4
|
+
def initialize(parsed_cli_options:)
|
5
|
+
@cli = parsed_cli_options
|
6
|
+
end
|
7
|
+
|
8
|
+
def options
|
9
|
+
return @_options if defined?(@_options)
|
10
|
+
options = build_initial_options
|
11
|
+
Updater.new(options: options, apply: config_file).update! if config_file
|
12
|
+
Updater.new(options: options, apply: cli).update!
|
13
|
+
@_options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :cli
|
19
|
+
|
20
|
+
def build_initial_options
|
21
|
+
tools = tool_names.map { |name| ToolOptions.new(name) }
|
22
|
+
Options.new.tap { |opts| opts.tools = tools }
|
23
|
+
end
|
24
|
+
|
25
|
+
def tool_names
|
26
|
+
if cli.tools.any?
|
27
|
+
cli.tools
|
28
|
+
elsif config_file&.tools&.any?
|
29
|
+
config_file.tools
|
30
|
+
else
|
31
|
+
Tools::AVAILABLE.keys
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def config_finder
|
36
|
+
@_config_finder ||= Finder.new(from: ".")
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_path
|
40
|
+
return @_config_path if defined?(@_config_path)
|
41
|
+
|
42
|
+
@_config_path =
|
43
|
+
if cli.global_option(:no_config)
|
44
|
+
nil
|
45
|
+
elsif cli.global_option(:config_path)
|
46
|
+
cli.global_option(:config_path)
|
47
|
+
elsif config_finder.config_path
|
48
|
+
config_finder.config_path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def config_file
|
53
|
+
return @_parsed_config_options if defined?(@_parsed_config_options)
|
54
|
+
|
55
|
+
if config_path
|
56
|
+
parser = Parser.new(config_path)
|
57
|
+
@_parsed_config_options = parser.parsed_options
|
58
|
+
else
|
59
|
+
@_parsed_config_options = nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Updater
|
64
|
+
def initialize(options:, apply:)
|
65
|
+
@options, @apply = options, apply
|
66
|
+
end
|
67
|
+
|
68
|
+
def update!
|
69
|
+
update_globals
|
70
|
+
update_tools
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
attr_reader :options, :apply
|
76
|
+
|
77
|
+
def set_unless_nil(object, method, value)
|
78
|
+
return if value.nil?
|
79
|
+
object.send("#{method}=", value)
|
80
|
+
end
|
81
|
+
|
82
|
+
# ---- update the global options -------------
|
83
|
+
|
84
|
+
def update_globals
|
85
|
+
update_annotator
|
86
|
+
update_executor
|
87
|
+
update_comparison_branch
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_annotator
|
91
|
+
annotator_name = apply.global_option(:annotator)
|
92
|
+
return if annotator_name.nil?
|
93
|
+
options.annotator = Annotators::ANNOTATOR_TYPES.fetch(annotator_name)
|
94
|
+
end
|
95
|
+
|
96
|
+
def update_executor
|
97
|
+
executor_name = apply.global_option(:executor)
|
98
|
+
return if executor_name.nil?
|
99
|
+
options.executor = Executors::AVAILABLE.fetch(executor_name)
|
100
|
+
end
|
101
|
+
|
102
|
+
def update_comparison_branch
|
103
|
+
set_unless_nil(options, :comparison_branch, apply.global_option(:comparison_branch))
|
104
|
+
end
|
105
|
+
|
106
|
+
# ---- update the tool options (apply global forms first) -------
|
107
|
+
|
108
|
+
def update_tools
|
109
|
+
options.tools.each do |tool_options|
|
110
|
+
update_tool_option(tool_options, :limit_targets)
|
111
|
+
update_tool_option(tool_options, :filter_messages)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_tool_option(tool_options, option_name)
|
116
|
+
tool_name = tool_options.tool_name
|
117
|
+
set_unless_nil(tool_options, option_name, apply.global_option(option_name))
|
118
|
+
set_unless_nil(tool_options, option_name, apply.tool_option(tool_name, option_name))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Config
|
3
|
+
class Finder
|
4
|
+
CONFIG_FILENAME = ".quiet_quality.yml"
|
5
|
+
MAXIMUM_SEARCH_DEPTH = 100
|
6
|
+
|
7
|
+
def initialize(from:)
|
8
|
+
@from = from
|
9
|
+
end
|
10
|
+
|
11
|
+
def config_path
|
12
|
+
return @_config_path if defined?(@_config_path)
|
13
|
+
each_successive_enclosing_directory do |dir_path|
|
14
|
+
file_path = dir_path.join(CONFIG_FILENAME)
|
15
|
+
if file_path.exist?
|
16
|
+
return @_config_path = file_path.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@_config_path = nil
|
20
|
+
rescue Errno::EACCES
|
21
|
+
@_config_path = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :from
|
27
|
+
|
28
|
+
def config_path_within(dir)
|
29
|
+
File.join(dir, CONFIG_FILENAME)
|
30
|
+
end
|
31
|
+
|
32
|
+
def each_successive_enclosing_directory(max_depth: 100, &block)
|
33
|
+
d = Pathname.new(from)
|
34
|
+
depth = 0
|
35
|
+
MAXIMUM_SEARCH_DEPTH.times do
|
36
|
+
block.call(d.expand_path)
|
37
|
+
d = d.parent
|
38
|
+
depth += 1
|
39
|
+
return nil if d.root?
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Config
|
3
|
+
class Options
|
4
|
+
def initialize
|
5
|
+
@annotator = nil
|
6
|
+
@executor = Executors::ConcurrentExecutor
|
7
|
+
@tools = nil
|
8
|
+
@comparison_branch = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :tools, :comparison_branch, :annotator, :executor
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Config
|
3
|
+
class ParsedOptions
|
4
|
+
def initialize
|
5
|
+
@tools = []
|
6
|
+
@tool_options = {}
|
7
|
+
@global_options = {}
|
8
|
+
@helping = false
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :tools
|
12
|
+
attr_writer :helping
|
13
|
+
|
14
|
+
def helping?
|
15
|
+
@helping
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_global_option(name, value)
|
19
|
+
@global_options[name.to_sym] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def global_option(name)
|
23
|
+
@global_options.fetch(name.to_sym, nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_tool_option(tool, name, value)
|
27
|
+
@tool_options[tool.to_sym] ||= {}
|
28
|
+
@tool_options[tool.to_sym][name.to_sym] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def tool_option(tool, name)
|
32
|
+
@tool_options.dig(tool.to_sym, name.to_sym)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Config
|
3
|
+
class Parser
|
4
|
+
InvalidConfig = Class.new(Config::Error)
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def parsed_options
|
11
|
+
@_parsed_options ||= ParsedOptions.new.tap do |opts|
|
12
|
+
store_default_tools(opts)
|
13
|
+
store_global_options(opts)
|
14
|
+
store_tool_options(opts)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :path
|
21
|
+
|
22
|
+
def text
|
23
|
+
@_text ||= File.read(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def data
|
27
|
+
@_data ||= YAML.safe_load(text, symbolize_names: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def store_default_tools(opts)
|
31
|
+
tool_names = data.fetch(:default_tools, [])
|
32
|
+
invalid!("default_tools must be an array") unless tool_names.is_a?(Array)
|
33
|
+
tool_names.each do |name|
|
34
|
+
invalid!("each default tool must be a string") unless name.is_a?(String)
|
35
|
+
invalid!("unrecognized tool name '#{name}'") unless valid_tool?(name)
|
36
|
+
end
|
37
|
+
opts.tools = tool_names.map(&:to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
def store_global_options(opts)
|
41
|
+
read_global_option(opts, :executor, as: :symbol, validate_from: Executors::AVAILABLE)
|
42
|
+
read_global_option(opts, :annotator, as: :symbol, validate_from: Annotators::ANNOTATOR_TYPES)
|
43
|
+
read_global_option(opts, :comparison_branch, as: :string)
|
44
|
+
read_global_option(opts, :changed_files, as: :boolean)
|
45
|
+
read_global_option(opts, :filter_messages, as: :boolean)
|
46
|
+
end
|
47
|
+
|
48
|
+
def store_tool_options(opts)
|
49
|
+
Tools::AVAILABLE.keys.each do |tool_name|
|
50
|
+
store_tool_options_for(opts, tool_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def store_tool_options_for(opts, tool_name)
|
55
|
+
entries = data.fetch(tool_name, nil)
|
56
|
+
return if entries.nil?
|
57
|
+
read_tool_option(opts, tool_name, :filter_messages, as: :boolean)
|
58
|
+
read_tool_option(opts, tool_name, :changed_files, as: :boolean)
|
59
|
+
end
|
60
|
+
|
61
|
+
def invalid!(message)
|
62
|
+
fail(InvalidConfig, message)
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid_tool?(name)
|
66
|
+
Tools::AVAILABLE.key?(name.to_sym)
|
67
|
+
end
|
68
|
+
|
69
|
+
def valid_boolean?(value)
|
70
|
+
[true, false].include?(value)
|
71
|
+
end
|
72
|
+
|
73
|
+
def read_global_option(opts, name, as:, validate_from: nil)
|
74
|
+
parsed_value = data.fetch(name.to_sym, nil)
|
75
|
+
return if parsed_value.nil?
|
76
|
+
|
77
|
+
validate_value(name, parsed_value, as: as, from: validate_from)
|
78
|
+
coerced_value = coerce_value(parsed_value, as: as)
|
79
|
+
opts.set_global_option(name, coerced_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_tool_option(opts, tool, name, as:)
|
83
|
+
parsed_value = data.dig(tool.to_sym, name.to_sym)
|
84
|
+
return if parsed_value.nil?
|
85
|
+
|
86
|
+
validate_value("#{tool}.#{name}", parsed_value, as: as)
|
87
|
+
coerced_value = coerce_value(parsed_value, as: as)
|
88
|
+
opts.set_tool_option(tool, name, coerced_value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_value(name, value, as:, from: nil)
|
92
|
+
case as
|
93
|
+
when :boolean then validate_boolean(name, value)
|
94
|
+
when :symbol then validate_symbol(name, value, from: from)
|
95
|
+
when :string then validate_string(name, value)
|
96
|
+
else
|
97
|
+
fail ArgumentError, "validate_value does not handle type #{as}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_boolean(name, value)
|
102
|
+
return if valid_boolean?(value)
|
103
|
+
invalid!("option #{name} must be either true or false")
|
104
|
+
end
|
105
|
+
|
106
|
+
def validate_symbol(name, value, from: nil)
|
107
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
108
|
+
invalid!("option #{name} must be a string or symbol")
|
109
|
+
end
|
110
|
+
|
111
|
+
unless from.nil? || from.include?(value.to_sym)
|
112
|
+
allowed_list = from.respond_to?(:keys) ? from.keys : from
|
113
|
+
allowed_string = allowed_list.map(&:to_s).join(", ")
|
114
|
+
invalid!("option #{name} must be one of the allowed values: #{allowed_string}")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate_string(name, value)
|
119
|
+
invalid!("option #{name} must be a string") unless value.is_a?(String)
|
120
|
+
invalid!("option #{name} must not be empty") if value.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
def coerce_value(value, as:)
|
124
|
+
case as
|
125
|
+
when :boolean then !!value
|
126
|
+
when :string then value.to_s
|
127
|
+
when :symbol then value.to_sym
|
128
|
+
else
|
129
|
+
fail ArgumentError, "coerce_value does not handle type #{as}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Config
|
3
|
+
class ToolOptions
|
4
|
+
def initialize(tool, limit_targets: true, filter_messages: true)
|
5
|
+
@tool_name = tool.to_sym
|
6
|
+
@limit_targets = limit_targets
|
7
|
+
@filter_messages = filter_messages
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :tool_name
|
11
|
+
attr_writer :limit_targets, :filter_messages
|
12
|
+
|
13
|
+
def limit_targets?
|
14
|
+
@limit_targets
|
15
|
+
end
|
16
|
+
|
17
|
+
def filter_messages?
|
18
|
+
@filter_messages
|
19
|
+
end
|
20
|
+
|
21
|
+
def tool_namespace
|
22
|
+
Tools::AVAILABLE.fetch(tool_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def runner_class
|
26
|
+
tool_namespace::Runner
|
27
|
+
end
|
28
|
+
|
29
|
+
def parser_class
|
30
|
+
tool_namespace::Parser
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Tools
|
3
|
+
module Brakeman
|
4
|
+
class Parser
|
5
|
+
def initialize(text)
|
6
|
+
@text = text
|
7
|
+
end
|
8
|
+
|
9
|
+
def messages
|
10
|
+
return @_messages if defined?(@_messages)
|
11
|
+
check_errors!
|
12
|
+
messages = warnings.map { |w| message_for(w) }
|
13
|
+
@_messages = Messages.new(messages)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :text
|
19
|
+
|
20
|
+
def data
|
21
|
+
@_data ||= JSON.parse(text, symbolize_names: true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_errors!
|
25
|
+
errors = data[:errors]
|
26
|
+
return if errors.nil? || errors.empty?
|
27
|
+
fail(ParsingError, "Found #{errors.length} errors in brakeman output")
|
28
|
+
end
|
29
|
+
|
30
|
+
def warnings
|
31
|
+
data[:warnings] || []
|
32
|
+
end
|
33
|
+
|
34
|
+
def message_for(warning)
|
35
|
+
path = warning.fetch(:file)
|
36
|
+
body = warning.fetch(:message)
|
37
|
+
line = warning.fetch(:line)
|
38
|
+
level = warning.fetch(:confidence, nil)
|
39
|
+
rule = warning.fetch(:warning_type)
|
40
|
+
Message.new(path: path, body: body, start_line: line, level: level, rule: rule)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Tools
|
3
|
+
module Brakeman
|
4
|
+
class Runner
|
5
|
+
# These are specified in constants at the top of brakeman.rb:
|
6
|
+
# https://github.com/presidentbeef/brakeman/blob/main/lib/brakeman.rb#L6-L25
|
7
|
+
KNOWN_EXIT_STATUSES = [3, 4, 5, 6, 7, 8].to_set
|
8
|
+
|
9
|
+
def initialize(changed_files: nil)
|
10
|
+
@changed_files = changed_files
|
11
|
+
end
|
12
|
+
|
13
|
+
def invoke!
|
14
|
+
@_outcome ||= performed_outcome
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def command
|
20
|
+
["brakeman", "-f", "json"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def performed_outcome
|
24
|
+
out, err, stat = Open3.capture3(*command)
|
25
|
+
if stat.success?
|
26
|
+
Outcome.new(tool: :brakeman, output: out, logging: err)
|
27
|
+
elsif KNOWN_EXIT_STATUSES.include?(stat.exitstatus)
|
28
|
+
Outcome.new(tool: :brakeman, output: out, logging: err, failure: true)
|
29
|
+
else
|
30
|
+
fail(ExecutionError, "Execution of brakeman failed with #{stat.exitstatus}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative "./rubocop"
|
2
|
+
|
3
|
+
module QuietQuality
|
4
|
+
module Tools
|
5
|
+
module Brakeman
|
6
|
+
ExecutionError = Class.new(Tools::Error)
|
7
|
+
ParsingError = Class.new(Tools::Error)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
glob = File.expand_path("../brakeman/*.rb", __FILE__)
|
13
|
+
Dir.glob(glob).sort.each { |f| require f }
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Tools
|
3
|
+
module HamlLint
|
4
|
+
class Parser
|
5
|
+
def initialize(text)
|
6
|
+
@text = text
|
7
|
+
end
|
8
|
+
|
9
|
+
def messages
|
10
|
+
return @_messages if defined?(@_messages)
|
11
|
+
messages = content
|
12
|
+
.fetch(:files)
|
13
|
+
.map { |f| messages_for_file(f) }
|
14
|
+
.flatten
|
15
|
+
@_messages = Messages.new(messages)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :text
|
21
|
+
|
22
|
+
def content
|
23
|
+
@_content ||= JSON.parse(text, symbolize_names: true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def messages_for_file(file_details)
|
27
|
+
path = file_details.fetch(:path)
|
28
|
+
file_details.fetch(:offenses).map do |offense|
|
29
|
+
message_for_offense(path, offense)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def message_for_offense(path, offense)
|
34
|
+
Message.new(
|
35
|
+
path: path,
|
36
|
+
body: offense.fetch(:message),
|
37
|
+
start_line: offense.dig(:location, :line),
|
38
|
+
level: offense.fetch(:severity, nil),
|
39
|
+
rule: offense.fetch(:linter_name, nil)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Tools
|
3
|
+
module HamlLint
|
4
|
+
class Runner
|
5
|
+
MAX_FILES = 100
|
6
|
+
NO_FILES_OUTPUT = %({"files": []})
|
7
|
+
|
8
|
+
# haml-lint uses the `sysexits` gem, and exits with Sysexits::EX_DATAERR for the
|
9
|
+
# failures case here in lib/haml_lint/cli.rb. That's mapped to status 65 - other
|
10
|
+
# statuses have other failure meanings, which we don't want to interpret as "problems
|
11
|
+
# encountered"
|
12
|
+
FAILURE_STATUS = 65
|
13
|
+
|
14
|
+
def initialize(changed_files: nil)
|
15
|
+
@changed_files = changed_files
|
16
|
+
end
|
17
|
+
|
18
|
+
def invoke!
|
19
|
+
@_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :changed_files
|
25
|
+
|
26
|
+
def skip_execution?
|
27
|
+
changed_files && relevant_files.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def relevant_files
|
31
|
+
return nil if changed_files.nil?
|
32
|
+
changed_files.paths.select { |path| path.end_with?(".haml") }
|
33
|
+
end
|
34
|
+
|
35
|
+
def target_files
|
36
|
+
return [] if changed_files.nil?
|
37
|
+
return [] if relevant_files.length > MAX_FILES
|
38
|
+
relevant_files
|
39
|
+
end
|
40
|
+
|
41
|
+
def command
|
42
|
+
return nil if skip_execution?
|
43
|
+
["haml-lint", "--reporter", "json"] + target_files.sort
|
44
|
+
end
|
45
|
+
|
46
|
+
def skipped_outcome
|
47
|
+
Outcome.new(tool: :haml_lint, output: NO_FILES_OUTPUT)
|
48
|
+
end
|
49
|
+
|
50
|
+
def performed_outcome
|
51
|
+
out, err, stat = Open3.capture3(*command)
|
52
|
+
if stat.success?
|
53
|
+
Outcome.new(tool: :haml_lint, output: out, logging: err)
|
54
|
+
elsif stat.exitstatus == FAILURE_STATUS
|
55
|
+
Outcome.new(tool: :haml_lint, output: out, logging: err, failure: true)
|
56
|
+
else
|
57
|
+
fail(ExecutionError, "Execution of haml-lint failed with #{stat.exitstatus}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Tools
|
3
|
+
module HamlLint
|
4
|
+
ExecutionError = Class.new(Tools::Error)
|
5
|
+
ParsingError = Class.new(Tools::Error)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
glob = File.expand_path("../haml_lint/*.rb", __FILE__)
|
11
|
+
Dir.glob(glob).sort.each { |f| require f }
|
data/lib/quiet_quality/tools.rb
CHANGED
data/lib/quiet_quality.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quiet_quality
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Mueller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-05-
|
11
|
+
date: 2023-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: git
|
@@ -150,6 +150,7 @@ files:
|
|
150
150
|
- ".github/workflows/linters.yml"
|
151
151
|
- ".github/workflows/rspec.yml"
|
152
152
|
- ".gitignore"
|
153
|
+
- ".quiet_quality.yml"
|
153
154
|
- ".rspec"
|
154
155
|
- ".rubocop.yml"
|
155
156
|
- Gemfile
|
@@ -163,9 +164,15 @@ files:
|
|
163
164
|
- lib/quiet_quality/changed_file.rb
|
164
165
|
- lib/quiet_quality/changed_files.rb
|
165
166
|
- lib/quiet_quality/cli.rb
|
166
|
-
- lib/quiet_quality/cli/
|
167
|
-
- lib/quiet_quality/cli/
|
168
|
-
- lib/quiet_quality/
|
167
|
+
- lib/quiet_quality/cli/arg_parser.rb
|
168
|
+
- lib/quiet_quality/cli/entrypoint.rb
|
169
|
+
- lib/quiet_quality/config.rb
|
170
|
+
- lib/quiet_quality/config/builder.rb
|
171
|
+
- lib/quiet_quality/config/finder.rb
|
172
|
+
- lib/quiet_quality/config/options.rb
|
173
|
+
- lib/quiet_quality/config/parsed_options.rb
|
174
|
+
- lib/quiet_quality/config/parser.rb
|
175
|
+
- lib/quiet_quality/config/tool_options.rb
|
169
176
|
- lib/quiet_quality/executors.rb
|
170
177
|
- lib/quiet_quality/executors/base_executor.rb
|
171
178
|
- lib/quiet_quality/executors/concurrent_executor.rb
|
@@ -174,8 +181,13 @@ files:
|
|
174
181
|
- lib/quiet_quality/message.rb
|
175
182
|
- lib/quiet_quality/message_filter.rb
|
176
183
|
- lib/quiet_quality/messages.rb
|
177
|
-
- lib/quiet_quality/tool_options.rb
|
178
184
|
- lib/quiet_quality/tools.rb
|
185
|
+
- lib/quiet_quality/tools/brakeman.rb
|
186
|
+
- lib/quiet_quality/tools/brakeman/parser.rb
|
187
|
+
- lib/quiet_quality/tools/brakeman/runner.rb
|
188
|
+
- lib/quiet_quality/tools/haml_lint.rb
|
189
|
+
- lib/quiet_quality/tools/haml_lint/parser.rb
|
190
|
+
- lib/quiet_quality/tools/haml_lint/runner.rb
|
179
191
|
- lib/quiet_quality/tools/outcome.rb
|
180
192
|
- lib/quiet_quality/tools/rspec.rb
|
181
193
|
- lib/quiet_quality/tools/rspec/parser.rb
|
@@ -1,103 +0,0 @@
|
|
1
|
-
require "optparse"
|
2
|
-
|
3
|
-
module QuietQuality
|
4
|
-
module Cli
|
5
|
-
class OptionParser
|
6
|
-
attr_reader :options, :tool_options, :output
|
7
|
-
|
8
|
-
def initialize(args)
|
9
|
-
@args = args
|
10
|
-
@options = {
|
11
|
-
executor: :concurrent
|
12
|
-
}
|
13
|
-
@tool_options = {}
|
14
|
-
@output = nil
|
15
|
-
end
|
16
|
-
|
17
|
-
def parse!
|
18
|
-
parser.parse!(@args)
|
19
|
-
[positional, options, tool_options]
|
20
|
-
end
|
21
|
-
|
22
|
-
def positional
|
23
|
-
@args
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def parser
|
29
|
-
::OptionParser.new do |parser|
|
30
|
-
setup_banner(parser)
|
31
|
-
setup_help_output(parser)
|
32
|
-
setup_executor_options(parser)
|
33
|
-
setup_annotation_options(parser)
|
34
|
-
setup_file_target_options(parser)
|
35
|
-
setup_filter_messages_options(parser)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def setup_banner(parser)
|
40
|
-
parser.banner = "Usage: qq [TOOLS] [GLOBAL_OPTIONS] [TOOL_OPTIONS]"
|
41
|
-
end
|
42
|
-
|
43
|
-
def setup_help_output(parser)
|
44
|
-
parser.on("-h", "--help", "Prints this help") do
|
45
|
-
@output = parser.to_s
|
46
|
-
@options[:exit_immediately] = true
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def setup_executor_options(parser)
|
51
|
-
parser.on("-E", "--executor EXECUTOR", "Which executor to use") do |name|
|
52
|
-
fail(UsageError, "Executor not recognized: #{name}") unless Executors::AVAILABLE.include?(name.to_sym)
|
53
|
-
@options[:executor] = name.to_sym
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def setup_annotation_options(parser)
|
58
|
-
parser.on("-A", "--annotate ANNOTATOR", "Annotate with this annotator") do |name|
|
59
|
-
fail(UsageError, "Annotator not recognized: #{name}") unless Annotators::ANNOTATOR_TYPES.include?(name.to_sym)
|
60
|
-
@options[:annotator] = name.to_sym
|
61
|
-
end
|
62
|
-
|
63
|
-
# shortcut option
|
64
|
-
parser.on("-G", "--annotate-github-stdout", "Annotate with GitHub Workflow commands") do
|
65
|
-
@options[:annotator] = :github_stdout
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def read_tool_or_global_option(name, tool, value)
|
70
|
-
if tool
|
71
|
-
@tool_options[tool.to_sym] ||= {}
|
72
|
-
@tool_options[tool.to_sym][name] = value
|
73
|
-
else
|
74
|
-
@options[name] = value
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def setup_file_target_options(parser)
|
79
|
-
parser.on("-a", "--all-files [tool]", "Use the tool(s) on all files") do |tool|
|
80
|
-
read_tool_or_global_option(:all_files, tool, true)
|
81
|
-
end
|
82
|
-
|
83
|
-
parser.on("-c", "--changed-files [tool]", "Use the tool(s) only on changed files") do |tool|
|
84
|
-
read_tool_or_global_option(:all_files, tool, false)
|
85
|
-
end
|
86
|
-
|
87
|
-
parser.on("-B", "--comparison-branch BRANCH", "Specify the branch to compare against") do |branch|
|
88
|
-
@options[:comparison_branch] = branch
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def setup_filter_messages_options(parser)
|
93
|
-
parser.on("-f", "--filter-messages [tool]", "Filter messages from tool(s) based on changed lines") do |tool|
|
94
|
-
read_tool_or_global_option(:filter_messages, tool, true)
|
95
|
-
end
|
96
|
-
|
97
|
-
parser.on("-u", "--unfiltered [tool]", "Don't filter messages from tool(s)") do |tool|
|
98
|
-
read_tool_or_global_option(:filter_messages, tool, false)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
module QuietQuality
|
2
|
-
module Cli
|
3
|
-
class Options
|
4
|
-
def initialize
|
5
|
-
@annotator = nil
|
6
|
-
@executor = Executors::ConcurrentExecutor
|
7
|
-
@tools = nil
|
8
|
-
@comparison_branch = nil
|
9
|
-
end
|
10
|
-
|
11
|
-
attr_reader :annotator, :executor
|
12
|
-
attr_accessor :tools, :comparison_branch
|
13
|
-
|
14
|
-
def annotator=(name)
|
15
|
-
@annotator = Annotators::ANNOTATOR_TYPES.fetch(name.to_sym)
|
16
|
-
rescue KeyError
|
17
|
-
fail(UsageError, "Unrecognized annotator: #{name}")
|
18
|
-
end
|
19
|
-
|
20
|
-
def executor=(name)
|
21
|
-
@executor = Executors::AVAILABLE.fetch(name.to_sym)
|
22
|
-
rescue KeyError
|
23
|
-
fail(UsageError, "Unrecognized executor: #{name}")
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module QuietQuality
|
2
|
-
module Cli
|
3
|
-
class OptionsBuilder
|
4
|
-
def initialize(tool_names:, global_options:, tool_options:)
|
5
|
-
@raw_tool_names = tool_names
|
6
|
-
@raw_global_options = global_options
|
7
|
-
@raw_tool_options = tool_options
|
8
|
-
end
|
9
|
-
|
10
|
-
def options
|
11
|
-
return @_options if defined?(@_options)
|
12
|
-
options = Options.new
|
13
|
-
set_unless_nil(options, :annotator, @raw_global_options[:annotator])
|
14
|
-
set_unless_nil(options, :executor, @raw_global_options[:executor])
|
15
|
-
set_unless_nil(options, :comparison_branch, @raw_global_options[:comparison_branch])
|
16
|
-
options.tools = tool_names.map { |tool_name| tool_options_for(tool_name) }
|
17
|
-
@_options = options
|
18
|
-
end
|
19
|
-
|
20
|
-
private
|
21
|
-
|
22
|
-
def set_unless_nil(object, method, value)
|
23
|
-
return if value.nil?
|
24
|
-
object.send("#{method}=", value)
|
25
|
-
end
|
26
|
-
|
27
|
-
def tool_options_for(tool_name)
|
28
|
-
raw_tool_opts = @raw_tool_options.fetch(tool_name.to_sym, {})
|
29
|
-
ToolOptions.new(tool_name).tap do |tool_options|
|
30
|
-
set_unless_nil(tool_options, :limit_targets, @raw_global_options[:limit_targets])
|
31
|
-
set_unless_nil(tool_options, :limit_targets, raw_tool_opts[:limit_targets])
|
32
|
-
|
33
|
-
set_unless_nil(tool_options, :filter_messages, @raw_global_options[:filter_messages])
|
34
|
-
set_unless_nil(tool_options, :filter_messages, raw_tool_opts[:filter_messages])
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def tool_names
|
39
|
-
names = @raw_tool_names.empty? ? Tools::AVAILABLE.keys : @raw_tool_names
|
40
|
-
names.map(&:to_sym).tap do |names|
|
41
|
-
unexpected_names = names - Tools::AVAILABLE.keys
|
42
|
-
if unexpected_names.any?
|
43
|
-
fail(UsageError, "Tool(s) not recognized: #{unexpected_names.join(", ")}")
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module QuietQuality
|
2
|
-
class ToolOptions
|
3
|
-
def initialize(tool, limit_targets: true, filter_messages: true)
|
4
|
-
@tool_name = tool.to_sym
|
5
|
-
@limit_targets = limit_targets
|
6
|
-
@filter_messages = filter_messages
|
7
|
-
end
|
8
|
-
|
9
|
-
attr_reader :tool_name
|
10
|
-
attr_writer :limit_targets, :filter_messages
|
11
|
-
|
12
|
-
def limit_targets?
|
13
|
-
@limit_targets
|
14
|
-
end
|
15
|
-
|
16
|
-
def filter_messages?
|
17
|
-
@filter_messages
|
18
|
-
end
|
19
|
-
|
20
|
-
def tool_namespace
|
21
|
-
Tools::AVAILABLE.fetch(tool_name)
|
22
|
-
end
|
23
|
-
|
24
|
-
def runner_class
|
25
|
-
tool_namespace::Runner
|
26
|
-
end
|
27
|
-
|
28
|
-
def parser_class
|
29
|
-
tool_namespace::Parser
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|