quiet_quality 1.4.0 → 1.5.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dogfood.yml +1 -1
  3. data/.github/workflows/linters.yml +1 -1
  4. data/.github/workflows/rspec.yml +1 -1
  5. data/.quiet_quality.yml +1 -0
  6. data/.standard.yml +3 -0
  7. data/CHANGELOG.md +20 -0
  8. data/README.md +37 -0
  9. data/lib/quiet_quality/cli/arg_parser.rb +12 -0
  10. data/lib/quiet_quality/cli/entrypoint.rb +21 -1
  11. data/lib/quiet_quality/cli/message_formatter.rb +190 -0
  12. data/lib/quiet_quality/cli/presenter.rb +18 -2
  13. data/lib/quiet_quality/cli.rb +2 -2
  14. data/lib/quiet_quality/config/builder.rb +24 -3
  15. data/lib/quiet_quality/config/file_filter.rb +6 -0
  16. data/lib/quiet_quality/config/options.rb +5 -1
  17. data/lib/quiet_quality/config/parsed_options.rb +4 -1
  18. data/lib/quiet_quality/config/parser.rb +10 -10
  19. data/lib/quiet_quality/executors/execcer.rb +46 -0
  20. data/lib/quiet_quality/executors/serial_executor.rb +1 -1
  21. data/lib/quiet_quality/logger.rb +4 -2
  22. data/lib/quiet_quality/tools/base_runner.rb +4 -0
  23. data/lib/quiet_quality/tools/brakeman/parser.rb +4 -0
  24. data/lib/quiet_quality/tools/brakeman/runner.rb +4 -0
  25. data/lib/quiet_quality/tools/brakeman.rb +1 -1
  26. data/lib/quiet_quality/tools/haml_lint/runner.rb +4 -0
  27. data/lib/quiet_quality/tools/markdown_lint/runner.rb +9 -3
  28. data/lib/quiet_quality/tools/relevant_runner.rb +10 -1
  29. data/lib/quiet_quality/tools/rspec/parser.rb +24 -1
  30. data/lib/quiet_quality/tools/rspec/runner.rb +4 -0
  31. data/lib/quiet_quality/tools/rspec.rb +2 -0
  32. data/lib/quiet_quality/tools/rubocop/runner.rb +4 -0
  33. data/lib/quiet_quality/tools/standardrb/runner.rb +4 -0
  34. data/lib/quiet_quality/tools/standardrb.rb +1 -1
  35. data/lib/quiet_quality/tools.rb +2 -2
  36. data/lib/quiet_quality/version.rb +1 -1
  37. data/lib/quiet_quality.rb +2 -2
  38. data/quiet_quality.gemspec +3 -3
  39. metadata +15 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81e7296beb4ced635f0e6e0d1b5521adb6481b3ca4d027e86e76ad1ee8682643
4
- data.tar.gz: 27dfab991d21f98536860a0fdbeed31faa1ea5b65a2af0941e0cc286fe6dcbc1
3
+ metadata.gz: 3084fe3f419db403944af870a9ccc39cccfe046f9f01841f52872808cd38b186
4
+ data.tar.gz: bc3b18bd54ec79e6adf72c9947142050cd0a14a329d665f58290fe40b3382b27
5
5
  SHA512:
6
- metadata.gz: b84bb256a178326bc76143952098db02723fcfa792ed6e86f9b6b63a0c602d87eb270d81d93e34742c2026abd7fd2118964b3a039eb8e3968863a8d1e5fe1697
7
- data.tar.gz: 76fc65f2ce473f91a8cb98bc32c62e98ecc154aef519ab8d9e908f7def8330306b76ee2c54ed99a57bfba4b5997065b6d953747756b9a0b83e9e557c96de1d23
6
+ metadata.gz: 761966c658f578970e2d420d194d8d81a71e2d8808f95c8862366656a88e2ee3bf10e1ec8907ac21f4b4e62d799cd923ea481c7bbbd856da8a9b5c8656c3cf1e
7
+ data.tar.gz: 2a91a00a403943d7a6051dc788dae5d2a963740cfd6d543228d8dbc810a5efd2ac5bdc0d8fc9c5f6c799e97c0c81742fd17543a43b184b86e4be117365146a0c
@@ -6,7 +6,7 @@ jobs:
6
6
  QuietQuality:
7
7
  runs-on: ubuntu-latest
8
8
  steps:
9
- - uses: actions/checkout@v3
9
+ - uses: actions/checkout@v4
10
10
  with:
11
11
  fetch-depth: 0
12
12
 
@@ -6,7 +6,7 @@ jobs:
6
6
  StandardRB:
7
7
  runs-on: ubuntu-latest
8
8
  steps:
9
- - uses: actions/checkout@v3
9
+ - uses: actions/checkout@v4
10
10
 
11
11
  - name: Set up ruby
12
12
  uses: ruby/setup-ruby@v1
@@ -11,7 +11,7 @@ jobs:
11
11
  ruby-version: ['2.7', '3.0', '3.1', '3.2', 'head']
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v3
14
+ - uses: actions/checkout@v4
15
15
 
16
16
  - name: Set up ruby
17
17
  uses: ruby/setup-ruby@v1
data/.quiet_quality.yml CHANGED
@@ -6,3 +6,4 @@ changed_files: false
6
6
  filter_messages: false
7
7
  logging: light
8
8
  colorize: true
9
+ message_format: "%lcyan10tool| [%myellow40rule] %bred60loc %e-90body"
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ ---
2
+ format: progress
3
+ ruby_version: 2.7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## Release 1.5.1
4
+
5
+ * Refactor ConfigParser to just parse the config into ParsedOptions, separating
6
+ the file-filter/excludes handling properly (#114)
7
+ * Update the standard/rubocop constraints (dev-only)
8
+ * Fail the pipeline when rspec encounters non-spec failures (#120 resolves #119)
9
+ * Expose error messages when brakeman encounters errors (#122 resolves #115)
10
+
11
+ ## Release 1.5.0
12
+
13
+ * Update to comply with current standardrb rules, and use checkout@v4
14
+ * Add a `-X/--exec` argument that allows you to let qq craft the command, but
15
+ then actually exec the command instead of running it and handling its output.
16
+ Especially useful for things like `rspec`, where the output it gives you about
17
+ failing tests is very useful, and qq is mostly only helpful for determining
18
+ what specs to run.
19
+ * Add a `--message-format` argument and `message_format` config file option,
20
+ which allow for a fairly complex configuration of the output format for
21
+ messages, so they can be displayed in various colorized/tabular formats.
22
+
3
23
  ## Release 1.4.0
4
24
 
5
25
  * Support specifying `excludes` per-tool, so that certain files won't be passed
data/README.md CHANGED
@@ -151,6 +151,9 @@ The configuration file supports the following _global_ options (top-level keys):
151
151
  * `colorize`: by default, `bin/qq` will include color codes in its output, to
152
152
  make failing tools easier to spot, and messages easier to read. But you can
153
153
  supply `colorize: false` to tell it not to do that if you don't want them.
154
+ * `message_format`: you can specify a format string with which to render the
155
+ messages, which interpolates values with various formatting flags. Details
156
+ given in the "Message Formatting" section below.
154
157
 
155
158
  And then each tool can have an entry, within which `changed_files` and
156
159
  `filter_messages` can be specified - the tool-specific settings override the
@@ -181,6 +184,40 @@ generated file like `db/schema.rb`, and that file doesn't meet your rubocop (or
181
184
  standardrb) rules, you'll get _told_ unless you exclude it at the quiet-quality
182
185
  level as well.
183
186
 
187
+ ### Message Formatting
188
+
189
+ You can supply a message-format string on the cli or in your config file, which
190
+ will override the default formatting for message output on the CLI. These format
191
+ strings are intended to be a single line containing "substitution tokens", which
192
+ each look like `%[lr]?[bem]?color?(Size)(Source)`.
193
+
194
+ * The first (optional) flag can be an "l", and "r", or be left off (which is the
195
+ same as "l"). This flag indicates the 'justification' - left or right.
196
+ * The second (optional) flag can be a "b", an "e", or an "m", defaulting to "e";
197
+ these stand for "beginning", "ending", and "middle", and represent what part
198
+ of the string should be truncated if it needs to be shortened.
199
+ * The third (optional) part is a color name, and can be any of "yellow", "red",
200
+ "green", "blue", "cyan", or "none" (leaving it off is the same as specifing
201
+ "none"). This is the color to use for the token in the output - note that any
202
+ color supplied here is used regardless of the '--colorize' flag.
203
+ * The fourth part of the token is required, and is the _size_ of the token. If a
204
+ positive integer is supplied, then the token will take up that much space, and
205
+ will be padded on the appropriate side if necessary; if a negative integer is
206
+ supplied, then the token will not be padded out, but will still get truncated
207
+ if it is too long. The value '0' is special, and indicates that the token
208
+ should be neither padded nor truncated.
209
+ * The last part of the token is a string indicating the _source_ data to
210
+ represent, and must be one of these values: "tool", "loc", "level", "path",
211
+ "lines", "rule", "body". Each of these represents one piece of data out of the
212
+ message object that can be rendered into the message line.
213
+
214
+ Some example message formats:
215
+
216
+ ```text
217
+ %lcyan8tool | %lmyellow30rule | %0loc
218
+ %le6tool [%mblue20rule] %b45loc %cyan-100body
219
+ ```
220
+
184
221
  ### CLI Options
185
222
 
186
223
  To specify which _tools_ to run (and if any are specified, the `default_tools`
@@ -67,6 +67,7 @@ module QuietQuality
67
67
  setup_filter_messages_options(parser)
68
68
  setup_colorization_options(parser)
69
69
  setup_logging_options(parser)
70
+ setup_message_formatting_options(parser)
70
71
  setup_verbosity_options(parser)
71
72
  end
72
73
  end
@@ -100,6 +101,11 @@ module QuietQuality
100
101
  validate_value_from("executor", name, Executors::AVAILABLE)
101
102
  set_global_option(:executor, name.to_sym)
102
103
  end
104
+
105
+ parser.on("-X", "--exec TOOL", "Exec one tool instead of managing several") do |tool_name|
106
+ validate_value_from("tool", tool_name, Tools::AVAILABLE)
107
+ set_global_option(:exec_tool, tool_name.to_sym)
108
+ end
103
109
  end
104
110
 
105
111
  def setup_annotation_options(parser)
@@ -163,6 +169,12 @@ module QuietQuality
163
169
  end
164
170
  end
165
171
 
172
+ def setup_message_formatting_options(parser)
173
+ parser.on("-F", "--message-format FMT", "A format string with which to print messages") do |fmt|
174
+ set_global_option(:message_format, fmt)
175
+ end
176
+ end
177
+
166
178
  def setup_verbosity_options(parser)
167
179
  parser.on("-v", "--verbose", "Log more verbosely - multiple times is more verbose") do
168
180
  QuietQuality.logger.increase_level!
@@ -18,7 +18,7 @@ module QuietQuality
18
18
  log_no_tools_text
19
19
  else
20
20
  log_options
21
- executed
21
+ execute!
22
22
  log_results
23
23
  annotate_messages
24
24
  end
@@ -114,6 +114,26 @@ module QuietQuality
114
114
  @_executed = executor
115
115
  end
116
116
 
117
+ def exec_tool_options
118
+ @_exec_tool_options ||= options.tools
119
+ .detect { |topts| topts.tool_name == options.exec_tool.to_sym }
120
+ end
121
+
122
+ def execcer
123
+ @_execcer ||= QuietQuality::Executors::Execcer.new(
124
+ tool_options: exec_tool_options,
125
+ changed_files: changed_files
126
+ )
127
+ end
128
+
129
+ def execute!
130
+ if options.exec_tool
131
+ execcer.exec!
132
+ else
133
+ executed
134
+ end
135
+ end
136
+
117
137
  def annotate_messages
118
138
  return unless options.annotator
119
139
  info("Annotating with #{options.annotator}")
@@ -0,0 +1,190 @@
1
+ module QuietQuality
2
+ module Cli
3
+ class MessageFormatter
4
+ TOKEN_MATCHING_REGEX = %r{%[a-z]*-?\d+(?:tool|loc|level|path|lines|rule|body)}
5
+
6
+ def initialize(message_format:)
7
+ @message_format = message_format
8
+ end
9
+
10
+ def format(message)
11
+ formatted_tokens = parsed_tokens.map { |pt| FormattedToken.new(parsed_token: pt, message: message) }
12
+ formatted_tokens.reduce(message_format) do |interpolating, ftok|
13
+ interpolating.gsub(ftok.token, ftok.formatted_token)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :message_format
20
+
21
+ def tokens
22
+ @_tokens ||= message_format.scan(TOKEN_MATCHING_REGEX)
23
+ end
24
+
25
+ def parsed_tokens
26
+ @_parsed_tokens ||= tokens.map { |tok| ParsedToken.new(tok) }
27
+ end
28
+
29
+ class ParsedToken
30
+ TOKEN_PARSING_REGEX = %r{
31
+ % # start the interplation token
32
+ (?<just>[lr])? # specify the justification
33
+ (?<trunc>[bem])? # where to truncate from
34
+ (?<color>yellow|red|green|blue|cyan|none)? # what color
35
+ (?<size>-?\d+) # string size (may be negative)
36
+ (?<source>tool|loc|level|path|lines|rule|body) # data source name
37
+ }x
38
+
39
+ COLORS = {
40
+ "yellow" => :yellow,
41
+ "red" => :red,
42
+ "green" => :green,
43
+ "blue" => :light_blue,
44
+ "cyan" => :light_cyan,
45
+ "none" => nil
46
+ }.freeze
47
+
48
+ JUSTIFICATIONS = {"l" => :left, "r" => :right}.freeze
49
+ TRUNCATIONS = {"b" => :beginning, "m" => :middle, "e" => :ending}.freeze
50
+
51
+ def initialize(token)
52
+ @token = token
53
+ end
54
+
55
+ attr_reader :token
56
+
57
+ def justification
58
+ JUSTIFICATIONS.fetch(token_pieces[:just]&.downcase, :left)
59
+ end
60
+
61
+ def truncation
62
+ TRUNCATIONS.fetch(token_pieces[:trunc]&.downcase, :ending)
63
+ end
64
+
65
+ def color
66
+ COLORS.fetch(token_pieces[:color]&.downcase, nil)
67
+ end
68
+
69
+ def size
70
+ raw_size.abs
71
+ end
72
+
73
+ def source
74
+ token_pieces[:source]
75
+ end
76
+
77
+ def allow_pad?
78
+ raw_size.positive?
79
+ end
80
+
81
+ def allow_truncate?
82
+ !raw_size.zero?
83
+ end
84
+
85
+ private
86
+
87
+ def token_pieces
88
+ @_token_pieces ||= token.match(TOKEN_PARSING_REGEX)
89
+ end
90
+
91
+ def raw_size
92
+ @_raw_size ||= token_pieces[:size].to_i
93
+ end
94
+ end
95
+ private_constant :ParsedToken
96
+
97
+ class FormattedToken
98
+ def initialize(parsed_token:, message:)
99
+ @parsed_token = parsed_token
100
+ @message = message
101
+ end
102
+
103
+ def formatted_token
104
+ colorized(padded(truncated(base_string)))
105
+ end
106
+
107
+ def token
108
+ parsed_token.token
109
+ end
110
+
111
+ private
112
+
113
+ attr_reader :parsed_token, :message
114
+
115
+ def line_range
116
+ if message.start_line == message.stop_line
117
+ message.start_line.to_s
118
+ else
119
+ "#{message.start_line}-#{message.stop_line}"
120
+ end
121
+ end
122
+
123
+ def base_string
124
+ case parsed_token.source
125
+ when "tool" then message.tool_name
126
+ when "loc" then location_string
127
+ when "level" then message.level
128
+ when "path" then message.path
129
+ when "lines" then line_range
130
+ when "rule" then message.rule
131
+ when "body" then flattened_body
132
+ end
133
+ end
134
+
135
+ def location_string
136
+ "#{message.path}:#{line_range}"
137
+ end
138
+
139
+ def flattened_body
140
+ message.body.gsub(/ *\n */, "\\n")
141
+ end
142
+
143
+ def truncated(s)
144
+ return s unless parsed_token.allow_truncate?
145
+ return s if s.length <= parsed_token.size
146
+ size = parsed_token.size
147
+
148
+ case parsed_token.truncation
149
+ when :beginning then truncate_beginning(s, size)
150
+ when :middle then truncate_middle(s, size)
151
+ when :ending then truncate_ending(s, size)
152
+ end
153
+ end
154
+
155
+ def truncate_beginning(s, size)
156
+ "…" + s.slice(1 - size, size - 1)
157
+ end
158
+
159
+ def truncate_middle(s, size)
160
+ front_len = (size / 2.0).floor
161
+ back_len = (size / 2.0).ceil - 1
162
+ s.slice(0, front_len) + "…" + s.slice(-back_len, back_len)
163
+ end
164
+
165
+ def truncate_ending(s, size)
166
+ s.slice(0, size - 1) + "…"
167
+ end
168
+
169
+ def padded(s)
170
+ return s unless parsed_token.allow_pad?
171
+ return s if s.length >= parsed_token.size
172
+
173
+ case parsed_token.justification
174
+ when :left then s.ljust(parsed_token.size)
175
+ when :right then s.rjust(parsed_token.size)
176
+ end
177
+ end
178
+
179
+ def colorized(s)
180
+ if parsed_token.color.nil?
181
+ s
182
+ else
183
+ Colorize.colorize(s, color: parsed_token.color)
184
+ end
185
+ end
186
+ end
187
+ private_constant :FormattedToken
188
+ end
189
+ end
190
+ end
@@ -78,12 +78,28 @@ module QuietQuality
78
78
  s.gsub(/ *\n */, "\\n").slice(0, length)
79
79
  end
80
80
 
81
- def log_message(msg)
81
+ def locally_formatted_message(msg)
82
82
  tool = colorize(:yellow, msg.tool_name)
83
83
  line_range = line_range_for(msg)
84
84
  rule_string = msg.rule ? " [#{colorize(:yellow, msg.rule)}]" : ""
85
85
  truncated_body = reduce_text(msg.body, 120)
86
- stream.puts "#{tool} #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
86
+ "#{tool} #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
87
+ end
88
+
89
+ def loggable_message(msg)
90
+ if options.message_format
91
+ message_formatter.format(msg)
92
+ else
93
+ stream.puts locally_formatted_message(msg)
94
+ end
95
+ end
96
+
97
+ def log_message(msg)
98
+ stream.puts loggable_message(msg)
99
+ end
100
+
101
+ def message_formatter
102
+ @_message_formatter ||= MessageFormatter.new(message_format: options.message_format)
87
103
  end
88
104
  end
89
105
  end
@@ -1,5 +1,5 @@
1
- require_relative "./annotators"
2
- require_relative "./tools"
1
+ require_relative "annotators"
2
+ require_relative "tools"
3
3
 
4
4
  module QuietQuality
5
5
  module Cli
@@ -22,7 +22,7 @@ module QuietQuality
22
22
  Options.new.tap { |opts| opts.tools = tools }
23
23
  end
24
24
 
25
- def tool_names
25
+ def specified_tool_names
26
26
  if cli.tools.any?
27
27
  cli.tools
28
28
  elsif config_file&.tools&.any?
@@ -32,6 +32,14 @@ module QuietQuality
32
32
  end
33
33
  end
34
34
 
35
+ def exec_tool_name
36
+ cli.global_option(:exec_tool)
37
+ end
38
+
39
+ def tool_names
40
+ (specified_tool_names + [exec_tool_name]).compact.uniq
41
+ end
42
+
35
43
  def config_finder
36
44
  @_config_finder ||= Finder.new(from: ".")
37
45
  end
@@ -76,7 +84,7 @@ module QuietQuality
76
84
 
77
85
  def set_unless_nil(object, method, value)
78
86
  return if value.nil?
79
- object.send("#{method}=", value)
87
+ object.send(:"#{method}=", value)
80
88
  end
81
89
 
82
90
  # ---- update the global options -------------
@@ -84,6 +92,7 @@ module QuietQuality
84
92
  def update_globals
85
93
  update_annotator
86
94
  update_executor
95
+ update_exec_tool
87
96
  update_comparison_branch
88
97
  update_logging
89
98
  end
@@ -100,6 +109,10 @@ module QuietQuality
100
109
  options.executor = Executors::AVAILABLE.fetch(executor_name)
101
110
  end
102
111
 
112
+ def update_exec_tool
113
+ set_unless_nil(options, :exec_tool, apply.global_option(:exec_tool))
114
+ end
115
+
103
116
  def update_comparison_branch
104
117
  set_unless_nil(options, :comparison_branch, apply.global_option(:comparison_branch))
105
118
  end
@@ -107,6 +120,7 @@ module QuietQuality
107
120
  def update_logging
108
121
  set_unless_nil(options, :logging, apply.global_option(:logging))
109
122
  set_unless_nil(options, :colorize, apply.global_option(:colorize))
123
+ set_unless_nil(options, :message_format, apply.global_option(:message_format))
110
124
  end
111
125
 
112
126
  # ---- update the tool options (apply global forms first) -------
@@ -115,7 +129,7 @@ module QuietQuality
115
129
  options.tools.each do |tool_options|
116
130
  update_tool_option(tool_options, :limit_targets)
117
131
  update_tool_option(tool_options, :filter_messages)
118
- update_tool_option(tool_options, :file_filter)
132
+ set_unless_nil(tool_options, :file_filter, build_file_filter(tool_options.tool_name))
119
133
  end
120
134
  end
121
135
 
@@ -124,6 +138,13 @@ module QuietQuality
124
138
  set_unless_nil(tool_options, option_name, apply.global_option(option_name))
125
139
  set_unless_nil(tool_options, option_name, apply.tool_option(tool_name, option_name))
126
140
  end
141
+
142
+ def build_file_filter(tool_name)
143
+ filter_string = apply.tool_option(tool_name, :file_filter)
144
+ excludes_strings = apply.tool_option(tool_name, :excludes)
145
+ return nil if filter_string.nil? && (excludes_strings.nil? || excludes_strings.empty?)
146
+ Config::FileFilter.new(regex: filter_string, excludes: excludes_strings)
147
+ end
127
148
  end
128
149
  end
129
150
  end
@@ -8,6 +8,8 @@ module QuietQuality
8
8
  @excludes_strings = excludes
9
9
  end
10
10
 
11
+ attr_reader :regex_string, :excludes_strings
12
+
11
13
  def regex
12
14
  return nil if @regex_string.nil?
13
15
  @_regex ||= Regexp.new(@regex_string)
@@ -33,6 +35,10 @@ module QuietQuality
33
35
  regex_match?(s) && !excludes_match?(s)
34
36
  end
35
37
 
38
+ def ==(other)
39
+ regex_string == other.regex_string && excludes_strings.sort == other.excludes_strings.sort
40
+ end
41
+
36
42
  private
37
43
 
38
44
  # The regex is an allow-match - if it's not supplied, treat everything as matching.
@@ -7,12 +7,14 @@ module QuietQuality
7
7
  @annotator = nil
8
8
  @executor = Executors::ConcurrentExecutor
9
9
  @tools = nil
10
+ @exec_tool = nil
10
11
  @comparison_branch = nil
11
12
  @colorize = true
12
13
  @logging = :normal
14
+ @message_format = nil
13
15
  end
14
16
 
15
- attr_accessor :tools, :comparison_branch, :annotator, :executor
17
+ attr_accessor :tools, :comparison_branch, :annotator, :executor, :exec_tool, :message_format
16
18
  attr_reader :logging
17
19
  attr_writer :colorize
18
20
 
@@ -37,9 +39,11 @@ module QuietQuality
37
39
  {
38
40
  annotator: annotator,
39
41
  executor: executor.name,
42
+ exec_tool: exec_tool,
40
43
  comparison_branch: comparison_branch,
41
44
  colorize: colorize?,
42
45
  logging: logging,
46
+ message_format: message_format,
43
47
  tools: tool_hashes_by_name
44
48
  }
45
49
  end
@@ -8,9 +8,11 @@ module QuietQuality
8
8
  :config_path,
9
9
  :annotator,
10
10
  :executor,
11
+ :exec_tool,
11
12
  :comparison_branch,
12
13
  :colorize,
13
14
  :logging,
15
+ :message_format,
14
16
  :limit_targets,
15
17
  :filter_messages,
16
18
  :file_filter
@@ -19,7 +21,8 @@ module QuietQuality
19
21
  TOOL_OPTIONS = [
20
22
  :limit_targets,
21
23
  :filter_messages,
22
- :file_filter
24
+ :file_filter,
25
+ :excludes
23
26
  ].to_set
24
27
 
25
28
  def initialize
@@ -48,6 +48,7 @@ module QuietQuality
48
48
  read_global_option(opts, :unfiltered, :filter_messages, as: :reversed_boolean)
49
49
  read_global_option(opts, :colorize, :colorize, as: :boolean)
50
50
  read_global_option(opts, :logging, :logging, as: :symbol, validate_from: Options::LOGGING_LEVELS)
51
+ read_global_option(opts, :message_format, :message_format, as: :string)
51
52
  end
52
53
 
53
54
  def store_tool_options(opts)
@@ -64,7 +65,8 @@ module QuietQuality
64
65
  read_tool_option(opts, tool_name, :unfiltered, :filter_messages, as: :reversed_boolean)
65
66
  read_tool_option(opts, tool_name, :changed_files, :limit_targets, as: :boolean)
66
67
  read_tool_option(opts, tool_name, :all_files, :limit_targets, as: :reversed_boolean)
67
- read_file_filter(opts, tool_name)
68
+ read_tool_option(opts, tool_name, :file_filter, :file_filter, as: :string)
69
+ read_tool_option(opts, tool_name, :excludes, :excludes, as: :strings)
68
70
  end
69
71
 
70
72
  def invalid!(message)
@@ -97,21 +99,13 @@ module QuietQuality
97
99
  opts.set_tool_option(tool, into, coerced_value)
98
100
  end
99
101
 
100
- def read_file_filter(opts, tool)
101
- parsed_regex = data.dig(tool.to_sym, :file_filter)
102
- parsed_excludes = data.dig(tool.to_sym, :excludes)
103
- if parsed_regex || parsed_excludes
104
- filter = Config::FileFilter.new(regex: parsed_regex, excludes: parsed_excludes)
105
- opts.set_tool_option(tool, :file_filter, filter)
106
- end
107
- end
108
-
109
102
  def validate_value(name, value, as:, from: nil)
110
103
  case as
111
104
  when :boolean then validate_boolean(name, value)
112
105
  when :reversed_boolean then validate_boolean(name, value)
113
106
  when :symbol then validate_symbol(name, value, from: from)
114
107
  when :string then validate_string(name, value)
108
+ when :strings then validate_strings(name, value)
115
109
  end
116
110
  end
117
111
 
@@ -137,11 +131,17 @@ module QuietQuality
137
131
  invalid!("option #{name} must not be empty") if value.empty?
138
132
  end
139
133
 
134
+ def validate_strings(name, value)
135
+ invalid!("option #{name} must be an array") unless value.is_a?(Array)
136
+ value.each_with_index { |item, n| validate_string("#{name}[#{n}]", item) }
137
+ end
138
+
140
139
  def coerce_value(value, as:)
141
140
  case as
142
141
  when :boolean then !!value
143
142
  when :reversed_boolean then !value
144
143
  when :string then value.to_s
144
+ when :strings then value.map(&:to_s)
145
145
  when :symbol then value.to_sym
146
146
  end
147
147
  end
@@ -0,0 +1,46 @@
1
+ module QuietQuality
2
+ module Executors
3
+ class Execcer
4
+ include Logging
5
+
6
+ def initialize(tool_options:, changed_files: nil)
7
+ @tool_options = tool_options
8
+ @changed_files = changed_files
9
+ end
10
+
11
+ def exec!
12
+ if runner.exec_command
13
+ Kernel.exec(*runner.exec_command)
14
+ else
15
+ info <<~LOG_MESSAGE
16
+ This runner does not believe it needs to execute at all.
17
+ This typically means that it was told to target changed-files, but no relevant
18
+ files were changed.
19
+ LOG_MESSAGE
20
+ Kernel.exit(0)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :tool_options, :changed_files
27
+
28
+ def limit_targets?
29
+ tool_options.limit_targets?
30
+ end
31
+
32
+ def runner
33
+ @_runner ||= tool_options.runner_class.new(
34
+ changed_files: limit_targets? ? changed_files : nil,
35
+ file_filter: tool_options.file_filter
36
+ ).tap { |r| log_runner(r) }
37
+ end
38
+
39
+ def log_runner(r)
40
+ command_string = r.exec_command ? "`#{r.exec_command.join(" ")}`" : "(skipped)"
41
+ info("Runner #{r.tool_name} exec_command: #{command_string}")
42
+ debug("Full exec_command for #{r.tool_name}", data: r.exec_command)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,4 +1,4 @@
1
- require_relative "./base_executor"
1
+ require_relative "base_executor"
2
2
 
3
3
  module QuietQuality
4
4
  module Executors
@@ -53,8 +53,10 @@ module QuietQuality
53
53
  data_text = JSON.pretty_generate(data)
54
54
  message = message + "\n" + data_text
55
55
  end
56
- prefixed_message = message.split("\n").map { |line| "[#{prefix}] #{line}" }.join("\n")
57
- colorize(prefixed_message, message_level)
56
+ message.split("\n")
57
+ .map { |line| "[#{prefix}] #{line}" }
58
+ .map { |prefixed_line| colorize(prefixed_line, message_level) }
59
+ .join("\n")
58
60
  end
59
61
 
60
62
  def colorize(s, message_level)
@@ -23,6 +23,10 @@ module QuietQuality
23
23
  fail(NoMethodError, "BaseRunner subclass must implement `command`")
24
24
  end
25
25
 
26
+ def exec_command
27
+ fail(NoMethodError, "BaseRunner subclass must implement `exec_command`")
28
+ end
29
+
26
30
  def success_status?(stat)
27
31
  stat.success?
28
32
  end
@@ -2,6 +2,8 @@ module QuietQuality
2
2
  module Tools
3
3
  module Brakeman
4
4
  class Parser
5
+ include Logging
6
+
5
7
  def initialize(text)
6
8
  @text = text
7
9
  end
@@ -24,6 +26,8 @@ module QuietQuality
24
26
  def check_errors!
25
27
  errors = data[:errors]
26
28
  return if errors.nil? || errors.empty?
29
+ warn "Brakeman errors:"
30
+ errors.each { |error| warn " #{error}" }
27
31
  fail(ParsingError, "Found #{errors.length} errors in brakeman output")
28
32
  end
29
33
 
@@ -10,6 +10,10 @@ module QuietQuality
10
10
  ["brakeman", "-f", "json"]
11
11
  end
12
12
 
13
+ def exec_command
14
+ ["brakeman"]
15
+ end
16
+
13
17
  # These are specified in constants at the top of brakeman.rb:
14
18
  # https://github.com/presidentbeef/brakeman/blob/main/lib/brakeman.rb#L6-L25
15
19
  def failure_status?(stat)
@@ -1,4 +1,4 @@
1
- require_relative "./rubocop"
1
+ require_relative "rubocop"
2
2
 
3
3
  module QuietQuality
4
4
  module Tools
@@ -14,6 +14,10 @@ module QuietQuality
14
14
  ["haml-lint", "--reporter", "json"]
15
15
  end
16
16
 
17
+ def base_exec_command
18
+ ["haml-lint"]
19
+ end
20
+
17
21
  def relevant_path?(path)
18
22
  path.end_with?(".haml")
19
23
  end
@@ -10,15 +10,21 @@ module QuietQuality
10
10
  "[]"
11
11
  end
12
12
 
13
- def command
13
+ def command(json: true)
14
14
  return nil if skip_execution?
15
+ base_command = ["mdl"]
16
+ base_command << "--json" if json
15
17
  if target_files.any?
16
- ["mdl", "--json"] + target_files.sort
18
+ base_command + target_files.sort
17
19
  else
18
- ["mdl", "--json", "."]
20
+ base_command + ["."]
19
21
  end
20
22
  end
21
23
 
24
+ def exec_command
25
+ command(json: false)
26
+ end
27
+
22
28
  def relevant_path?(path)
23
29
  path.end_with?(".md")
24
30
  end
@@ -1,4 +1,4 @@
1
- require_relative "./base_runner"
1
+ require_relative "base_runner"
2
2
 
3
3
  module QuietQuality
4
4
  module Tools
@@ -16,6 +16,11 @@ module QuietQuality
16
16
  base_command + target_files.sort
17
17
  end
18
18
 
19
+ def exec_command
20
+ return nil if skip_execution?
21
+ base_exec_command + target_files.sort
22
+ end
23
+
19
24
  def relevant_path?(path)
20
25
  fail(NoMethodError, "RelevantRunner subclass must implement `relevant_path?`")
21
26
  end
@@ -24,6 +29,10 @@ module QuietQuality
24
29
  fail(NoMethodError, "RelevantRunner subclass must implement either `command` or `base_command`")
25
30
  end
26
31
 
32
+ def base_exec_command
33
+ fail(NoMethodError, "RelevantRunner subclass must implement either `exec_command` or `base_exec_command`")
34
+ end
35
+
27
36
  def no_files_output
28
37
  fail(NoMethodError, "RelevantRunner subclass must implement `no_files_output`")
29
38
  end
@@ -2,6 +2,8 @@ module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
4
  class Parser
5
+ include Logging
6
+
5
7
  def initialize(text)
6
8
  @text = text
7
9
  end
@@ -39,7 +41,9 @@ module QuietQuality
39
41
  end
40
42
 
41
43
  def examples
42
- @_examples ||= content.fetch(:examples)
44
+ return @_examples if defined?(@_examples)
45
+ raise_if_errors_outside_of_examples!
46
+ @_examples = content.fetch(:examples)
43
47
  end
44
48
 
45
49
  def failed_examples
@@ -63,6 +67,25 @@ module QuietQuality
63
67
  tool_name: TOOL_NAME
64
68
  )
65
69
  end
70
+
71
+ def errors_count
72
+ @_errors_count ||= content.dig(:summary, :errors_outside_of_examples_count) || 0
73
+ end
74
+
75
+ def error_messages
76
+ @_error_messages ||= content.fetch(:messages, [])
77
+ end
78
+
79
+ def raise_if_errors_outside_of_examples!
80
+ return if errors_count < 1
81
+ warn "RSpec errors:"
82
+ warn "-" * 80
83
+ error_messages.each do |msg|
84
+ warn msg
85
+ warn "-" * 80
86
+ end
87
+ fail Rspec::Error, "Rspec encountered #{errors_count} errors outside of examples"
88
+ end
66
89
  end
67
90
  end
68
91
  end
@@ -14,6 +14,10 @@ module QuietQuality
14
14
  ["rspec", "-f", "json"]
15
15
  end
16
16
 
17
+ def base_exec_command
18
+ ["rspec"]
19
+ end
20
+
17
21
  def relevant_path?(path)
18
22
  path.end_with?("_spec.rb")
19
23
  end
@@ -2,6 +2,8 @@ module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
4
  TOOL_NAME = :rspec
5
+
6
+ Error = Class.new(Tools::Error)
5
7
  end
6
8
  end
7
9
  end
@@ -14,6 +14,10 @@ module QuietQuality
14
14
  ["rubocop", "-f", "json"]
15
15
  end
16
16
 
17
+ def base_exec_command
18
+ ["rubocop"]
19
+ end
20
+
17
21
  def relevant_path?(path)
18
22
  path.end_with?(".rb")
19
23
  end
@@ -14,6 +14,10 @@ module QuietQuality
14
14
  ["standardrb", "-f", "json"]
15
15
  end
16
16
 
17
+ def base_exec_command
18
+ ["standardrb"]
19
+ end
20
+
17
21
  def relevant_path?(path)
18
22
  path.end_with?(".rb")
19
23
  end
@@ -1,4 +1,4 @@
1
- require_relative "./rubocop"
1
+ require_relative "rubocop"
2
2
 
3
3
  module QuietQuality
4
4
  module Tools
@@ -8,8 +8,8 @@ module QuietQuality
8
8
  end
9
9
  end
10
10
 
11
- require_relative "./tools/base_runner"
12
- require_relative "./tools/relevant_runner"
11
+ require_relative "tools/base_runner"
12
+ require_relative "tools/relevant_runner"
13
13
 
14
14
  glob = File.expand_path("../tools/*.rb", __FILE__)
15
15
  Dir.glob(glob).sort.each { |f| require f }
@@ -1,3 +1,3 @@
1
1
  module QuietQuality
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.1"
3
3
  end
data/lib/quiet_quality.rb CHANGED
@@ -16,7 +16,7 @@ module QuietQuality
16
16
  end
17
17
  end
18
18
 
19
- require_relative "./quiet_quality/logger"
20
- require_relative "./quiet_quality/logging"
19
+ require_relative "quiet_quality/logger"
20
+ require_relative "quiet_quality/logging"
21
21
  glob = File.expand_path("../quiet_quality/*.rb", __FILE__)
22
22
  Dir.glob(glob).sort.each { |f| require f }
@@ -34,12 +34,12 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "git", "~> 1.18"
35
35
  spec.add_dependency "git_diff_parser", "~> 4"
36
36
 
37
- spec.add_development_dependency "rspec", "~> 3.10"
37
+ spec.add_development_dependency "rspec", "~> 3.13"
38
38
  spec.add_development_dependency "rspec-its", "~> 1.3"
39
39
  spec.add_development_dependency "simplecov", "~> 0.22.0"
40
40
  spec.add_development_dependency "pry", "~> 0.14"
41
- spec.add_development_dependency "standard", "~> 1.28"
42
- spec.add_development_dependency "rubocop", "~> 1.50"
41
+ spec.add_development_dependency "standard", ">= 1.35.1"
42
+ spec.add_development_dependency "rubocop", ">= 1.62"
43
43
  spec.add_development_dependency "debug", "~> 1.7"
44
44
  spec.add_development_dependency "mdl", "~> 0.12"
45
45
  spec.add_development_dependency "rspec-cover_it", "~> 0.1.0"
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.4.0
4
+ version: 1.5.1
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-07-12 00:00:00.000000000 Z
11
+ date: 2024-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.10'
47
+ version: '3.13'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.10'
54
+ version: '3.13'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec-its
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -98,30 +98,30 @@ dependencies:
98
98
  name: standard
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '1.28'
103
+ version: 1.35.1
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '1.28'
110
+ version: 1.35.1
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rubocop
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '1.50'
117
+ version: '1.62'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: '1.50'
124
+ version: '1.62'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: debug
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +184,7 @@ files:
184
184
  - ".quiet_quality.yml"
185
185
  - ".rspec"
186
186
  - ".rubocop.yml"
187
+ - ".standard.yml"
187
188
  - CHANGELOG.md
188
189
  - Gemfile
189
190
  - LICENSE
@@ -199,6 +200,7 @@ files:
199
200
  - lib/quiet_quality/cli.rb
200
201
  - lib/quiet_quality/cli/arg_parser.rb
201
202
  - lib/quiet_quality/cli/entrypoint.rb
203
+ - lib/quiet_quality/cli/message_formatter.rb
202
204
  - lib/quiet_quality/cli/presenter.rb
203
205
  - lib/quiet_quality/colorize.rb
204
206
  - lib/quiet_quality/config.rb
@@ -212,6 +214,7 @@ files:
212
214
  - lib/quiet_quality/executors.rb
213
215
  - lib/quiet_quality/executors/base_executor.rb
214
216
  - lib/quiet_quality/executors/concurrent_executor.rb
217
+ - lib/quiet_quality/executors/execcer.rb
215
218
  - lib/quiet_quality/executors/pipeline.rb
216
219
  - lib/quiet_quality/executors/serial_executor.rb
217
220
  - lib/quiet_quality/logger.rb