quiet_quality 0.1.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 +7 -0
- data/.github/workflows/dogfood.yml +28 -0
- data/.github/workflows/linters.yml +31 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +34 -0
- data/bin/qq +35 -0
- data/lib/quiet_quality/annotation_locator.rb +33 -0
- data/lib/quiet_quality/annotators/github_stdout.rb +36 -0
- data/lib/quiet_quality/annotators.rb +19 -0
- data/lib/quiet_quality/changed_file.rb +40 -0
- data/lib/quiet_quality/changed_files.rb +35 -0
- data/lib/quiet_quality/cli/option_parser.rb +103 -0
- data/lib/quiet_quality/cli/options.rb +27 -0
- data/lib/quiet_quality/cli/options_builder.rb +49 -0
- data/lib/quiet_quality/cli.rb +12 -0
- data/lib/quiet_quality/executors/base_executor.rb +46 -0
- data/lib/quiet_quality/executors/concurrent_executor.rb +20 -0
- data/lib/quiet_quality/executors/pipeline.rb +59 -0
- data/lib/quiet_quality/executors/serial_executor.rb +15 -0
- data/lib/quiet_quality/executors.rb +17 -0
- data/lib/quiet_quality/message.rb +25 -0
- data/lib/quiet_quality/message_filter.rb +33 -0
- data/lib/quiet_quality/messages.rb +54 -0
- data/lib/quiet_quality/tool_options.rb +32 -0
- data/lib/quiet_quality/tools/outcome.rb +26 -0
- data/lib/quiet_quality/tools/rspec/parser.rb +45 -0
- data/lib/quiet_quality/tools/rspec/runner.rb +57 -0
- data/lib/quiet_quality/tools/rspec.rb +11 -0
- data/lib/quiet_quality/tools/rubocop/parser.rb +46 -0
- data/lib/quiet_quality/tools/rubocop/runner.rb +67 -0
- data/lib/quiet_quality/tools/rubocop.rb +11 -0
- data/lib/quiet_quality/tools/standardrb/parser.rb +8 -0
- data/lib/quiet_quality/tools/standardrb/runner.rb +11 -0
- data/lib/quiet_quality/tools/standardrb.rb +13 -0
- data/lib/quiet_quality/tools.rb +21 -0
- data/lib/quiet_quality/version.rb +3 -0
- data/lib/quiet_quality/version_control_systems/git.rb +117 -0
- data/lib/quiet_quality/version_control_systems.rb +8 -0
- data/lib/quiet_quality.rb +15 -0
- data/quiet_quality.gemspec +44 -0
- data/tmp/.gitkeep +0 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f9c943644f5453e92b2b839bf42f8f031f67b13d6c8385c89822b17b02dccf60
|
4
|
+
data.tar.gz: 780325cd5db5fdac051880d404756887e60882929503e76876f178945d167f92
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3afd25c6af0810f3c6195ba0947173be2b926e1d689d7e095458c18e97e445a40935ce62cb730195a0b33a93f57568d4328a4260651183df08a7cac41b5cb8eb
|
7
|
+
data.tar.gz: c345f28884539737a9d37b1aaa8480ee06957c43d6b83b8eaa7d302855285f3f098f8f2b110b880da54cfdec7e4105a81a9701358494e92b9425849fc7231576
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: QuietQuality Itself
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
QuietQuality:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v3
|
10
|
+
|
11
|
+
- name: Set up ruby
|
12
|
+
uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: 3.2
|
15
|
+
|
16
|
+
- name: Cache gems
|
17
|
+
uses: actions/cache@v3
|
18
|
+
with:
|
19
|
+
path: vendor/bundle
|
20
|
+
key: ${{ runner.os }}-dogfood-${{ hashFiles('Gemfile.lock') }}
|
21
|
+
restore-keys:
|
22
|
+
${{ runner.os }}-dogfood-
|
23
|
+
|
24
|
+
- name: Install gems
|
25
|
+
run: bundle install --jobs 4 --retry 3
|
26
|
+
|
27
|
+
- name: Run QuietQuality
|
28
|
+
run: bundle exec bin/qq standardrb rubocop rspec --all-files --unfiltered --annotate github_stdout
|
@@ -0,0 +1,31 @@
|
|
1
|
+
name: Linters
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
StandardRB:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v3
|
10
|
+
|
11
|
+
- name: Set up ruby
|
12
|
+
uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: 3.2
|
15
|
+
|
16
|
+
- name: Cache gems
|
17
|
+
uses: actions/cache@v3
|
18
|
+
with:
|
19
|
+
path: vendor/bundle
|
20
|
+
key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.lock') }}
|
21
|
+
restore-keys:
|
22
|
+
${{ runner.os }}-linters-
|
23
|
+
|
24
|
+
- name: Install gems
|
25
|
+
run: bundle install --jobs 4 --retry 3
|
26
|
+
|
27
|
+
- name: Run standard
|
28
|
+
run: bundle exec standardrb
|
29
|
+
|
30
|
+
- name: Run rubocop (complexity checks)
|
31
|
+
run: bundle exec rubocop --parallel
|
@@ -0,0 +1,33 @@
|
|
1
|
+
name: RSpec
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
RSpec:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
ruby-version: ['2.7', '3.0', '3.1', '3.2', 'head']
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v3
|
15
|
+
|
16
|
+
- name: Set up ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
20
|
+
|
21
|
+
- name: Cache gems
|
22
|
+
uses: actions/cache@v3
|
23
|
+
with:
|
24
|
+
path: vendor/bundle
|
25
|
+
key: ${{ runner.os }}-rspec-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
|
26
|
+
restore-keys:
|
27
|
+
${{ runner.os }}-rspec-${{ matrix.ruby-version }}-
|
28
|
+
|
29
|
+
- name: Install gems
|
30
|
+
run: bundle install --jobs 4 --retry 3
|
31
|
+
|
32
|
+
- name: Run RSpec
|
33
|
+
run: bundle exec rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
SuggestExtensions: false
|
4
|
+
DisabledByDefault: true
|
5
|
+
|
6
|
+
Metrics/AbcSize:
|
7
|
+
Max: 15
|
8
|
+
Metrics/CyclomaticComplexity:
|
9
|
+
Max: 8
|
10
|
+
Metrics/PerceivedComplexity:
|
11
|
+
Max: 7
|
12
|
+
|
13
|
+
Metrics/ClassLength:
|
14
|
+
CountComments: false
|
15
|
+
Max: 150
|
16
|
+
Metrics/MethodLength:
|
17
|
+
CountComments: false
|
18
|
+
Max: 15
|
19
|
+
Metrics/ParameterLists:
|
20
|
+
Max: 5
|
21
|
+
CountKeywordArgs: true
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 Eric Mueller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# QuietQuality
|
2
|
+
|
3
|
+
Work in Progress
|
4
|
+
|
5
|
+
But essentially, QuietQuality is intended for two purposes:
|
6
|
+
|
7
|
+
1. Let you conveniently run tools like rubocop, rspec, and standard against the _relevant_
|
8
|
+
files locally (the files that have changed locally relative to the default branch)
|
9
|
+
2. Let you run those tools in CI (probably github actions) and annotate any issues found
|
10
|
+
with _new or modified_ code, without bothering you about existing issues that you didn't
|
11
|
+
touch.
|
12
|
+
|
13
|
+
|
14
|
+
Basic usage examples:
|
15
|
+
|
16
|
+
```
|
17
|
+
# you have five commits in your feature branch and 3 more files changes but not committed.
|
18
|
+
# this will run rubocop against all of those files.
|
19
|
+
qq rubocop
|
20
|
+
|
21
|
+
# run rspec against the changed specs, and annotate any failing specs (well, the first 10
|
22
|
+
# of them) against the commit using github's inline output-based annotation approach. Which
|
23
|
+
# will of course only produce actual annotations if this happens to have been run in a
|
24
|
+
# github action.
|
25
|
+
qq rspec --annotate=stdout
|
26
|
+
|
27
|
+
# run standardrb against all of the files (not just the changed ones). Still only print out
|
28
|
+
# problems to lines that have changed, so not particularly useful :-)
|
29
|
+
qq standard --all --incremental
|
30
|
+
|
31
|
+
# run all of the tools against the entire repository, and print the first three messages
|
32
|
+
# out for each tool.
|
33
|
+
qq all --all --full --limit-per-tool=3
|
34
|
+
```
|
data/bin/qq
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
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
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
class AnnotationLocator
|
3
|
+
def initialize(changed_files:)
|
4
|
+
@changed_files = changed_files
|
5
|
+
end
|
6
|
+
|
7
|
+
def update!(message)
|
8
|
+
changed_file = changed_files.file(message.path)
|
9
|
+
message.annotated_line = changed_file ? file_line_for(message, changed_file) : nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_all!(messages)
|
13
|
+
messages.map { |m| update!(m) }.compact.length
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :changed_files
|
19
|
+
|
20
|
+
def file_line_for(message, changed_file)
|
21
|
+
return message.stop_line if changed_file.entire?
|
22
|
+
message_range = (message.start_line..message.stop_line)
|
23
|
+
last_match(changed_file.line_numbers, message_range)
|
24
|
+
end
|
25
|
+
|
26
|
+
def last_match(array, range)
|
27
|
+
array.reverse_each do |value|
|
28
|
+
return value if range.cover?(value)
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Annotators
|
3
|
+
class GithubStdout
|
4
|
+
# github will only accept the first 10 annotations of each type in this form.
|
5
|
+
MAX_ANNOTATIONS = 10
|
6
|
+
|
7
|
+
def initialize(output_stream: $stdout)
|
8
|
+
@output_stream = output_stream
|
9
|
+
end
|
10
|
+
|
11
|
+
def annotate!(messages)
|
12
|
+
messages.first(MAX_ANNOTATIONS).each do |message|
|
13
|
+
output_stream.puts self.class.format(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Example annotation output from github's docs:
|
18
|
+
# ::warning file={name},line={line},title={title}::{message}
|
19
|
+
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
|
20
|
+
def self.format(message)
|
21
|
+
attributes = {
|
22
|
+
file: message.path,
|
23
|
+
line: message.annotated_line || message.start_line,
|
24
|
+
title: message.rule
|
25
|
+
}.compact
|
26
|
+
|
27
|
+
attributes_string = attributes.map { |k, v| "#{k}=#{v}" }.join(",")
|
28
|
+
"::warning #{attributes_string}::#{message.body}"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :output_stream
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
glob = File.expand_path("../annotators/*.rb", __FILE__)
|
2
|
+
Dir.glob(glob).sort.each { |f| require(f) }
|
3
|
+
|
4
|
+
module QuietQuality
|
5
|
+
module Annotators
|
6
|
+
Unrecognized = Class.new(Error)
|
7
|
+
|
8
|
+
ANNOTATOR_TYPES = {
|
9
|
+
github_stdout: Annotators::GithubStdout
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def self.annotate!(annotator:, messages:, limit: nil)
|
13
|
+
limited_messages = limit ? messages.first(limit) : messages
|
14
|
+
ANNOTATOR_TYPES.fetch(annotator.to_sym).new.annotate!(limited_messages)
|
15
|
+
rescue KeyError
|
16
|
+
fail Unrecognized, "Unrecognized annotator_type '#{annotator}'"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
class ChangedFile
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize(path:, lines:)
|
6
|
+
@path = path
|
7
|
+
|
8
|
+
if lines == :all || lines == "all"
|
9
|
+
@entire = true
|
10
|
+
@lines = nil
|
11
|
+
else
|
12
|
+
@entire = false
|
13
|
+
@lines = lines
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def entire?
|
18
|
+
@entire
|
19
|
+
end
|
20
|
+
|
21
|
+
def lines
|
22
|
+
return nil if @lines.nil?
|
23
|
+
@_lines ||= @lines.to_set
|
24
|
+
end
|
25
|
+
|
26
|
+
def line_numbers
|
27
|
+
return nil if @lines.nil?
|
28
|
+
@_line_numbers ||= @lines.sort
|
29
|
+
end
|
30
|
+
|
31
|
+
def merge(other)
|
32
|
+
if path != other.path
|
33
|
+
fail ArgumentError, "Cannot merge ChangedFiles '#{path}' and '#{other.path}', they're different files"
|
34
|
+
end
|
35
|
+
|
36
|
+
new_lines = (entire? || other.entire?) ? :all : (lines + other.lines).to_a
|
37
|
+
self.class.new(path: path, lines: new_lines)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
class ChangedFiles
|
3
|
+
attr_reader :files
|
4
|
+
|
5
|
+
def initialize(files)
|
6
|
+
@files = files
|
7
|
+
end
|
8
|
+
|
9
|
+
def paths
|
10
|
+
@_paths ||= files.map(&:path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def file(path)
|
14
|
+
files_by_path.fetch(path, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def include?(path)
|
18
|
+
files_by_path.include?(path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge(other)
|
22
|
+
merged_files = []
|
23
|
+
(files + other.files)
|
24
|
+
.group_by(&:path)
|
25
|
+
.each_pair { |_path, pfiles| merged_files << pfiles.reduce(&:merge) }
|
26
|
+
self.class.new(merged_files)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def files_by_path
|
32
|
+
@_files_by_path ||= files.map { |f| [f.path, f] }.to_h
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,103 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,49 @@
|
|
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
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "./annotators"
|
2
|
+
require_relative "./tools"
|
3
|
+
|
4
|
+
module QuietQuality
|
5
|
+
module Cli
|
6
|
+
Error = Class.new(QuietQuality::Error)
|
7
|
+
UsageError = Class.new(Error)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
glob = File.expand_path("../cli/*.rb", __FILE__)
|
12
|
+
Dir.glob(glob).sort.each { |f| require f }
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Executors
|
3
|
+
class BaseExecutor
|
4
|
+
def initialize(tools:, changed_files: nil)
|
5
|
+
@tools = tools
|
6
|
+
@changed_files = changed_files
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute!
|
10
|
+
fail NoMethodError, "execute! should be implemented by the subclass of BaseExecutor"
|
11
|
+
end
|
12
|
+
|
13
|
+
def outcomes
|
14
|
+
@_outcomes ||= pipelines.map(&:outcome)
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages
|
18
|
+
@_messages ||= Messages.new(pipelines.map(&:messages).map(&:all).reduce(&:+))
|
19
|
+
end
|
20
|
+
|
21
|
+
def any_failure?
|
22
|
+
pipelines.any?(&:failure?)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :tools, :changed_files
|
28
|
+
|
29
|
+
def pipelines
|
30
|
+
@_pipelines ||= tools.map do |topts|
|
31
|
+
Pipeline.new(tool_options: topts, changed_files: changed_files)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def pipeline_by_tool
|
36
|
+
@_pipeline_by_tool ||= pipelines
|
37
|
+
.map { |p| [p.tool_name, p] }
|
38
|
+
.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
def pipeline_for(tool)
|
42
|
+
pipeline_by_tool.fetch(tool.to_sym)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Executors
|
3
|
+
class ConcurrentExecutor < BaseExecutor
|
4
|
+
def execute!
|
5
|
+
threads = pipelines.map { |pipeline| threaded_pipeline(pipeline) }
|
6
|
+
threads.each(&:join)
|
7
|
+
pipelines.none?(&:failure?)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def threaded_pipeline(pipeline)
|
13
|
+
Thread.new do
|
14
|
+
pipeline.outcome
|
15
|
+
pipeline.messages
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|