quiet_quality 1.4.0 → 1.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/dogfood.yml +1 -1
- data/.github/workflows/linters.yml +1 -1
- data/.github/workflows/rspec.yml +1 -1
- data/.quiet_quality.yml +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +12 -0
- data/README.md +37 -0
- data/lib/quiet_quality/cli/arg_parser.rb +12 -0
- data/lib/quiet_quality/cli/entrypoint.rb +21 -1
- data/lib/quiet_quality/cli/message_formatter.rb +190 -0
- data/lib/quiet_quality/cli/presenter.rb +18 -2
- data/lib/quiet_quality/cli.rb +2 -2
- data/lib/quiet_quality/config/builder.rb +15 -1
- data/lib/quiet_quality/config/options.rb +5 -1
- data/lib/quiet_quality/config/parsed_options.rb +2 -0
- data/lib/quiet_quality/config/parser.rb +1 -0
- data/lib/quiet_quality/executors/execcer.rb +46 -0
- data/lib/quiet_quality/executors/serial_executor.rb +1 -1
- data/lib/quiet_quality/tools/base_runner.rb +4 -0
- data/lib/quiet_quality/tools/brakeman/runner.rb +4 -0
- data/lib/quiet_quality/tools/brakeman.rb +1 -1
- data/lib/quiet_quality/tools/haml_lint/runner.rb +4 -0
- data/lib/quiet_quality/tools/markdown_lint/runner.rb +9 -3
- data/lib/quiet_quality/tools/relevant_runner.rb +10 -1
- data/lib/quiet_quality/tools/rspec/runner.rb +4 -0
- data/lib/quiet_quality/tools/rubocop/runner.rb +4 -0
- data/lib/quiet_quality/tools/standardrb/runner.rb +4 -0
- data/lib/quiet_quality/tools/standardrb.rb +1 -1
- data/lib/quiet_quality/tools.rb +2 -2
- data/lib/quiet_quality/version.rb +1 -1
- data/lib/quiet_quality.rb +2 -2
- metadata +5 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 7ff5a82876936d746842d76e055475ba19e78d0bfff7967b2f179a2262cb5d7e
         | 
| 4 | 
            +
              data.tar.gz: 529ed1d29fdf3c3c6ba381e7ab71cbf006bc44c8d608858e8ec84d67c2efb267
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 1b840cfe8d90929c0fcc32fdc1370310924a46464006b39afa5dceff69b8c24254d701002e77f33a845baaa7fe5373243486b5d1422a3beea4fd45a50e65ec8b
         | 
| 7 | 
            +
              data.tar.gz: e3176f406a5de5a027cae5a2edcbd897395a0191dad1a6cb402bf48fb005f9dce3783d8e6778f91e7d05c03b3f12f473ee23438b9235ed5eedd5e4d588a58526
         | 
    
        data/.github/workflows/rspec.yml
    CHANGED
    
    
    
        data/.quiet_quality.yml
    CHANGED
    
    
    
        data/.standard.yml
    ADDED
    
    
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,17 @@ | |
| 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 | 
            +
             | 
| 3 15 | 
             
            ## Release 1.4.0
         | 
| 4 16 |  | 
| 5 17 | 
             
            * 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 | 
            -
                       | 
| 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  | 
| 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 | 
            -
                     | 
| 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
         | 
    
        data/lib/quiet_quality/cli.rb
    CHANGED
    
    
| @@ -22,7 +22,7 @@ module QuietQuality | |
| 22 22 | 
             
                    Options.new.tap { |opts| opts.tools = tools }
         | 
| 23 23 | 
             
                  end
         | 
| 24 24 |  | 
| 25 | 
            -
                  def  | 
| 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) -------
         | 
| @@ -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
         | 
| @@ -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)
         | 
| @@ -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
         | 
| @@ -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)
         | 
| @@ -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 | 
            -
                         | 
| 18 | 
            +
                        base_command + target_files.sort
         | 
| 17 19 | 
             
                      else
         | 
| 18 | 
            -
                         | 
| 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 " | 
| 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
         | 
    
        data/lib/quiet_quality/tools.rb
    CHANGED
    
    | @@ -8,8 +8,8 @@ module QuietQuality | |
| 8 8 | 
             
              end
         | 
| 9 9 | 
             
            end
         | 
| 10 10 |  | 
| 11 | 
            -
            require_relative " | 
| 12 | 
            -
            require_relative " | 
| 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 }
         | 
    
        data/lib/quiet_quality.rb
    CHANGED
    
    | @@ -16,7 +16,7 @@ module QuietQuality | |
| 16 16 | 
             
              end
         | 
| 17 17 | 
             
            end
         | 
| 18 18 |  | 
| 19 | 
            -
            require_relative " | 
| 20 | 
            -
            require_relative " | 
| 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. | 
| 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- | 
| 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,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
         |