quiet_quality 1.3.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) 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 +17 -0
  8. data/README.md +54 -8
  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 +15 -1
  15. data/lib/quiet_quality/config/file_filter.rb +52 -0
  16. data/lib/quiet_quality/config/options.rb +5 -1
  17. data/lib/quiet_quality/config/parsed_options.rb +2 -0
  18. data/lib/quiet_quality/config/parser.rb +11 -1
  19. data/lib/quiet_quality/config/tool_options.rb +4 -7
  20. data/lib/quiet_quality/executors/execcer.rb +46 -0
  21. data/lib/quiet_quality/executors/serial_executor.rb +1 -1
  22. data/lib/quiet_quality/tools/base_runner.rb +4 -0
  23. data/lib/quiet_quality/tools/brakeman/runner.rb +4 -0
  24. data/lib/quiet_quality/tools/brakeman.rb +1 -1
  25. data/lib/quiet_quality/tools/haml_lint/runner.rb +4 -0
  26. data/lib/quiet_quality/tools/markdown_lint/runner.rb +9 -3
  27. data/lib/quiet_quality/tools/relevant_runner.rb +10 -1
  28. data/lib/quiet_quality/tools/rspec/runner.rb +4 -0
  29. data/lib/quiet_quality/tools/rubocop/runner.rb +4 -0
  30. data/lib/quiet_quality/tools/standardrb/runner.rb +4 -0
  31. data/lib/quiet_quality/tools/standardrb.rb +1 -1
  32. data/lib/quiet_quality/tools.rb +2 -2
  33. data/lib/quiet_quality/version.rb +1 -1
  34. data/lib/quiet_quality.rb +2 -2
  35. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7049c44ac27b14f8f599dfcb080dcd7d6aadceed0cee69f0d9f460ccb23bd66c
4
- data.tar.gz: 6ec55838027c9bd81659109807e27e50199b5c54e1e17d04b2b0727819851d89
3
+ metadata.gz: 7ff5a82876936d746842d76e055475ba19e78d0bfff7967b2f179a2262cb5d7e
4
+ data.tar.gz: 529ed1d29fdf3c3c6ba381e7ab71cbf006bc44c8d608858e8ec84d67c2efb267
5
5
  SHA512:
6
- metadata.gz: 1cac5b2196f3409ab37252830b115cf7ec54b0cc21e2e5a25062cb4cd79615f7378a6ac4bd2d4fd39a0c1ff6882948fa2e6e1f7029dd88b1b9989fb8910aa0b6
7
- data.tar.gz: e48380c23c35946e11c011bb1bdb694f6731d93828fa06a4d725a9d9a9c347e3f8ee5a8687904593905da9be74c5d8903b09f72c815f2dbdcb5fdedcc5f8e69f
6
+ metadata.gz: 1b840cfe8d90929c0fcc32fdc1370310924a46464006b39afa5dceff69b8c24254d701002e77f33a845baaa7fe5373243486b5d1422a3beea4fd45a50e65ec8b
7
+ data.tar.gz: e3176f406a5de5a027cae5a2edcbd897395a0191dad1a6cb402bf48fb005f9dce3783d8e6778f91e7d05c03b3f12f473ee23438b9235ed5eedd5e4d588a58526
@@ -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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## Release 1.5.0
4
+
5
+ * Update to comply with current standardrb rules, and use checkout@v4
6
+ * Add a `-X/--exec` argument that allows you to let qq craft the command, but
7
+ then actually exec the command instead of running it and handling its output.
8
+ Especially useful for things like `rspec`, where the output it gives you about
9
+ failing tests is very useful, and qq is mostly only helpful for determining
10
+ what specs to run.
11
+ * Add a `--message-format` argument and `message_format` config file option,
12
+ which allow for a fairly complex configuration of the output format for
13
+ messages, so they can be displayed in various colorized/tabular formats.
14
+
15
+ ## Release 1.4.0
16
+
17
+ * Support specifying `excludes` per-tool, so that certain files won't be passed
18
+ to those tools on the command-line (#107 resolves #106)
19
+
3
20
  ## Release 1.3.1
4
21
 
5
22
  * Fix a bug around the logging of nil commands when runners are skipped (#104
data/README.md CHANGED
@@ -151,19 +151,22 @@ 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
157
160
  global ones.
158
161
 
159
- The tools have one additional setting that is not available at a global level:
160
- `file_filter`. This is a string that will be turned into a _ruby regex_, and
161
- used to limit what file paths are passed to the tool. For example, if you are
162
- working in a rails engine `engines/foo/`, and you touch one of the rspec tests
163
- there, you would not want `qq` in the root of the repository to run
164
- `rspec engines/foo/spec/foo/thing_spec.rb` - that probably won't work, as your
165
- engine will have its own test setup code and Gemfile. This setting is mostly
166
- intended to be used like this:
162
+ The tools have two additional settings that are not available at a global level:
163
+ `file_filter` and `excludes`. `file_filter` is a string that will be turned into
164
+ a _ruby regex_, and used to limit what file paths are passed to the tool. For
165
+ example, if you are working in a rails engine `engines/foo/`, and you touch one
166
+ of the rspec tests there, you would not want `qq` in the root of the repository
167
+ to run `rspec engines/foo/spec/foo/thing_spec.rb` - that probably won't work, as
168
+ your engine will have its own test setup code and Gemfile. This setting is
169
+ mostly intended to be used like this:
167
170
 
168
171
  ```yaml
169
172
  rspec:
@@ -172,6 +175,49 @@ rspec:
172
175
  file_filter: "^spec/"
173
176
  ```
174
177
 
178
+ `excludes` are more specific in meaning - this is an _array_ of regexes, and any
179
+ file that matches any of these regexes will _not_ be passed to the tool as an
180
+ explicit command line argument. This is generally because tools like rubocop
181
+ have internal systems for excluding files, but if you pass a filename on the
182
+ cli, those systems are ignored. That means that if you have changes to a
183
+ generated file like `db/schema.rb`, and that file doesn't meet your rubocop (or
184
+ standardrb) rules, you'll get _told_ unless you exclude it at the quiet-quality
185
+ level as well.
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
+
175
221
  ### CLI Options
176
222
 
177
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
@@ -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) -------
@@ -0,0 +1,52 @@
1
+ module QuietQuality
2
+ module Config
3
+ class FileFilter
4
+ # * regex is a regex string
5
+ # * excludes is an array of regex strings OR a single regex string
6
+ def initialize(regex: nil, excludes: nil)
7
+ @regex_string = regex
8
+ @excludes_strings = excludes
9
+ end
10
+
11
+ def regex
12
+ return nil if @regex_string.nil?
13
+ @_regex ||= Regexp.new(@regex_string)
14
+ end
15
+
16
+ def excludes
17
+ return @_excludes if defined?(@_excludes)
18
+
19
+ @_excludes =
20
+ if @excludes_strings.nil?
21
+ nil
22
+ elsif @excludes_strings.is_a?(String)
23
+ [Regexp.new(@excludes_strings)]
24
+ else
25
+ @excludes_strings.map { |xs| Regexp.new(xs) }
26
+ end
27
+ end
28
+
29
+ # The filter _overall_ matches if:
30
+ # (a) the regex either matches or is not supplied AND
31
+ # (b) either none of the excludes match or none are supplied
32
+ def match?(s)
33
+ regex_match?(s) && !excludes_match?(s)
34
+ end
35
+
36
+ private
37
+
38
+ # The regex is an allow-match - if it's not supplied, treat everything as matching.
39
+ def regex_match?(s)
40
+ return true if regex.nil?
41
+ regex.match?(s)
42
+ end
43
+
44
+ # The excludes are a list of deny-matches - if they're not supplied, treat _nothing_
45
+ # as matching.
46
+ def excludes_match?(s)
47
+ return false if excludes.nil? || excludes.empty?
48
+ excludes.any? { |exclude| exclude.match?(s) }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -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
@@ -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,7 @@ 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_tool_option(opts, tool_name, :file_filter, :file_filter, as: :string)
68
+ read_file_filter(opts, tool_name)
68
69
  end
69
70
 
70
71
  def invalid!(message)
@@ -97,6 +98,15 @@ module QuietQuality
97
98
  opts.set_tool_option(tool, into, coerced_value)
98
99
  end
99
100
 
101
+ def read_file_filter(opts, tool)
102
+ parsed_regex = data.dig(tool.to_sym, :file_filter)
103
+ parsed_excludes = data.dig(tool.to_sym, :excludes)
104
+ if parsed_regex || parsed_excludes
105
+ filter = Config::FileFilter.new(regex: parsed_regex, excludes: parsed_excludes)
106
+ opts.set_tool_option(tool, :file_filter, filter)
107
+ end
108
+ end
109
+
100
110
  def validate_value(name, value, as:, from: nil)
101
111
  case as
102
112
  when :boolean then validate_boolean(name, value)
@@ -8,8 +8,9 @@ module QuietQuality
8
8
  @file_filter = file_filter
9
9
  end
10
10
 
11
+ attr_accessor :file_filter
11
12
  attr_reader :tool_name
12
- attr_writer :limit_targets, :filter_messages, :file_filter
13
+ attr_writer :limit_targets, :filter_messages
13
14
 
14
15
  def limit_targets?
15
16
  @limit_targets
@@ -31,17 +32,13 @@ module QuietQuality
31
32
  tool_namespace::Parser
32
33
  end
33
34
 
34
- def file_filter
35
- return nil if @file_filter.nil?
36
- Regexp.new(@file_filter)
37
- end
38
-
39
35
  def to_h
40
36
  {
41
37
  tool_name: tool_name,
42
38
  limit_targets: limit_targets?,
43
39
  filter_messages: filter_messages?,
44
- file_filter: file_filter&.to_s
40
+ file_filter: file_filter&.regex&.to_s,
41
+ excludes: file_filter&.excludes&.map(&:to_s)
45
42
  }
46
43
  end
47
44
  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
@@ -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
@@ -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
@@ -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
@@ -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.3.1"
2
+ VERSION = "1.5.0"
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 }
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.3.1
4
+ version: 1.5.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-14 00:00:00.000000000 Z
11
+ date: 2023-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git
@@ -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,10 +200,12 @@ 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
205
207
  - lib/quiet_quality/config/builder.rb
208
+ - lib/quiet_quality/config/file_filter.rb
206
209
  - lib/quiet_quality/config/finder.rb
207
210
  - lib/quiet_quality/config/options.rb
208
211
  - lib/quiet_quality/config/parsed_options.rb
@@ -211,6 +214,7 @@ files:
211
214
  - lib/quiet_quality/executors.rb
212
215
  - lib/quiet_quality/executors/base_executor.rb
213
216
  - lib/quiet_quality/executors/concurrent_executor.rb
217
+ - lib/quiet_quality/executors/execcer.rb
214
218
  - lib/quiet_quality/executors/pipeline.rb
215
219
  - lib/quiet_quality/executors/serial_executor.rb
216
220
  - lib/quiet_quality/logger.rb
@@ -266,7 +270,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
270
  - !ruby/object:Gem::Version
267
271
  version: '0'
268
272
  requirements: []
269
- rubygems_version: 3.3.7
273
+ rubygems_version: 3.4.10
270
274
  signing_key:
271
275
  specification_version: 4
272
276
  summary: A system for comparing quality tool outputs against the forward diffs