quiet_quality 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dogfood.yml +1 -1
  3. data/.quiet_quality.ci.yml +6 -0
  4. data/.quiet_quality.yml +2 -1
  5. data/CHANGELOG.md +52 -0
  6. data/README.md +43 -13
  7. data/lib/quiet_quality/cli/arg_parser.rb +16 -0
  8. data/lib/quiet_quality/cli/entrypoint.rb +29 -30
  9. data/lib/quiet_quality/cli/presenter.rb +77 -0
  10. data/lib/quiet_quality/config/builder.rb +5 -0
  11. data/lib/quiet_quality/config/logging.rb +23 -0
  12. data/lib/quiet_quality/config/options.rb +6 -0
  13. data/lib/quiet_quality/config/parser.rb +1 -0
  14. data/lib/quiet_quality/executors/base_executor.rb +8 -0
  15. data/lib/quiet_quality/logger.rb +17 -0
  16. data/lib/quiet_quality/tools/base_runner.rb +49 -0
  17. data/lib/quiet_quality/tools/brakeman/runner.rb +7 -26
  18. data/lib/quiet_quality/tools/brakeman.rb +0 -2
  19. data/lib/quiet_quality/tools/haml_lint/runner.rb +15 -50
  20. data/lib/quiet_quality/tools/haml_lint.rb +0 -2
  21. data/lib/quiet_quality/tools/markdown_lint/parser.rb +34 -0
  22. data/lib/quiet_quality/tools/markdown_lint/runner.rb +28 -0
  23. data/lib/quiet_quality/tools/markdown_lint.rb +9 -0
  24. data/lib/quiet_quality/tools/relevant_runner.rb +55 -0
  25. data/lib/quiet_quality/tools/rspec/runner.rb +9 -46
  26. data/lib/quiet_quality/tools/rspec.rb +0 -2
  27. data/lib/quiet_quality/tools/rubocop/runner.rb +9 -56
  28. data/lib/quiet_quality/tools/rubocop.rb +0 -2
  29. data/lib/quiet_quality/tools/standardrb/runner.rb +15 -3
  30. data/lib/quiet_quality/tools/standardrb.rb +0 -2
  31. data/lib/quiet_quality/tools.rb +6 -0
  32. data/lib/quiet_quality/version.rb +1 -1
  33. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6aea488d2026da63fdf9483d1df9bd4e2363270d7b441cbca384cd803cab803
4
- data.tar.gz: 0536d6eb0b8cdd1c4678f49058be4d37017b7ff6156d363afa71df8a979d30aa
3
+ metadata.gz: ae3f031f4d627e624ce2ae28a5cf58d8e9e0663822371089d54bda51cb1aefeb
4
+ data.tar.gz: c9c26fd4d3df19e70162b38ef78622cc569ea492656d9264c4a41a964931934a
5
5
  SHA512:
6
- metadata.gz: 766f24f8211b83610c05e57b0f81d77bbf2d4bf8c206cc5e9af1d4e302adc9fee6425c506b344850a1421b7a30f18b6dfa6a23132aaa64dc3e6fbeb5e59c4c0b
7
- data.tar.gz: 7b72dcf22a6696b2ff88a5efb08fb0338d2d4f0b43f506f147a2d5cb0ed7186328157ff4b8ded7198fff3785f6636b32d8b1ecd1102cd462b341f28c57dfd429
6
+ metadata.gz: 6925f90d2f144b9bd59b4f0ca8d9a28e41cebf8570b30e3fa85cee6eefe2961aaaf246f733cd9b62de0d070afb7cca545c650ead15e45a6a39372d34c4240023
7
+ data.tar.gz: 3589c916dab556a467ab64c974ea91eda6a4f6f229ccda58aa51488b13b3a9a44217206fb3962697570cd176fb1bd282bee6231eccad5b50e1d948f0758eb8fb
@@ -27,4 +27,4 @@ jobs:
27
27
  run: bundle install --jobs 4 --retry 3
28
28
 
29
29
  - name: Run QuietQuality
30
- run: bundle exec bin/qq standardrb rubocop rspec --all-files --unfiltered --annotate github_stdout --comparison-branch origin/main
30
+ run: bundle exec bin/qq -C .quiet_quality.ci.yml
@@ -0,0 +1,6 @@
1
+ ---
2
+ default_tools: ["standardrb", "rubocop", "markdown_lint", "rspec"]
3
+ executor: concurrent
4
+ comparison_branch: origin/main
5
+ all_files: true
6
+ unfiltered: true
data/.quiet_quality.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
- default_tools: ["standardrb", "rubocop", "rspec"]
2
+ default_tools: ["standardrb", "rubocop", "markdown_lint", "rspec"]
3
3
  executor: concurrent
4
4
  comparison_branch: main
5
5
  changed_files: false
6
6
  filter_messages: false
7
+ logging: light
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## Release 1.2.0
4
+
5
+ * Support `--light`, `--quiet`, and `--logging LEVEL` arguments for less output
6
+ (#78, resolves #37)
7
+ * Support the [markdownlint](https://github.com/markdownlint/markdownlint) tool
8
+ (#79, resolves #58)
9
+ * Extract BaseRunner (#82) and RelevantRunner (#83) parent classes from the tool
10
+ runners, to allow new tools to be more easily implemented. (Resolves #81)
11
+ * Extract a Cli::Presenter from the Entrypoint, to simplify pending work on cli
12
+ presentation (#84, resolves #42)
13
+ * Update the docs a bit, and add a changelog (hi!)
14
+
15
+ ## Release 1.1.0
16
+
17
+ * Support a `file_filter` config entry per-tool (without a cli option), to limit
18
+ what file paths a runner might supply to its tool based on a regex
19
+ (#74, resolves #68)
20
+ * When what tools to execute is not specified (by cli or by config file), abort
21
+ `bin/qq` and explain, rather than assuming "all of them" (#79, resolves #58)
22
+ * Update the config parser to handle keys named to match the cli options
23
+ alongside the ones that were (mistakenly) named differently. This is a
24
+ backwards-compatible change; if we eventually deprecate and simplify some of
25
+ these option names, you'll have plenty of warning (#77, resolves #75)
26
+ * Support `--version/-V` flag (#73, resolves #69)
27
+
28
+ ## Release 1.0.3
29
+
30
+ * Fix the printed _output_ for the case where there were some warnings from a
31
+ tool, but all of them were filtered out (because they targetted lines that
32
+ were not changed, for example). This situation should tell you that nothing
33
+ is wrong with your PR, not that there is a problem (#71)
34
+
35
+ ## Release 1.0.2
36
+
37
+ * Fix the _exit status_ for the case where there were some warnings from a
38
+ tool, but all of them were filtered out (because they targetted lines that
39
+ were not changed, for example). This situation should produce a successful
40
+ result, and not fail a CI pipeline (#67)
41
+
42
+ ## Release 1.0.1
43
+
44
+ * Fix the calculation of `changed_files` for the executor - in the migration
45
+ to Entrypoint, the actual git call to get a ChangedFiles object to pass into
46
+ other service classes was lost, which had the result that the entire system
47
+ behaved (outside of tests) as if you were always running with `--all-files`
48
+ (#65).
49
+
50
+ ## Release 1.0.0
51
+
52
+ Initial functional public release.
data/README.md CHANGED
@@ -14,11 +14,13 @@ that too.
14
14
 
15
15
  So far, we have support for the following tools:
16
16
 
17
- * rubocop
18
- * standardrb
19
- * rspec
20
- * haml-lint
21
- * brakeman (though there's no way to run this against only changed files)
17
+ * [rubocop](https://github.com/rubocop/rubocop)
18
+ * [standardrb](https://github.com/standardrb/standard)
19
+ * [rspec](https://rspec.info/)
20
+ * [haml-lint](https://github.com/sds/haml-lint)
21
+ * [markdownlint](https://github.com/markdownlint/markdownlint)
22
+ * [brakeman](https://brakemanscanner.org/) (though there's no way to run this
23
+ against only changed files)
22
24
 
23
25
  Supporting more tools is relatively straightforward - they're implemented by
24
26
  wrapping cli invocations and parsing output files (which overall seem to be much
@@ -126,6 +128,9 @@ And then run `qq -C config/quiet_quality/linters_workflow.yml`
126
128
 
127
129
  The configuration file supports the following _global_ options (top-level keys):
128
130
 
131
+ * `default_tools`: Which tools should be run when you `qq` without specifying?
132
+ Valid values are: `rubocop`, `rspec`, `standardrb`, `haml_lint`, `brakeman`,
133
+ and `markdown_lint`.
129
134
  * `executor`: 'serial' or 'concurrent' (the latter is the default)
130
135
  * `annotator`: none set by default, and `github_stdout` is the only supported
131
136
  value so far.
@@ -139,6 +144,10 @@ The configuration file supports the following _global_ options (top-level keys):
139
144
  * `filter_messages`: defaults to false - should the resulting messages that do
140
145
  not refer to lines that were changed or added relative to the comparison
141
146
  branch be skipped? Also possible to set for each tool.
147
+ * `logging`: defaults to full messages printed. The `light` option
148
+ prints a aggregated result (e.g. "3 tools executed: 1 passed, 2 failed
149
+ (rubocop, standardrb)"). The `quiet` option will only return a status code,
150
+ printing nothing.
142
151
 
143
152
  And then each tool can have an entry, within which `changed_files` and
144
153
  `filter_messages` can be specified - the tool-specific settings override the
@@ -162,11 +171,32 @@ rspec:
162
171
 
163
172
  ### CLI Options
164
173
 
165
- The same options are all available on the CLI, plus some additional ones - run
166
- `qq --help` for a detailed list of the options, but the notable additions are:
167
-
168
- * `--help/-H`: See a list of the options
169
- * `--no-config/-N`: Do _not_ load a config file, even if present.
170
- * `--config/-C`: load the supplied config file (instead of the detected one, if
171
- found)
172
- * `--version/-V`: what version of the gem are you using?
174
+ To specify which _tools_ to run (and if any are specified, the `default_tools`
175
+ from the configuration file will be ignored), you supply them as positional
176
+ arguments: `qq rubocop rspec --all-files -L` will run the `rubocop` and `rspec`
177
+ tools, for example.
178
+
179
+ Run `qq --help` for a detailed list of the CLI options, they largely agree with
180
+ those in the configuration file, but there are some differences. There's no way
181
+ to specify a `file_filter` for a tool on the command-line, and there are some
182
+ additional options available focused on managing the interactions with
183
+ configuration files.
184
+
185
+ ```text
186
+ Usage: qq [TOOLS] [GLOBAL_OPTIONS] [TOOL_OPTIONS]
187
+ -h, --help Prints this help
188
+ -V, --version Print the current version of the gem
189
+ -C, --config PATH Load a config file from this path
190
+ -N, --no-config Do not load a config file, even if present
191
+ -E, --executor EXECUTOR Which executor to use
192
+ -A, --annotate ANNOTATOR Annotate with this annotator
193
+ -G, --annotate-github-stdout Annotate with GitHub Workflow commands
194
+ -a, --all-files [tool] Use the tool(s) on all files
195
+ -c, --changed-files [tool] Use the tool(s) only on changed files
196
+ -B, --comparison-branch BRANCH Specify the branch to compare against
197
+ -f, --filter-messages [tool] Filter messages from tool(s) based on changed lines
198
+ -u, --unfiltered [tool] Don't filter messages from tool(s)
199
+ -l, --light Print aggregated results only
200
+ -q, --quiet Don't print results, only return a status code
201
+ -L, --logging LEVEL Specify logging mode that results will be returned in. Valid options: light, quiet
202
+ ```
@@ -65,6 +65,7 @@ module QuietQuality
65
65
  setup_annotation_options(parser)
66
66
  setup_file_target_options(parser)
67
67
  setup_filter_messages_options(parser)
68
+ setup_logging_options(parser)
68
69
  end
69
70
  end
70
71
 
@@ -134,6 +135,21 @@ module QuietQuality
134
135
  read_tool_or_global_option(:filter_messages, tool, false)
135
136
  end
136
137
  end
138
+
139
+ def setup_logging_options(parser)
140
+ parser.on("-l", "--light", "Print aggregated results only") do
141
+ set_global_option(:logging, Config::Logging::LIGHT)
142
+ end
143
+
144
+ parser.on("-q", "--quiet", "Don't print results, only return a status code") do
145
+ set_global_option(:logging, Config::Logging::QUIET)
146
+ end
147
+
148
+ parser.on("-L", "--logging LEVEL", "Specify logging mode that results will be returned in. Valid options: light, quiet") do |level|
149
+ validate_value_from("logging level", level, Config::Logging::LEVELS)
150
+ set_global_option(:logging, level.to_sym)
151
+ end
152
+ end
137
153
  end
138
154
  end
139
155
  end
@@ -16,8 +16,7 @@ module QuietQuality
16
16
  log_no_tools_text
17
17
  else
18
18
  executed
19
- log_outcomes
20
- log_messages
19
+ log_results
21
20
  annotate_messages
22
21
  end
23
22
 
@@ -34,6 +33,23 @@ module QuietQuality
34
33
 
35
34
  attr_reader :argv, :output_stream, :error_stream
36
35
 
36
+ def logger
37
+ @_logger ||= QuietQuality::Logger.new(stream: error_stream, logging: options.logging)
38
+ end
39
+
40
+ def presenter
41
+ @_presenter ||= Presenter.new(
42
+ logger: logger,
43
+ logging: options.logging,
44
+ outcomes: executor.outcomes,
45
+ messages: executor.messages
46
+ )
47
+ end
48
+
49
+ def log_results
50
+ presenter.log_results
51
+ end
52
+
37
53
  def arg_parser
38
54
  @_arg_parser ||= ArgParser.new(argv.dup)
39
55
  end
@@ -46,6 +62,14 @@ module QuietQuality
46
62
  parsed_options.helping?
47
63
  end
48
64
 
65
+ def quiet_logging?
66
+ options.logging.quiet?
67
+ end
68
+
69
+ def light_logging?
70
+ options.logging.light?
71
+ end
72
+
49
73
  def printing_version?
50
74
  parsed_options.printing_version?
51
75
  end
@@ -55,15 +79,15 @@ module QuietQuality
55
79
  end
56
80
 
57
81
  def log_help_text
58
- error_stream.puts(arg_parser.help_text)
82
+ logger.puts(arg_parser.help_text)
59
83
  end
60
84
 
61
85
  def log_version_text
62
- error_stream.puts(QuietQuality::VERSION)
86
+ logger.puts(QuietQuality::VERSION)
63
87
  end
64
88
 
65
89
  def log_no_tools_text
66
- error_stream.puts(<<~TEXT)
90
+ logger.puts(<<~TEXT)
67
91
  You must specify one or more tools to run, either on the command-line or in the
68
92
  default_tools key in a configuration file.
69
93
  TEXT
@@ -95,31 +119,6 @@ module QuietQuality
95
119
  @_executed = executor
96
120
  end
97
121
 
98
- def log_outcomes
99
- executed.outcomes.each do |outcome|
100
- result = outcome.success? ? "Passed" : "Failed"
101
- error_stream.puts "--- #{result}: #{outcome.tool}"
102
- end
103
- end
104
-
105
- def log_message(msg)
106
- line_range =
107
- if msg.start_line == msg.stop_line
108
- msg.start_line.to_s
109
- else
110
- "#{msg.start_line}-#{msg.stop_line}"
111
- end
112
- rule_string = msg.rule ? " [#{msg.rule}]" : ""
113
- truncated_body = msg.body.gsub(/ *\n */, "\\n").slice(0, 120)
114
- error_stream.puts " #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
115
- end
116
-
117
- def log_messages
118
- return unless executed.messages.any?
119
- error_stream.puts "\n\n#{executed.messages.count} messages:"
120
- executed.messages.each { |msg| log_message(msg) }
121
- end
122
-
123
122
  def annotate_messages
124
123
  return unless options.annotator
125
124
  annotator = options.annotator.new(output_stream: output_stream)
@@ -0,0 +1,77 @@
1
+ module QuietQuality
2
+ module Cli
3
+ class Presenter
4
+ def initialize(logger:, logging:, outcomes:, messages:)
5
+ @logger = logger
6
+ @logging = logging
7
+ @outcomes = outcomes
8
+ @messages = messages
9
+ end
10
+
11
+ def log_results
12
+ return if logging.quiet?
13
+
14
+ if logging.light?
15
+ log_light_outcomes
16
+ else
17
+ log_outcomes
18
+ log_messages
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :logger, :logging, :outcomes, :messages
25
+
26
+ def failed_outcomes
27
+ @_failed_outcomes ||= outcomes.select(&:failure?)
28
+ end
29
+
30
+ def successful_outcomes
31
+ @_successful_outcomes ||= outcomes.select(&:success?)
32
+ end
33
+
34
+ def log_light_outcomes
35
+ line = "%d tools executed: %d passed, %d failed" % [
36
+ outcomes.count,
37
+ successful_outcomes.count,
38
+ failed_outcomes.count
39
+ ]
40
+ line += " (#{failed_outcomes.map(&:tool).join(", ")})" if failed_outcomes.any?
41
+ logger.puts line
42
+ end
43
+
44
+ def log_outcomes
45
+ outcomes.each do |outcome|
46
+ result = outcome.success? ? "Passed" : "Failed"
47
+ logger.puts "--- #{result}: #{outcome.tool}"
48
+ end
49
+ end
50
+
51
+ def log_messages
52
+ return unless messages.any?
53
+ logger.puts "\n\n#{messages.count} messages:"
54
+ messages.each { |msg| log_message(msg) }
55
+ end
56
+
57
+ def line_range_for(msg)
58
+ if msg.start_line == msg.stop_line
59
+ msg.start_line.to_s
60
+ else
61
+ "#{msg.start_line}-#{msg.stop_line}"
62
+ end
63
+ end
64
+
65
+ def reduce_text(s, length)
66
+ s.gsub(/ *\n */, "\\n").slice(0, length)
67
+ end
68
+
69
+ def log_message(msg)
70
+ line_range = line_range_for(msg)
71
+ rule_string = msg.rule ? " [#{msg.rule}]" : ""
72
+ truncated_body = reduce_text(msg.body, 120)
73
+ logger.puts " #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -85,6 +85,7 @@ module QuietQuality
85
85
  update_annotator
86
86
  update_executor
87
87
  update_comparison_branch
88
+ update_logging
88
89
  end
89
90
 
90
91
  def update_annotator
@@ -103,6 +104,10 @@ module QuietQuality
103
104
  set_unless_nil(options, :comparison_branch, apply.global_option(:comparison_branch))
104
105
  end
105
106
 
107
+ def update_logging
108
+ set_unless_nil(options, :logging, apply.global_option(:logging))
109
+ end
110
+
106
111
  # ---- update the tool options (apply global forms first) -------
107
112
 
108
113
  def update_tools
@@ -0,0 +1,23 @@
1
+ module QuietQuality
2
+ module Config
3
+ class Logging
4
+ LIGHT = :light
5
+ QUIET = :quiet
6
+ LEVELS = [LIGHT, QUIET].freeze
7
+
8
+ attr_accessor :level
9
+
10
+ def initialize(level: nil)
11
+ @level = level
12
+ end
13
+
14
+ def light?
15
+ @level == LIGHT
16
+ end
17
+
18
+ def quiet?
19
+ @level == QUIET
20
+ end
21
+ end
22
+ end
23
+ end
@@ -6,9 +6,15 @@ module QuietQuality
6
6
  @executor = Executors::ConcurrentExecutor
7
7
  @tools = nil
8
8
  @comparison_branch = nil
9
+ @logging = Logging.new
9
10
  end
10
11
 
11
12
  attr_accessor :tools, :comparison_branch, :annotator, :executor
13
+ attr_reader :logging
14
+
15
+ def logging=(level)
16
+ @logging.level = level
17
+ end
12
18
  end
13
19
  end
14
20
  end
@@ -46,6 +46,7 @@ module QuietQuality
46
46
  read_global_option(opts, :all_files, :changed_files, as: :reversed_boolean)
47
47
  read_global_option(opts, :filter_messages, :filter_messages, as: :boolean)
48
48
  read_global_option(opts, :unfiltered, :filter_messages, as: :reversed_boolean)
49
+ read_global_option(opts, :logging, :logging, as: :symbol, validate_from: Logging::LEVELS)
49
50
  end
50
51
 
51
52
  def store_tool_options(opts)
@@ -22,6 +22,14 @@ module QuietQuality
22
22
  pipelines.any?(&:failure?)
23
23
  end
24
24
 
25
+ def successful_outcomes
26
+ @_successful_outcomes ||= outcomes.select(&:success?)
27
+ end
28
+
29
+ def failed_outcomes
30
+ @_failed_outcomes ||= outcomes.select(&:failure?)
31
+ end
32
+
25
33
  private
26
34
 
27
35
  attr_reader :tools, :changed_files
@@ -0,0 +1,17 @@
1
+ module QuietQuality
2
+ class Logger
3
+ def initialize(stream:, logging:)
4
+ @stream = stream
5
+ @logging = logging
6
+ end
7
+
8
+ def puts(s)
9
+ return if logging.quiet?
10
+ stream.puts(s)
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :stream, :logging
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ module QuietQuality
2
+ module Tools
3
+ class BaseRunner
4
+ # In general, we don't want to supply a huge number of arguments to a command-line tool.
5
+ MAX_FILES = 100
6
+
7
+ def initialize(changed_files: nil, file_filter: nil)
8
+ @changed_files = changed_files
9
+ @file_filter = file_filter
10
+ end
11
+
12
+ def invoke!
13
+ @_outcome ||= performed_outcome
14
+ end
15
+
16
+ def tool_name
17
+ fail(NoMethodError, "BaseRunner subclass must implement `tool_name`")
18
+ end
19
+
20
+ def command
21
+ fail(NoMethodError, "BaseRunner subclass must implement `command`")
22
+ end
23
+
24
+ def success_status?(stat)
25
+ stat.success?
26
+ end
27
+
28
+ # distinct from _error_ status - this is asking "does this status represent failures-found?"
29
+ def failure_status?(stat)
30
+ stat.exitstatus == 1
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :changed_files, :file_filter
36
+
37
+ def performed_outcome
38
+ out, err, stat = Open3.capture3(*command)
39
+ if success_status?(stat)
40
+ Outcome.new(tool: tool_name, output: out, logging: err)
41
+ elsif failure_status?(stat)
42
+ Outcome.new(tool: tool_name, output: out, logging: err, failure: true)
43
+ else
44
+ fail(ExecutionError, "Execution of #{tool_name} failed with #{stat.exitstatus}")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,38 +1,19 @@
1
1
  module QuietQuality
2
2
  module Tools
3
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
- # brakeman does not support being run against a portion of the project, so neither
10
- # changed_files nor file_filter is actually used. But they are accepted here because
11
- # that is what Runner initializers are required to accept.
12
- def initialize(changed_files: nil, file_filter: nil)
13
- @changed_files = changed_files
14
- @file_filter = file_filter
15
- end
16
-
17
- def invoke!
18
- @_outcome ||= performed_outcome
4
+ class Runner < BaseRunner
5
+ def tool_name
6
+ :brakeman
19
7
  end
20
8
 
21
- private
22
-
23
9
  def command
24
10
  ["brakeman", "-f", "json"]
25
11
  end
26
12
 
27
- def performed_outcome
28
- out, err, stat = Open3.capture3(*command)
29
- if stat.success?
30
- Outcome.new(tool: :brakeman, output: out, logging: err)
31
- elsif KNOWN_EXIT_STATUSES.include?(stat.exitstatus)
32
- Outcome.new(tool: :brakeman, output: out, logging: err, failure: true)
33
- else
34
- fail(ExecutionError, "Execution of brakeman failed with #{stat.exitstatus}")
35
- end
13
+ # These are specified in constants at the top of brakeman.rb:
14
+ # https://github.com/presidentbeef/brakeman/blob/main/lib/brakeman.rb#L6-L25
15
+ def failure_status?(stat)
16
+ [3, 4, 5, 6, 7, 8].include?(stat.exitstatus)
36
17
  end
37
18
  end
38
19
  end
@@ -3,8 +3,6 @@ require_relative "./rubocop"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  module Brakeman
6
- ExecutionError = Class.new(Tools::Error)
7
- ParsingError = Class.new(Tools::Error)
8
6
  end
9
7
  end
10
8
  end
@@ -1,64 +1,29 @@
1
1
  module QuietQuality
2
2
  module Tools
3
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, file_filter: nil)
15
- @changed_files = changed_files
16
- @file_filter = file_filter
17
- end
18
-
19
- def invoke!
20
- @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
21
- end
22
-
23
- private
24
-
25
- attr_reader :changed_files, :file_filter
26
-
27
- def skip_execution?
28
- changed_files && relevant_files.empty?
29
- end
30
-
31
- def relevant_files
32
- return nil if changed_files.nil?
33
- changed_files.paths
34
- .select { |path| path.end_with?(".haml") }
35
- .select { |path| file_filter.nil? || file_filter.match?(path) }
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :haml_lint
36
7
  end
37
8
 
38
- def target_files
39
- return [] if changed_files.nil?
40
- return [] if relevant_files.length > MAX_FILES
41
- relevant_files
9
+ def no_files_output
10
+ %({"files": []})
42
11
  end
43
12
 
44
- def command
45
- return nil if skip_execution?
46
- ["haml-lint", "--reporter", "json"] + target_files.sort
13
+ def base_command
14
+ ["haml-lint", "--reporter", "json"]
47
15
  end
48
16
 
49
- def skipped_outcome
50
- Outcome.new(tool: :haml_lint, output: NO_FILES_OUTPUT)
17
+ def relevant_path?(path)
18
+ path.end_with?(".haml")
51
19
  end
52
20
 
53
- def performed_outcome
54
- out, err, stat = Open3.capture3(*command)
55
- if stat.success?
56
- Outcome.new(tool: :haml_lint, output: out, logging: err)
57
- elsif stat.exitstatus == FAILURE_STATUS
58
- Outcome.new(tool: :haml_lint, output: out, logging: err, failure: true)
59
- else
60
- fail(ExecutionError, "Execution of haml-lint failed with #{stat.exitstatus}")
61
- end
21
+ # haml-lint uses the `sysexits` gem, and exits with Sysexits::EX_DATAERR for the
22
+ # failures case here in lib/haml_lint/cli.rb. That's mapped to status 65 - other
23
+ # statuses have other failure meanings, which we don't want to interpret as "problems
24
+ # encountered"
25
+ def failure_status?(stat)
26
+ stat.exitstatus == 65
62
27
  end
63
28
  end
64
29
  end
@@ -1,8 +1,6 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module HamlLint
4
- ExecutionError = Class.new(Tools::Error)
5
- ParsingError = Class.new(Tools::Error)
6
4
  end
7
5
  end
8
6
  end
@@ -0,0 +1,34 @@
1
+ module QuietQuality
2
+ module Tools
3
+ module MarkdownLint
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.map { |entry| message_for_entry(entry) }
12
+ @_messages = Messages.new(messages)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :text
18
+
19
+ def content
20
+ @_content ||= JSON.parse(text, symbolize_names: true)
21
+ end
22
+
23
+ def message_for_entry(entry)
24
+ Message.new(
25
+ path: entry.fetch(:filename),
26
+ start_line: entry.fetch(:line),
27
+ rule: entry.fetch(:description),
28
+ body: entry.fetch(:docs)
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module QuietQuality
2
+ module Tools
3
+ module MarkdownLint
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :markdown_lint
7
+ end
8
+
9
+ def no_files_output
10
+ "[]"
11
+ end
12
+
13
+ def command
14
+ return nil if skip_execution?
15
+ if target_files.any?
16
+ ["mdl", "--json"] + target_files.sort
17
+ else
18
+ ["mdl", "--json", "."]
19
+ end
20
+ end
21
+
22
+ def relevant_path?(path)
23
+ path.end_with?(".md")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module QuietQuality
2
+ module Tools
3
+ module MarkdownLint
4
+ end
5
+ end
6
+ end
7
+
8
+ glob = File.expand_path("../markdown_lint/*.rb", __FILE__)
9
+ Dir.glob(glob).sort.each { |f| require f }
@@ -0,0 +1,55 @@
1
+ require_relative "./base_runner"
2
+
3
+ module QuietQuality
4
+ module Tools
5
+ class RelevantRunner < BaseRunner
6
+ # In general, we don't want to supply a huge number of arguments to a command-line tool.
7
+ # This will probably become configurable later.
8
+ MAX_FILES = 100
9
+
10
+ def invoke!
11
+ @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
12
+ end
13
+
14
+ def command
15
+ return nil if skip_execution?
16
+ base_command + target_files.sort
17
+ end
18
+
19
+ def relevant_path?(path)
20
+ fail(NoMethodError, "RelevantRunner subclass must implement `relevant_path?`")
21
+ end
22
+
23
+ def base_command
24
+ fail(NoMethodError, "RelevantRunner subclass must implement either `command` or `base_command`")
25
+ end
26
+
27
+ def no_files_output
28
+ fail(NoMethodError, "RelevantRunner subclass must implement `no_files_output`")
29
+ end
30
+
31
+ private
32
+
33
+ def skip_execution?
34
+ changed_files && relevant_files.empty?
35
+ end
36
+
37
+ def relevant_files
38
+ return nil if changed_files.nil?
39
+ changed_files.paths
40
+ .select { |path| relevant_path?(path) }
41
+ .select { |path| file_filter.nil? || file_filter.match?(path) }
42
+ end
43
+
44
+ def target_files
45
+ return [] if changed_files.nil?
46
+ return [] if relevant_files.length > MAX_FILES
47
+ relevant_files
48
+ end
49
+
50
+ def skipped_outcome
51
+ Outcome.new(tool: tool_name, output: no_files_output)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,58 +1,21 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
- class Runner
5
- MAX_FILES = 100
6
- NO_FILES_OUTPUT = '{"examples": [], "summary": {"failure_count": 0}}'
7
-
8
- def initialize(changed_files: nil, file_filter: nil)
9
- @changed_files = changed_files
10
- @file_filter = file_filter
11
- end
12
-
13
- def invoke!
14
- @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
15
- end
16
-
17
- private
18
-
19
- attr_reader :changed_files, :file_filter
20
-
21
- def skip_execution?
22
- changed_files && relevant_files.empty?
23
- end
24
-
25
- def relevant_files
26
- return nil if changed_files.nil?
27
- changed_files.paths
28
- .select { |path| path.end_with?("_spec.rb") }
29
- .select { |path| file_filter.nil? || file_filter.match?(path) }
30
- end
31
-
32
- def target_files
33
- return [] if changed_files.nil?
34
- return [] if relevant_files.length > MAX_FILES
35
- relevant_files
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :rspec
36
7
  end
37
8
 
38
- def command
39
- return nil if skip_execution?
40
- ["rspec", "-f", "json"] + target_files.sort
9
+ def no_files_output
10
+ '{"examples": [], "summary": {"failure_count": 0}}'
41
11
  end
42
12
 
43
- def skipped_outcome
44
- Outcome.new(tool: :rspec, output: NO_FILES_OUTPUT)
13
+ def base_command
14
+ ["rspec", "-f", "json"]
45
15
  end
46
16
 
47
- def performed_outcome
48
- out, err, stat = Open3.capture3(*command)
49
- if stat.success?
50
- Outcome.new(tool: :rspec, output: out, logging: err)
51
- elsif stat.exitstatus == 1
52
- Outcome.new(tool: :rspec, output: out, logging: err, failure: true)
53
- else
54
- fail(ExecutionError, "Execution of rspec failed with #{stat.exitstatus}")
55
- end
17
+ def relevant_path?(path)
18
+ path.end_with?("_spec.rb")
56
19
  end
57
20
  end
58
21
  end
@@ -1,8 +1,6 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
- ExecutionError = Class.new(Tools::Error)
5
- ParsingError = Class.new(Tools::Error)
6
4
  end
7
5
  end
8
6
  end
@@ -1,68 +1,21 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rubocop
4
- class Runner
5
- MAX_FILES = 100
6
- NO_FILES_OUTPUT = '{"files": [], "summary": {"offense_count": 0}}'
7
-
8
- def command_name
9
- "rubocop"
10
- end
11
-
12
- # Supplying changed_files: nil means "run against all files".
13
- def initialize(changed_files: nil, file_filter: nil)
14
- @changed_files = changed_files
15
- @file_filter = file_filter
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, :file_filter
25
-
26
- # If we were told that _no files changed_ (which is distinct from not being told that
27
- # any files changed - a [] instead of a nil), then we shouldn't run rubocop at all.
28
- def skip_execution?
29
- changed_files && relevant_files.empty?
30
- end
31
-
32
- # Note: if target_files goes over MAX_FILES, it's _empty_ instead - that means that
33
- # we run against the full repository instead of the specific files (rubocop's behavior
34
- # when no target files are specified)
35
- def command
36
- return nil if skip_execution?
37
- [command_name, "-f", "json"] + target_files.sort
38
- end
39
-
40
- def relevant_files
41
- return nil if changed_files.nil?
42
- changed_files.paths
43
- .select { |path| path.end_with?(".rb") }
44
- .select { |path| file_filter.nil? || file_filter.match?(path) }
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :rubocop
45
7
  end
46
8
 
47
- def target_files
48
- return [] if changed_files.nil?
49
- return [] if relevant_files.length > MAX_FILES
50
- relevant_files
9
+ def no_files_output
10
+ '{"files": [], "summary": {"offense_count": 0}}'
51
11
  end
52
12
 
53
- def skipped_outcome
54
- Outcome.new(tool: command_name.to_sym, output: NO_FILES_OUTPUT)
13
+ def base_command
14
+ ["rubocop", "-f", "json"]
55
15
  end
56
16
 
57
- def performed_outcome
58
- out, err, stat = Open3.capture3(*command)
59
- if stat.success?
60
- Outcome.new(tool: command_name.to_sym, output: out, logging: err)
61
- elsif stat.exitstatus == 1
62
- Outcome.new(tool: command_name.to_sym, output: out, logging: err, failure: true)
63
- else
64
- fail(ExecutionError, "Execution of #{command_name} failed with #{stat.exitstatus}")
65
- end
17
+ def relevant_path?(path)
18
+ path.end_with?(".rb")
66
19
  end
67
20
  end
68
21
  end
@@ -1,8 +1,6 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rubocop
4
- ExecutionError = Class.new(Tools::Error)
5
- ParsingError = Class.new(Tools::Error)
6
4
  end
7
5
  end
8
6
  end
@@ -1,9 +1,21 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Standardrb
4
- class Runner < Rubocop::Runner
5
- def command_name
6
- "standardrb"
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :standardrb
7
+ end
8
+
9
+ def no_files_output
10
+ '{"files": [], "summary": {"offense_count": 0}}'
11
+ end
12
+
13
+ def base_command
14
+ ["standardrb", "-f", "json"]
15
+ end
16
+
17
+ def relevant_path?(path)
18
+ path.end_with?(".rb")
7
19
  end
8
20
  end
9
21
  end
@@ -3,8 +3,6 @@ require_relative "./rubocop"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  module Standardrb
6
- ExecutionError = Class.new(Tools::Error)
7
- ParsingError = Class.new(Tools::Error)
8
6
  end
9
7
  end
10
8
  end
@@ -3,9 +3,14 @@ require "open3"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  Error = Class.new(::QuietQuality::Error)
6
+ ExecutionError = Class.new(Error)
7
+ ParsingError = Class.new(Error)
6
8
  end
7
9
  end
8
10
 
11
+ require_relative "./tools/base_runner"
12
+ require_relative "./tools/relevant_runner"
13
+
9
14
  glob = File.expand_path("../tools/*.rb", __FILE__)
10
15
  Dir.glob(glob).sort.each { |f| require f }
11
16
 
@@ -15,6 +20,7 @@ module QuietQuality
15
20
  AVAILABLE = {
16
21
  brakeman: Brakeman,
17
22
  haml_lint: HamlLint,
23
+ markdown_lint: MarkdownLint,
18
24
  rspec: Rspec,
19
25
  rubocop: Rubocop,
20
26
  standardrb: Standardrb
@@ -1,3 +1,3 @@
1
1
  module QuietQuality
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
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: 1.1.0
4
+ version: 1.2.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-27 00:00:00.000000000 Z
11
+ date: 2023-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git
@@ -166,9 +166,11 @@ files:
166
166
  - ".gitignore"
167
167
  - ".mdl_rules.rb"
168
168
  - ".mdlrc"
169
+ - ".quiet_quality.ci.yml"
169
170
  - ".quiet_quality.yml"
170
171
  - ".rspec"
171
172
  - ".rubocop.yml"
173
+ - CHANGELOG.md
172
174
  - Gemfile
173
175
  - LICENSE
174
176
  - README.md
@@ -183,9 +185,11 @@ files:
183
185
  - lib/quiet_quality/cli.rb
184
186
  - lib/quiet_quality/cli/arg_parser.rb
185
187
  - lib/quiet_quality/cli/entrypoint.rb
188
+ - lib/quiet_quality/cli/presenter.rb
186
189
  - lib/quiet_quality/config.rb
187
190
  - lib/quiet_quality/config/builder.rb
188
191
  - lib/quiet_quality/config/finder.rb
192
+ - lib/quiet_quality/config/logging.rb
189
193
  - lib/quiet_quality/config/options.rb
190
194
  - lib/quiet_quality/config/parsed_options.rb
191
195
  - lib/quiet_quality/config/parser.rb
@@ -195,17 +199,23 @@ files:
195
199
  - lib/quiet_quality/executors/concurrent_executor.rb
196
200
  - lib/quiet_quality/executors/pipeline.rb
197
201
  - lib/quiet_quality/executors/serial_executor.rb
202
+ - lib/quiet_quality/logger.rb
198
203
  - lib/quiet_quality/message.rb
199
204
  - lib/quiet_quality/message_filter.rb
200
205
  - lib/quiet_quality/messages.rb
201
206
  - lib/quiet_quality/tools.rb
207
+ - lib/quiet_quality/tools/base_runner.rb
202
208
  - lib/quiet_quality/tools/brakeman.rb
203
209
  - lib/quiet_quality/tools/brakeman/parser.rb
204
210
  - lib/quiet_quality/tools/brakeman/runner.rb
205
211
  - lib/quiet_quality/tools/haml_lint.rb
206
212
  - lib/quiet_quality/tools/haml_lint/parser.rb
207
213
  - lib/quiet_quality/tools/haml_lint/runner.rb
214
+ - lib/quiet_quality/tools/markdown_lint.rb
215
+ - lib/quiet_quality/tools/markdown_lint/parser.rb
216
+ - lib/quiet_quality/tools/markdown_lint/runner.rb
208
217
  - lib/quiet_quality/tools/outcome.rb
218
+ - lib/quiet_quality/tools/relevant_runner.rb
209
219
  - lib/quiet_quality/tools/rspec.rb
210
220
  - lib/quiet_quality/tools/rspec/parser.rb
211
221
  - lib/quiet_quality/tools/rspec/runner.rb
@@ -241,7 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
251
  - !ruby/object:Gem::Version
242
252
  version: '0'
243
253
  requirements: []
244
- rubygems_version: 3.3.7
254
+ rubygems_version: 3.1.6
245
255
  signing_key:
246
256
  specification_version: 4
247
257
  summary: A system for comparing quality tool outputs against the forward diffs