quiet_quality 1.2.1 → 1.3.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.quiet_quality.ci.yml +1 -0
  3. data/.quiet_quality.yml +1 -0
  4. data/CHANGELOG.md +20 -0
  5. data/README.md +7 -1
  6. data/lib/quiet_quality/annotators/github_stdout.rb +3 -1
  7. data/lib/quiet_quality/cli/arg_parser.rb +22 -4
  8. data/lib/quiet_quality/cli/entrypoint.rb +13 -9
  9. data/lib/quiet_quality/cli/presenter.rb +26 -13
  10. data/lib/quiet_quality/colorize.rb +19 -0
  11. data/lib/quiet_quality/config/builder.rb +1 -0
  12. data/lib/quiet_quality/config/options.rb +39 -2
  13. data/lib/quiet_quality/config/parsed_options.rb +1 -0
  14. data/lib/quiet_quality/config/parser.rb +2 -1
  15. data/lib/quiet_quality/config/tool_options.rb +9 -0
  16. data/lib/quiet_quality/executors/pipeline.rb +26 -5
  17. data/lib/quiet_quality/logger.rb +54 -6
  18. data/lib/quiet_quality/logging.rb +21 -0
  19. data/lib/quiet_quality/message.rb +44 -9
  20. data/lib/quiet_quality/tools/base_runner.rb +9 -0
  21. data/lib/quiet_quality/tools/brakeman/parser.rb +8 -1
  22. data/lib/quiet_quality/tools/brakeman/runner.rb +1 -1
  23. data/lib/quiet_quality/tools/brakeman.rb +1 -0
  24. data/lib/quiet_quality/tools/haml_lint/parser.rb +2 -1
  25. data/lib/quiet_quality/tools/haml_lint/runner.rb +1 -1
  26. data/lib/quiet_quality/tools/haml_lint.rb +1 -0
  27. data/lib/quiet_quality/tools/markdown_lint/parser.rb +2 -1
  28. data/lib/quiet_quality/tools/markdown_lint/runner.rb +1 -1
  29. data/lib/quiet_quality/tools/markdown_lint.rb +1 -0
  30. data/lib/quiet_quality/tools/relevant_runner.rb +1 -0
  31. data/lib/quiet_quality/tools/rspec/parser.rb +26 -2
  32. data/lib/quiet_quality/tools/rspec/runner.rb +1 -1
  33. data/lib/quiet_quality/tools/rspec.rb +1 -0
  34. data/lib/quiet_quality/tools/rubocop/parser.rb +6 -1
  35. data/lib/quiet_quality/tools/rubocop/runner.rb +1 -1
  36. data/lib/quiet_quality/tools/rubocop.rb +1 -0
  37. data/lib/quiet_quality/tools/standardrb/parser.rb +3 -0
  38. data/lib/quiet_quality/tools/standardrb/runner.rb +1 -1
  39. data/lib/quiet_quality/tools/standardrb.rb +1 -0
  40. data/lib/quiet_quality/version.rb +1 -1
  41. data/lib/quiet_quality.rb +6 -0
  42. data/quiet_quality.gemspec +1 -1
  43. metadata +8 -7
  44. data/lib/quiet_quality/config/logging.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3847a8d0b996b19f34c32ac587dd1b9e51a2bedbea8e777e6b888294b16b5c0
4
- data.tar.gz: 742080e4b9193d0d99606f00c68077229ffba805c5b93ffa75c09705e0101b18
3
+ metadata.gz: 921a945aaedb335246c6944ae8f460505357a73d20f4f1cf2ae3dba3e9fe9041
4
+ data.tar.gz: fab1f3330661738cc146e26590d489bb9fff0cc0c0318a5e9529fb9fd0802038
5
5
  SHA512:
6
- metadata.gz: b61e91968e6bdd4943809a336d60cf6a7feee9ab58db225a33f8268bd02efc911c3fa812e45491be1cc9f6d461a6db71308e790f1d912fac011c82cf4864e8d7
7
- data.tar.gz: ace3273fb13dfcd04ae2432ed0cd488195789c75a27251617fc6be851bf865e4c4b689f9c3e3e83203ffb2b1ecd2be11c258405b10a3720c186b79b5e150fe3b
6
+ metadata.gz: 84a082f31fb8a6ae290399a97fdb5918bd91c2eaa701ec7b3909fe27e2d66845ceae7db7bcf9a59dd6696c71bfe99b4cc189f1d529bea49f5f82ba849c8fb553
7
+ data.tar.gz: 222a7432405df57d2739c0799bc5844b1a916c76febf34c798891ee39e17ce79c17dedaec15928488a819261fdc35d3d746276109455cba7d490b2140c5d31c4
@@ -4,3 +4,4 @@ executor: concurrent
4
4
  comparison_branch: origin/main
5
5
  all_files: true
6
6
  unfiltered: true
7
+ colorize: true
data/.quiet_quality.yml CHANGED
@@ -5,3 +5,4 @@ comparison_branch: main
5
5
  changed_files: false
6
6
  filter_messages: false
7
7
  logging: light
8
+ colorize: true
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## Release 1.3.0
4
+
5
+ * Support (and enable by default) colorizing the console stderr output from
6
+ `bin/qq` - disable with the `--no-colorize` flag or the `colorize: false`
7
+ configuration entry. (#94, resolved #36)
8
+ * Introduce a Logging facility, and add the `--verbose/-v` flag - supply it
9
+ either once or twice to enable (colorized) logging in either `info` or `debug`
10
+ level, providing much more detail about what's going on during execution.
11
+
12
+ ## Release 1.2.2
13
+
14
+ * Add some code to the Rspec::Parser that _cleans_ the json of certain text that
15
+ simplecov may write into the rspec json output. (#91, resolves #86)
16
+ * Include the name of the originating tool in the printed message, and the
17
+ annotation, when a warning is presented. (#90 resolves #72)
18
+ * Support `normal` as a logging level, and the `--normal` and `-n` cli
19
+ arguments. This is the default value, so this really only matters if your
20
+ config file sets another value and you want to override it from the cli.
21
+ (#91, resolves #86)
22
+
3
23
  ## Release 1.2.1
4
24
 
5
25
  * Fix the handling of the various ways to specify whether tools should limit
data/README.md CHANGED
@@ -148,6 +148,9 @@ The configuration file supports the following _global_ options (top-level keys):
148
148
  prints a aggregated result (e.g. "3 tools executed: 1 passed, 2 failed
149
149
  (rubocop, standardrb)"). The `quiet` option will only return a status code,
150
150
  printing nothing.
151
+ * `colorize`: by default, `bin/qq` will include color codes in its output, to
152
+ make failing tools easier to spot, and messages easier to read. But you can
153
+ supply `colorize: false` to tell it not to do that if you don't want them.
151
154
 
152
155
  And then each tool can have an entry, within which `changed_files` and
153
156
  `filter_messages` can be specified - the tool-specific settings override the
@@ -196,7 +199,10 @@ Usage: qq [TOOLS] [GLOBAL_OPTIONS] [TOOL_OPTIONS]
196
199
  -B, --comparison-branch BRANCH Specify the branch to compare against
197
200
  -f, --filter-messages [tool] Filter messages from tool(s) based on changed lines
198
201
  -u, --unfiltered [tool] Don't filter messages from tool(s)
202
+ --[no-]colorize Colorize the logging output
203
+ -n, --normal Print outcomes and messages
199
204
  -l, --light Print aggregated results only
200
205
  -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
206
+ -L, --logging LEVEL Specify logging mode (from light/quiet/normal)
207
+ -v, --verbose Log more verbosely - multiple times is more verbose
202
208
  ```
@@ -18,10 +18,12 @@ module QuietQuality
18
18
  # ::warning file={name},line={line},title={title}::{message}
19
19
  # See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
20
20
  def self.format(message)
21
+ title = message.tool_name.to_s
22
+ title += " #{message.rule}" if message.rule
21
23
  attributes = {
22
24
  file: message.path,
23
25
  line: message.annotated_line || message.start_line,
24
- title: message.rule
26
+ title: title
25
27
  }.compact
26
28
 
27
29
  attributes_string = attributes.map { |k, v| "#{k}=#{v}" }.join(",")
@@ -65,7 +65,9 @@ module QuietQuality
65
65
  setup_annotation_options(parser)
66
66
  setup_file_target_options(parser)
67
67
  setup_filter_messages_options(parser)
68
+ setup_colorization_options(parser)
68
69
  setup_logging_options(parser)
70
+ setup_verbosity_options(parser)
69
71
  end
70
72
  end
71
73
 
@@ -136,20 +138,36 @@ module QuietQuality
136
138
  end
137
139
  end
138
140
 
141
+ def setup_colorization_options(parser)
142
+ parser.on("--[no-]colorize", "Colorize the logging output") do |value|
143
+ set_global_option(:colorize, value)
144
+ end
145
+ end
146
+
139
147
  def setup_logging_options(parser)
148
+ parser.on("-n", "--normal", "Print outcomes and messages") do
149
+ set_global_option(:logging, :normal)
150
+ end
151
+
140
152
  parser.on("-l", "--light", "Print aggregated results only") do
141
- set_global_option(:logging, Config::Logging::LIGHT)
153
+ set_global_option(:logging, :light)
142
154
  end
143
155
 
144
156
  parser.on("-q", "--quiet", "Don't print results, only return a status code") do
145
- set_global_option(:logging, Config::Logging::QUIET)
157
+ set_global_option(:logging, :quiet)
146
158
  end
147
159
 
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)
160
+ parser.on("-L", "--logging LEVEL", "Specify logging mode (from normal/light/quiet)") do |level|
161
+ validate_value_from("logging level", level.to_sym, Config::Options::LOGGING_LEVELS)
150
162
  set_global_option(:logging, level.to_sym)
151
163
  end
152
164
  end
165
+
166
+ def setup_verbosity_options(parser)
167
+ parser.on("-v", "--verbose", "Log more verbosely - multiple times is more verbose") do
168
+ QuietQuality.logger.increase_level!
169
+ end
170
+ end
153
171
  end
154
172
  end
155
173
  end
@@ -1,6 +1,8 @@
1
1
  module QuietQuality
2
2
  module Cli
3
3
  class Entrypoint
4
+ include Logging
5
+
4
6
  def initialize(argv:, output_stream: $stdout, error_stream: $stderr)
5
7
  @argv = argv
6
8
  @output_stream = output_stream
@@ -15,6 +17,7 @@ module QuietQuality
15
17
  elsif no_tools?
16
18
  log_no_tools_text
17
19
  else
20
+ log_options
18
21
  executed
19
22
  log_results
20
23
  annotate_messages
@@ -33,19 +36,19 @@ module QuietQuality
33
36
 
34
37
  attr_reader :argv, :output_stream, :error_stream
35
38
 
36
- def logger
37
- @_logger ||= QuietQuality::Logger.new(stream: error_stream, logging: options.logging)
38
- end
39
-
40
39
  def presenter
41
40
  @_presenter ||= Presenter.new(
42
- logger: logger,
43
- logging: options.logging,
41
+ stream: error_stream,
42
+ options: options,
44
43
  outcomes: executor.outcomes,
45
44
  messages: executor.messages
46
45
  )
47
46
  end
48
47
 
48
+ def log_options
49
+ debug("Complete Options object:", data: options.to_h)
50
+ end
51
+
49
52
  def log_results
50
53
  presenter.log_results
51
54
  end
@@ -71,15 +74,15 @@ module QuietQuality
71
74
  end
72
75
 
73
76
  def log_help_text
74
- logger.puts(arg_parser.help_text)
77
+ error_stream.puts(arg_parser.help_text)
75
78
  end
76
79
 
77
80
  def log_version_text
78
- logger.puts(QuietQuality::VERSION)
81
+ error_stream.puts(QuietQuality::VERSION)
79
82
  end
80
83
 
81
84
  def log_no_tools_text
82
- logger.puts(<<~TEXT)
85
+ error_stream.puts(<<~TEXT)
83
86
  You must specify one or more tools to run, either on the command-line or in the
84
87
  default_tools key in a configuration file.
85
88
  TEXT
@@ -113,6 +116,7 @@ module QuietQuality
113
116
 
114
117
  def annotate_messages
115
118
  return unless options.annotator
119
+ info("Annotating with #{options.annotator}")
116
120
  annotator = options.annotator.new(output_stream: output_stream)
117
121
  annotator.annotate!(executed.messages)
118
122
  end
@@ -1,17 +1,17 @@
1
1
  module QuietQuality
2
2
  module Cli
3
3
  class Presenter
4
- def initialize(logger:, logging:, outcomes:, messages:)
5
- @logger = logger
6
- @logging = logging
4
+ def initialize(stream:, options:, outcomes:, messages:)
5
+ @stream = stream
6
+ @options = options
7
7
  @outcomes = outcomes
8
8
  @messages = messages
9
9
  end
10
10
 
11
11
  def log_results
12
- return if logging.quiet?
12
+ return if options.quiet?
13
13
 
14
- if logging.light?
14
+ if options.light?
15
15
  log_light_outcomes
16
16
  else
17
17
  log_outcomes
@@ -21,7 +21,7 @@ module QuietQuality
21
21
 
22
22
  private
23
23
 
24
- attr_reader :logger, :logging, :outcomes, :messages
24
+ attr_reader :stream, :options, :outcomes, :messages
25
25
 
26
26
  def failed_outcomes
27
27
  @_failed_outcomes ||= outcomes.select(&:failure?)
@@ -31,26 +31,38 @@ module QuietQuality
31
31
  @_successful_outcomes ||= outcomes.select(&:success?)
32
32
  end
33
33
 
34
+ def colorize(color_name, s)
35
+ return s unless options.colorize?
36
+ Colorize.colorize(s, color: color_name)
37
+ end
38
+
39
+ def failed_tools_text
40
+ colorize(:red, " (#{failed_outcomes.map(&:tool).join(", ")})")
41
+ end
42
+
34
43
  def log_light_outcomes
35
44
  line = "%d tools executed: %d passed, %d failed" % [
36
45
  outcomes.count,
37
46
  successful_outcomes.count,
38
47
  failed_outcomes.count
39
48
  ]
40
- line += " (#{failed_outcomes.map(&:tool).join(", ")})" if failed_outcomes.any?
41
- logger.puts line
49
+ line += failed_tools_text if failed_outcomes.any?
50
+ stream.puts line
42
51
  end
43
52
 
44
53
  def log_outcomes
45
54
  outcomes.each do |outcome|
46
- result = outcome.success? ? "Passed" : "Failed"
47
- logger.puts "--- #{result}: #{outcome.tool}"
55
+ if outcome.success?
56
+ stream.puts "--- " + colorize(:green, "Passed: #{outcome.tool}")
57
+ else
58
+ stream.puts "--- " + colorize(:red, "Failed: #{outcome.tool}")
59
+ end
48
60
  end
49
61
  end
50
62
 
51
63
  def log_messages
52
64
  return unless messages.any?
53
- logger.puts "\n\n#{messages.count} messages:"
65
+ stream.puts "\n\n#{messages.count} messages:"
54
66
  messages.each { |msg| log_message(msg) }
55
67
  end
56
68
 
@@ -67,10 +79,11 @@ module QuietQuality
67
79
  end
68
80
 
69
81
  def log_message(msg)
82
+ tool = colorize(:yellow, msg.tool_name)
70
83
  line_range = line_range_for(msg)
71
- rule_string = msg.rule ? " [#{msg.rule}]" : ""
84
+ rule_string = msg.rule ? " [#{colorize(:yellow, msg.rule)}]" : ""
72
85
  truncated_body = reduce_text(msg.body, 120)
73
- logger.puts " #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
86
+ stream.puts "#{tool} #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
74
87
  end
75
88
  end
76
89
  end
@@ -0,0 +1,19 @@
1
+ module QuietQuality
2
+ module Colorize
3
+ CODES = {
4
+ red: "\e[31m",
5
+ green: "\e[32m",
6
+ yellow: "\e[33m",
7
+ light_blue: "\e[94m",
8
+ light_cyan: "\e[96m"
9
+ }.freeze
10
+
11
+ RESET_CODE = "\e[0m"
12
+
13
+ def self.colorize(s, color:)
14
+ fail(ArgumentError, "Unrecognized color '#{color}'") unless CODES.include?(color.to_sym)
15
+ color_code = CODES.fetch(color.to_sym)
16
+ "#{color_code}#{s}#{RESET_CODE}"
17
+ end
18
+ end
19
+ end
@@ -106,6 +106,7 @@ module QuietQuality
106
106
 
107
107
  def update_logging
108
108
  set_unless_nil(options, :logging, apply.global_option(:logging))
109
+ set_unless_nil(options, :colorize, apply.global_option(:colorize))
109
110
  end
110
111
 
111
112
  # ---- update the tool options (apply global forms first) -------
@@ -1,19 +1,56 @@
1
1
  module QuietQuality
2
2
  module Config
3
3
  class Options
4
+ LOGGING_LEVELS = [:quiet, :light, :normal].freeze
5
+
4
6
  def initialize
5
7
  @annotator = nil
6
8
  @executor = Executors::ConcurrentExecutor
7
9
  @tools = nil
8
10
  @comparison_branch = nil
9
- @logging = Logging.new
11
+ @colorize = true
12
+ @logging = :normal
10
13
  end
11
14
 
12
15
  attr_accessor :tools, :comparison_branch, :annotator, :executor
13
16
  attr_reader :logging
17
+ attr_writer :colorize
14
18
 
15
19
  def logging=(level)
16
- @logging.level = level
20
+ fail(ArgumentError, "Unrecognized logging level '#{level}'") unless LOGGING_LEVELS.include?(level.to_sym)
21
+ @logging = level.to_sym
22
+ end
23
+
24
+ def colorize?
25
+ !!@colorize
26
+ end
27
+
28
+ def quiet?
29
+ logging == :quiet
30
+ end
31
+
32
+ def light?
33
+ logging == :light
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ annotator: annotator,
39
+ executor: executor.name,
40
+ comparison_branch: comparison_branch,
41
+ colorize: colorize?,
42
+ logging: logging,
43
+ tools: tool_hashes_by_name
44
+ }
45
+ end
46
+
47
+ private
48
+
49
+ def tool_hashes_by_name
50
+ return {} unless tools
51
+ tools
52
+ .map { |tool_option| [tool_option.tool_name, tool_option.to_h] }
53
+ .to_h
17
54
  end
18
55
  end
19
56
  end
@@ -9,6 +9,7 @@ module QuietQuality
9
9
  :annotator,
10
10
  :executor,
11
11
  :comparison_branch,
12
+ :colorize,
12
13
  :logging,
13
14
  :limit_targets,
14
15
  :filter_messages,
@@ -46,7 +46,8 @@ module QuietQuality
46
46
  read_global_option(opts, :all_files, :limit_targets, 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
+ read_global_option(opts, :colorize, :colorize, as: :boolean)
50
+ read_global_option(opts, :logging, :logging, as: :symbol, validate_from: Options::LOGGING_LEVELS)
50
51
  end
51
52
 
52
53
  def store_tool_options(opts)
@@ -35,6 +35,15 @@ module QuietQuality
35
35
  return nil if @file_filter.nil?
36
36
  Regexp.new(@file_filter)
37
37
  end
38
+
39
+ def to_h
40
+ {
41
+ tool_name: tool_name,
42
+ limit_targets: limit_targets?,
43
+ filter_messages: filter_messages?,
44
+ file_filter: file_filter&.to_s
45
+ }
46
+ end
38
47
  end
39
48
  end
40
49
  end
@@ -1,6 +1,8 @@
1
1
  module QuietQuality
2
2
  module Executors
3
3
  class Pipeline
4
+ include Logging
5
+
4
6
  def initialize(tool_options:, changed_files: nil)
5
7
  @tool_options = tool_options
6
8
  @changed_files = changed_files
@@ -25,10 +27,7 @@ module QuietQuality
25
27
 
26
28
  def messages
27
29
  return @_messages if defined?(@_messages)
28
- @_messages = parser.messages
29
- @_messages = relevance_filter.filter(@_messages) if filter_messages? && changed_files
30
- @_messages.each { |m| locator.update!(m) } if changed_files
31
- @_messages
30
+ @_messages = relocated(filtered(parser.messages))
32
31
  end
33
32
 
34
33
  private
@@ -51,7 +50,12 @@ module QuietQuality
51
50
  @_runner ||= tool_options.runner_class.new(
52
51
  changed_files: limit_targets? ? changed_files : nil,
53
52
  file_filter: tool_options.file_filter
54
- )
53
+ ).tap { |r| log_runner(r) }
54
+ end
55
+
56
+ def log_runner(r)
57
+ info("Runner #{r.tool_name} command: `#{r.command.join(" ")}`")
58
+ debug("Full command for #{r.tool_name}", data: r.command)
55
59
  end
56
60
 
57
61
  def parser
@@ -65,6 +69,23 @@ module QuietQuality
65
69
  def locator
66
70
  @_locator ||= AnnotationLocator.new(changed_files: changed_files)
67
71
  end
72
+
73
+ def filtered(messages_object)
74
+ return messages_object unless filter_messages? && changed_files
75
+
76
+ original_count = messages_object.count
77
+ relevance_filter.filter(messages_object).tap do |filtered|
78
+ info("Messages for #{tool_name} filtered from #{original_count} to #{filtered.count}")
79
+ end
80
+ end
81
+
82
+ def relocated(messages_object)
83
+ if changed_files && !messages_object.empty?
84
+ messages_object.each { |m| locator.update!(m) }
85
+ info("Messages for #{tool_name} positioned into the diff for annotation purposes")
86
+ end
87
+ messages_object
88
+ end
68
89
  end
69
90
  end
70
91
  end
@@ -1,17 +1,65 @@
1
1
  module QuietQuality
2
2
  class Logger
3
- def initialize(stream:, logging:)
3
+ LEVEL_UPS = {none: :warn, warn: :info, info: :debug}.freeze
4
+ LEVELS = {none: 0, warn: 1, info: 2, debug: 3}.freeze
5
+ COLORS = {warn: :yellow, info: :light_blue, debug: :light_cyan}.freeze
6
+
7
+ def initialize(level: :warn, stream: $stderr)
8
+ @level = level
4
9
  @stream = stream
5
- @logging = logging
6
10
  end
7
11
 
8
- def puts(s)
9
- return if logging.quiet?
10
- stream.puts(s)
12
+ attr_reader :level
13
+
14
+ def increase_level!
15
+ next_level = LEVEL_UPS.fetch(level, nil)
16
+ self.level = next_level if next_level
17
+ end
18
+
19
+ def show?(message_level)
20
+ LEVELS[message_level] <= LEVELS[level]
21
+ end
22
+
23
+ def level=(name)
24
+ fail(ArgumentError, "Unrecognized Logger level '#{name}'") unless LEVELS.include?(name.to_sym)
25
+ @level = name.to_sym
26
+ end
27
+
28
+ def warn(message, data: nil)
29
+ log_message(message, data, :warn)
30
+ end
31
+
32
+ def info(message, data: nil)
33
+ log_message(message, data, :info)
34
+ end
35
+
36
+ def debug(message, data: nil)
37
+ log_message(message, data, :debug)
11
38
  end
12
39
 
13
40
  private
14
41
 
15
- attr_reader :stream, :logging
42
+ attr_reader :stream
43
+
44
+ def log_message(message, data, message_level)
45
+ return unless show?(message_level)
46
+ stream.puts formatted_message(message, data, message_level)
47
+ stream.flush
48
+ end
49
+
50
+ def formatted_message(message, data, message_level)
51
+ prefix = message_level.to_s.upcase.rjust(5)
52
+ if data
53
+ data_text = JSON.pretty_generate(data)
54
+ message = message + "\n" + data_text
55
+ end
56
+ prefixed_message = message.split("\n").map { |line| "[#{prefix}] #{line}" }.join("\n")
57
+ colorize(prefixed_message, message_level)
58
+ end
59
+
60
+ def colorize(s, message_level)
61
+ color = COLORS.fetch(message_level)
62
+ Colorize.colorize(s, color: color)
63
+ end
16
64
  end
17
65
  end
@@ -0,0 +1,21 @@
1
+ module QuietQuality
2
+ module Logging
3
+ def warn(message, data: nil)
4
+ logger.warn(message, data: data)
5
+ end
6
+
7
+ def info(message, data: nil)
8
+ logger.info(message, data: data)
9
+ end
10
+
11
+ def debug(message, data: nil)
12
+ logger.debug(message, data: data)
13
+ end
14
+
15
+ private
16
+
17
+ def logger
18
+ QuietQuality.logger
19
+ end
20
+ end
21
+ end
@@ -1,7 +1,8 @@
1
1
  module QuietQuality
2
2
  class Message
3
- attr_accessor :annotated_line
4
- attr_reader :path, :body, :start_line, :stop_line, :level, :rule
3
+ REQUIRED_ATTRS = %w[path body start_line tool_name].freeze
4
+
5
+ attr_writer :annotated_line
5
6
 
6
7
  def self.load(hash)
7
8
  new(**hash)
@@ -9,17 +10,51 @@ module QuietQuality
9
10
 
10
11
  def initialize(**attrs)
11
12
  @attrs = attrs.map { |k, v| [k.to_s, v] }.to_h
12
- @path = @attrs.fetch("path")
13
- @body = @attrs.fetch("body")
14
- @start_line = @attrs.fetch("start_line")
15
- @stop_line = @attrs.fetch("stop_line", @start_line)
16
- @annotated_line = @attrs.fetch("annotated_line", nil)
17
- @level = @attrs.fetch("level", nil)
18
- @rule = @attrs.fetch("rule", nil)
13
+ validate_attrs!
19
14
  end
20
15
 
21
16
  def to_h
22
17
  @attrs.map { |k, v| [k.to_s, v] }.to_h
23
18
  end
19
+
20
+ def path
21
+ @_path ||= @attrs.fetch("path")
22
+ end
23
+
24
+ def body
25
+ @_body ||= @attrs.fetch("body")
26
+ end
27
+
28
+ def tool_name
29
+ @_tool_name ||= @attrs.fetch("tool_name")
30
+ end
31
+
32
+ def start_line
33
+ @_start_line ||= @attrs.fetch("start_line")
34
+ end
35
+
36
+ def stop_line
37
+ @_stop_line ||= @attrs.fetch("stop_line", start_line)
38
+ end
39
+
40
+ def annotated_line
41
+ @annotated_line ||= @attrs.fetch("annotated_line", nil)
42
+ end
43
+
44
+ def level
45
+ @_level ||= @attrs.fetch("level", nil)
46
+ end
47
+
48
+ def rule
49
+ @_rule ||= @attrs.fetch("rule", nil)
50
+ end
51
+
52
+ private
53
+
54
+ def validate_attrs!
55
+ REQUIRED_ATTRS.each do |attr|
56
+ raise ArgumentError, "Missing required attribute #{attr}" unless @attrs[attr]
57
+ end
58
+ end
24
59
  end
25
60
  end
@@ -1,6 +1,8 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  class BaseRunner
4
+ include Logging
5
+
4
6
  # In general, we don't want to supply a huge number of arguments to a command-line tool.
5
7
  MAX_FILES = 100
6
8
 
@@ -36,6 +38,8 @@ module QuietQuality
36
38
 
37
39
  def performed_outcome
38
40
  out, err, stat = Open3.capture3(*command)
41
+ log_performance(err, stat)
42
+
39
43
  if success_status?(stat)
40
44
  Outcome.new(tool: tool_name, output: out, logging: err)
41
45
  elsif failure_status?(stat)
@@ -44,6 +48,11 @@ module QuietQuality
44
48
  fail(ExecutionError, "Execution of #{tool_name} failed with #{stat.exitstatus}")
45
49
  end
46
50
  end
51
+
52
+ def log_performance(err, stat)
53
+ info("Runner #{tool_name} exited with #{stat.exitstatus}")
54
+ debug("Runner logs from #{tool_name}:", data: err&.split("\n"))
55
+ end
47
56
  end
48
57
  end
49
58
  end
@@ -37,7 +37,14 @@ module QuietQuality
37
37
  line = warning.fetch(:line)
38
38
  level = warning.fetch(:confidence, nil)
39
39
  rule = warning.fetch(:warning_type)
40
- Message.new(path: path, body: body, start_line: line, level: level, rule: rule)
40
+ Message.new(
41
+ path: path,
42
+ body: body,
43
+ start_line: line,
44
+ level: level,
45
+ rule: rule,
46
+ tool_name: TOOL_NAME
47
+ )
41
48
  end
42
49
  end
43
50
  end
@@ -3,7 +3,7 @@ module QuietQuality
3
3
  module Brakeman
4
4
  class Runner < BaseRunner
5
5
  def tool_name
6
- :brakeman
6
+ TOOL_NAME
7
7
  end
8
8
 
9
9
  def command
@@ -3,6 +3,7 @@ require_relative "./rubocop"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  module Brakeman
6
+ TOOL_NAME = :brakeman
6
7
  end
7
8
  end
8
9
  end
@@ -36,7 +36,8 @@ module QuietQuality
36
36
  body: offense.fetch(:message),
37
37
  start_line: offense.dig(:location, :line),
38
38
  level: offense.fetch(:severity, nil),
39
- rule: offense.fetch(:linter_name, nil)
39
+ rule: offense.fetch(:linter_name, nil),
40
+ tool_name: TOOL_NAME
40
41
  )
41
42
  end
42
43
  end
@@ -3,7 +3,7 @@ module QuietQuality
3
3
  module HamlLint
4
4
  class Runner < RelevantRunner
5
5
  def tool_name
6
- :haml_lint
6
+ TOOL_NAME
7
7
  end
8
8
 
9
9
  def no_files_output
@@ -1,6 +1,7 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module HamlLint
4
+ TOOL_NAME = :haml_lint
4
5
  end
5
6
  end
6
7
  end
@@ -25,7 +25,8 @@ module QuietQuality
25
25
  path: entry.fetch(:filename),
26
26
  start_line: entry.fetch(:line),
27
27
  rule: entry.fetch(:description),
28
- body: entry.fetch(:docs)
28
+ body: entry.fetch(:docs),
29
+ tool_name: TOOL_NAME
29
30
  )
30
31
  end
31
32
  end
@@ -3,7 +3,7 @@ module QuietQuality
3
3
  module MarkdownLint
4
4
  class Runner < RelevantRunner
5
5
  def tool_name
6
- :markdown_lint
6
+ TOOL_NAME
7
7
  end
8
8
 
9
9
  def no_files_output
@@ -1,6 +1,7 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module MarkdownLint
4
+ TOOL_NAME = :markdown_lint
4
5
  end
5
6
  end
6
7
  end
@@ -48,6 +48,7 @@ module QuietQuality
48
48
  end
49
49
 
50
50
  def skipped_outcome
51
+ info("Runner #{tool_name} was skipped")
51
52
  Outcome.new(tool: tool_name, output: no_files_output)
52
53
  end
53
54
  end
@@ -16,8 +16,26 @@ module QuietQuality
16
16
 
17
17
  attr_reader :text
18
18
 
19
+ # Many people use simplecov with rspec, and its default formatter
20
+ # writes text output into the stdout stream of rspec even when rspec is
21
+ # asked for json output. I have an issue open here, and I'll get a pair
22
+ # of PRs together if they indicate any willingness to accept such a
23
+ # change: https://github.com/simplecov-ruby/simplecov/issues/1060
24
+ #
25
+ # The only stdout writes are visible on these lines:
26
+ # https://github.com/simplecov-ruby/simplecov-html/blob/main/lib/simplecov-html.rb#L31
27
+ # https://github.com/simplecov-ruby/simplecov-html/blob/main/lib/simplecov-html.rb#L80
28
+ #
29
+ # There are _hundreds_ of rspec plugins, and any of them could write to
30
+ # stdout - we probably won't worry about any but the most common.
31
+ def cleaned_text
32
+ @_cleaned_text ||= text
33
+ .gsub(/Coverage report generated.*covered.$/, "")
34
+ .gsub(/Encoding problems with file.*$/, "")
35
+ end
36
+
19
37
  def content
20
- @_content ||= JSON.parse(text, symbolize_names: true)
38
+ @_content ||= JSON.parse(cleaned_text, symbolize_names: true)
21
39
  end
22
40
 
23
41
  def examples
@@ -37,7 +55,13 @@ module QuietQuality
37
55
  body = example.dig(:exception, :message) || example.fetch(:description)
38
56
  line = example.fetch(:line_number)
39
57
  rule = example.dig(:exception, :class) || "Failed Example"
40
- Message.new(path: path, body: body, start_line: line, rule: rule)
58
+ Message.new(
59
+ path: path,
60
+ body: body,
61
+ start_line: line,
62
+ rule: rule,
63
+ tool_name: TOOL_NAME
64
+ )
41
65
  end
42
66
  end
43
67
  end
@@ -3,7 +3,7 @@ module QuietQuality
3
3
  module Rspec
4
4
  class Runner < RelevantRunner
5
5
  def tool_name
6
- :rspec
6
+ TOOL_NAME
7
7
  end
8
8
 
9
9
  def no_files_output
@@ -1,6 +1,7 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
+ TOOL_NAME = :rspec
4
5
  end
5
6
  end
6
7
  end
@@ -37,9 +37,14 @@ module QuietQuality
37
37
  start_line: offense.dig(:location, :start_line),
38
38
  stop_line: offense.dig(:location, :last_line),
39
39
  level: offense.fetch(:severity, nil),
40
- rule: offense.fetch(:cop_name, nil)
40
+ rule: offense.fetch(:cop_name, nil),
41
+ tool_name: tool_name
41
42
  )
42
43
  end
44
+
45
+ def tool_name
46
+ TOOL_NAME
47
+ end
43
48
  end
44
49
  end
45
50
  end
@@ -3,7 +3,7 @@ module QuietQuality
3
3
  module Rubocop
4
4
  class Runner < RelevantRunner
5
5
  def tool_name
6
- :rubocop
6
+ TOOL_NAME
7
7
  end
8
8
 
9
9
  def no_files_output
@@ -1,6 +1,7 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rubocop
4
+ TOOL_NAME = :rubocop
4
5
  end
5
6
  end
6
7
  end
@@ -2,6 +2,9 @@ module QuietQuality
2
2
  module Tools
3
3
  module Standardrb
4
4
  class Parser < Rubocop::Parser
5
+ def tool_name
6
+ TOOL_NAME
7
+ end
5
8
  end
6
9
  end
7
10
  end
@@ -3,7 +3,7 @@ module QuietQuality
3
3
  module Standardrb
4
4
  class Runner < RelevantRunner
5
5
  def tool_name
6
- :standardrb
6
+ TOOL_NAME
7
7
  end
8
8
 
9
9
  def no_files_output
@@ -3,6 +3,7 @@ require_relative "./rubocop"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  module Standardrb
6
+ TOOL_NAME = :standardrb
6
7
  end
7
8
  end
8
9
  end
@@ -1,3 +1,3 @@
1
1
  module QuietQuality
2
- VERSION = "1.2.1"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/quiet_quality.rb CHANGED
@@ -10,7 +10,13 @@ require "set" # rubocop:disable Lint/RedundantRequireStatement
10
10
 
11
11
  module QuietQuality
12
12
  Error = Class.new(StandardError)
13
+
14
+ def self.logger
15
+ @_logger ||= QuietQuality::Logger.new
16
+ end
13
17
  end
14
18
 
19
+ require_relative "./quiet_quality/logger"
20
+ require_relative "./quiet_quality/logging"
15
21
  glob = File.expand_path("../quiet_quality/*.rb", __FILE__)
16
22
  Dir.glob(glob).sort.each { |f| require f }
@@ -40,6 +40,6 @@ Gem::Specification.new do |spec|
40
40
  spec.add_development_dependency "pry", "~> 0.14"
41
41
  spec.add_development_dependency "standard", "~> 1.28"
42
42
  spec.add_development_dependency "rubocop", "~> 1.50"
43
- spec.add_development_dependency "debug"
43
+ spec.add_development_dependency "debug", "~> 1.7"
44
44
  spec.add_development_dependency "mdl", "~> 0.12"
45
45
  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.2.1
4
+ version: 1.3.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-06-03 00:00:00.000000000 Z
11
+ date: 2023-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git
@@ -126,16 +126,16 @@ dependencies:
126
126
  name: debug
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '1.7'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ">="
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '1.7'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: mdl
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -186,10 +186,10 @@ files:
186
186
  - lib/quiet_quality/cli/arg_parser.rb
187
187
  - lib/quiet_quality/cli/entrypoint.rb
188
188
  - lib/quiet_quality/cli/presenter.rb
189
+ - lib/quiet_quality/colorize.rb
189
190
  - lib/quiet_quality/config.rb
190
191
  - lib/quiet_quality/config/builder.rb
191
192
  - lib/quiet_quality/config/finder.rb
192
- - lib/quiet_quality/config/logging.rb
193
193
  - lib/quiet_quality/config/options.rb
194
194
  - lib/quiet_quality/config/parsed_options.rb
195
195
  - lib/quiet_quality/config/parser.rb
@@ -200,6 +200,7 @@ files:
200
200
  - lib/quiet_quality/executors/pipeline.rb
201
201
  - lib/quiet_quality/executors/serial_executor.rb
202
202
  - lib/quiet_quality/logger.rb
203
+ - lib/quiet_quality/logging.rb
203
204
  - lib/quiet_quality/message.rb
204
205
  - lib/quiet_quality/message_filter.rb
205
206
  - lib/quiet_quality/messages.rb
@@ -1,23 +0,0 @@
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