Skip to content

Commit

Permalink
Merge pull request #111 from nevinera/nev-95-message-formatting
Browse files Browse the repository at this point in the history
Add a `--message-format` argument, so we can have more readable output on the CLI
  • Loading branch information
nevinera authored Nov 15, 2023
2 parents 3378efa + ebc0af2 commit 9cc1376
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 4 deletions.
1 change: 1 addition & 0 deletions .quiet_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ changed_files: false
filter_messages: false
logging: light
colorize: true
message_format: "%lcyan10tool| [%myellow40rule] %bred60loc %e-90body"
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ The configuration file supports the following _global_ options (top-level keys):
* `colorize`: by default, `bin/qq` will include color codes in its output, to
make failing tools easier to spot, and messages easier to read. But you can
supply `colorize: false` to tell it not to do that if you don't want them.
* `message_format`: you can specify a format string with which to render the
messages, which interpolates values with various formatting flags. Details
given in the "Message Formatting" section below.

And then each tool can have an entry, within which `changed_files` and
`filter_messages` can be specified - the tool-specific settings override the
Expand Down Expand Up @@ -181,6 +184,40 @@ generated file like `db/schema.rb`, and that file doesn't meet your rubocop (or
standardrb) rules, you'll get _told_ unless you exclude it at the quiet-quality
level as well.

### Message Formatting

You can supply a message-format string on the cli or in your config file, which
will override the default formatting for message output on the CLI. These format
strings are intended to be a single line containing "substitution tokens", which
each look like `%[lr]?[bem]?color?(Size)(Source)`.

* The first (optional) flag can be an "l", and "r", or be left off (which is the
same as "l"). This flag indicates the 'justification' - left or right.
* The second (optional) flag can be a "b", an "e", or an "m", defaulting to "e";
these stand for "beginning", "ending", and "middle", and represent what part
of the string should be truncated if it needs to be shortened.
* The third (optional) part is a color name, and can be any of "yellow", "red",
"green", "blue", "cyan", or "none" (leaving it off is the same as specifing
"none"). This is the color to use for the token in the output - note that any
color supplied here is used regardless of the '--colorize' flag.
* The fourth part of the token is required, and is the _size_ of the token. If a
positive integer is supplied, then the token will take up that much space, and
will be padded on the appropriate side if necessary; if a negative integer is
supplied, then the token will not be padded out, but will still get truncated
if it is too long. The value '0' is special, and indicates that the token
should be neither padded nor truncated.
* The last part of the token is a string indicating the _source_ data to
represent, and must be one of these values: "tool", "loc", "level", "path",
"lines", "rule", "body". Each of these represents one piece of data out of the
message object that can be rendered into the message line.

Some example message formats:

```text
%lcyan8tool | %lmyellow30rule | %0loc
%le6tool [%mblue20rule] %b45loc %cyan-100body
```

### CLI Options

To specify which _tools_ to run (and if any are specified, the `default_tools`
Expand Down
7 changes: 7 additions & 0 deletions lib/quiet_quality/cli/arg_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def parser
setup_filter_messages_options(parser)
setup_colorization_options(parser)
setup_logging_options(parser)
setup_message_formatting_options(parser)
setup_verbosity_options(parser)
end
end
Expand Down Expand Up @@ -168,6 +169,12 @@ def setup_logging_options(parser)
end
end

def setup_message_formatting_options(parser)
parser.on("-F", "--message-format FMT", "A format string with which to print messages") do |fmt|
set_global_option(:message_format, fmt)
end
end

def setup_verbosity_options(parser)
parser.on("-v", "--verbose", "Log more verbosely - multiple times is more verbose") do
QuietQuality.logger.increase_level!
Expand Down
190 changes: 190 additions & 0 deletions lib/quiet_quality/cli/message_formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
module QuietQuality
module Cli
class MessageFormatter
TOKEN_MATCHING_REGEX = %r{%[a-z]*-?\d+(?:tool|loc|level|path|lines|rule|body)}

def initialize(message_format:)
@message_format = message_format
end

def format(message)
formatted_tokens = parsed_tokens.map { |pt| FormattedToken.new(parsed_token: pt, message: message) }
formatted_tokens.reduce(message_format) do |interpolating, ftok|
interpolating.gsub(ftok.token, ftok.formatted_token)
end
end

private

attr_reader :message_format

def tokens
@_tokens ||= message_format.scan(TOKEN_MATCHING_REGEX)
end

def parsed_tokens
@_parsed_tokens ||= tokens.map { |tok| ParsedToken.new(tok) }
end

class ParsedToken
TOKEN_PARSING_REGEX = %r{
% # start the interplation token
(?<just>[lr])? # specify the justification
(?<trunc>[bem])? # where to truncate from
(?<color>yellow|red|green|blue|cyan|none)? # what color
(?<size>-?\d+) # string size (may be negative)
(?<source>tool|loc|level|path|lines|rule|body) # data source name
}x

COLORS = {
"yellow" => :yellow,
"red" => :red,
"green" => :green,
"blue" => :light_blue,
"cyan" => :light_cyan,
"none" => nil
}.freeze

JUSTIFICATIONS = {"l" => :left, "r" => :right}.freeze
TRUNCATIONS = {"b" => :beginning, "m" => :middle, "e" => :ending}.freeze

def initialize(token)
@token = token
end

attr_reader :token

def justification
JUSTIFICATIONS.fetch(token_pieces[:just]&.downcase, :left)
end

def truncation
TRUNCATIONS.fetch(token_pieces[:trunc]&.downcase, :ending)
end

def color
COLORS.fetch(token_pieces[:color]&.downcase, nil)
end

def size
raw_size.abs
end

def source
token_pieces[:source]
end

def allow_pad?
raw_size.positive?
end

def allow_truncate?
!raw_size.zero?
end

private

def token_pieces
@_token_pieces ||= token.match(TOKEN_PARSING_REGEX)
end

def raw_size
@_raw_size ||= token_pieces[:size].to_i
end
end
private_constant :ParsedToken

class FormattedToken
def initialize(parsed_token:, message:)
@parsed_token = parsed_token
@message = message
end

def formatted_token
colorized(padded(truncated(base_string)))
end

def token
parsed_token.token
end

private

attr_reader :parsed_token, :message

def line_range
if message.start_line == message.stop_line
message.start_line.to_s
else
"#{message.start_line}-#{message.stop_line}"
end
end

def base_string
case parsed_token.source
when "tool" then message.tool_name
when "loc" then location_string
when "level" then message.level
when "path" then message.path
when "lines" then line_range
when "rule" then message.rule
when "body" then flattened_body
end
end

def location_string
"#{message.path}:#{line_range}"
end

def flattened_body
message.body.gsub(/ *\n */, "\\n")
end

def truncated(s)
return s unless parsed_token.allow_truncate?
return s if s.length <= parsed_token.size
size = parsed_token.size

case parsed_token.truncation
when :beginning then truncate_beginning(s, size)
when :middle then truncate_middle(s, size)
when :ending then truncate_ending(s, size)
end
end

def truncate_beginning(s, size)
"…" + s.slice(1 - size, size - 1)
end

def truncate_middle(s, size)
front_len = (size / 2.0).floor
back_len = (size / 2.0).ceil - 1
s.slice(0, front_len) + "…" + s.slice(-back_len, back_len)
end

def truncate_ending(s, size)
s.slice(0, size - 1) + "…"
end

def padded(s)
return s unless parsed_token.allow_pad?
return s if s.length >= parsed_token.size

case parsed_token.justification
when :left then s.ljust(parsed_token.size)
when :right then s.rjust(parsed_token.size)
end
end

def colorized(s)
if parsed_token.color.nil?
s
else
Colorize.colorize(s, color: parsed_token.color)
end
end
end
private_constant :FormattedToken
end
end
end
20 changes: 18 additions & 2 deletions lib/quiet_quality/cli/presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,28 @@ def reduce_text(s, length)
s.gsub(/ *\n */, "\\n").slice(0, length)
end

def log_message(msg)
def locally_formatted_message(msg)
tool = colorize(:yellow, msg.tool_name)
line_range = line_range_for(msg)
rule_string = msg.rule ? " [#{colorize(:yellow, msg.rule)}]" : ""
truncated_body = reduce_text(msg.body, 120)
stream.puts "#{tool} #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
"#{tool} #{msg.path}:#{line_range}#{rule_string} #{truncated_body}"
end

def loggable_message(msg)
if options.message_format
message_formatter.format(msg)
else
stream.puts locally_formatted_message(msg)
end
end

def log_message(msg)
stream.puts loggable_message(msg)
end

def message_formatter
@_message_formatter ||= MessageFormatter.new(message_format: options.message_format)
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/quiet_quality/config/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def update_comparison_branch
def update_logging
set_unless_nil(options, :logging, apply.global_option(:logging))
set_unless_nil(options, :colorize, apply.global_option(:colorize))
set_unless_nil(options, :message_format, apply.global_option(:message_format))
end

# ---- update the tool options (apply global forms first) -------
Expand Down
4 changes: 3 additions & 1 deletion lib/quiet_quality/config/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ def initialize
@comparison_branch = nil
@colorize = true
@logging = :normal
@message_format = nil
end

attr_accessor :tools, :comparison_branch, :annotator, :executor, :exec_tool
attr_accessor :tools, :comparison_branch, :annotator, :executor, :exec_tool, :message_format
attr_reader :logging
attr_writer :colorize

Expand Down Expand Up @@ -42,6 +43,7 @@ def to_h
comparison_branch: comparison_branch,
colorize: colorize?,
logging: logging,
message_format: message_format,
tools: tool_hashes_by_name
}
end
Expand Down
1 change: 1 addition & 0 deletions lib/quiet_quality/config/parsed_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ParsedOptions
:comparison_branch,
:colorize,
:logging,
:message_format,
:limit_targets,
:filter_messages,
:file_filter
Expand Down
1 change: 1 addition & 0 deletions lib/quiet_quality/config/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def store_global_options(opts)
read_global_option(opts, :unfiltered, :filter_messages, as: :reversed_boolean)
read_global_option(opts, :colorize, :colorize, as: :boolean)
read_global_option(opts, :logging, :logging, as: :symbol, validate_from: Options::LOGGING_LEVELS)
read_global_option(opts, :message_format, :message_format, as: :string)
end

def store_tool_options(opts)
Expand Down
5 changes: 5 additions & 0 deletions spec/quiet_quality/cli/arg_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def self.expect_usage_error(desc, arguments, matcher)
-l, --light Print aggregated results only
-q, --quiet Don't print results, only return a status code
-L, --logging LEVEL Specify logging mode (from normal/light/quiet)
-F, --message-format FMT A format string with which to print messages
-v, --verbose Log more verbosely - multiple times is more verbose
HELP_OUTPUT
end
Expand Down Expand Up @@ -166,6 +167,10 @@ def self.expect_usage_error(desc, arguments, matcher)
expect_options("--logging normal", ["--logging", "normal"], global: {logging: :normal})
expect_options("-Lnormal", ["-Lnormal"], global: {logging: :normal})
expect_usage_error("-Lshenanigans", ["-Lshenanigans"], /Unrecognized logging level/i)

expect_options("without message-format", [], global: {message_format: nil})
expect_options("-F '%lmcyan20rule %lbred40tool'", ["-F", "%lmcyan20rule %lbred40tool"], global: {message_format: "%lmcyan20rule %lbred40tool"})
expect_options("--message-format '%lmcyan20rule %lbred40tool'", ["--message-format", "%lmcyan20rule %lbred40tool"], global: {message_format: "%lmcyan20rule %lbred40tool"})
end

describe "logging color options" do
Expand Down
Loading

0 comments on commit 9cc1376

Please sign in to comment.