diff --git a/README.md b/README.md index c2027864..d8396fbc 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,35 @@ def autocorrect(_processed_source, offense) end ``` +## Output formats + +You can change the output format of ERB Lint by specifying formatters with the `-f/--format` option. + +### Multiline (default) + +```sh +$ erblint +Linting 8 files with 12 linters... + +Remove multiple trailing newline at the end of the file. +In file: app/views/users/show.html.erb:95 + +Remove newline before `%>` to match start of tag. +In file: app/views/subscriptions/index.html.erb:38 + +2 error(s) were found in ERB files +``` + +### Compact + +```sh +erblint --format compact +Linting 8 files with 12 linters... +app/views/users/show.html.erb:95:0: Remove multiple trailing newline at the end of the file. +app/views/users/_graph.html.erb:27:37: Extra space detected where there should be no space +2 error(s) were found in ERB files +``` + ## License This project is released under the [MIT license](LICENSE.txt). diff --git a/lib/erb_lint.rb b/lib/erb_lint.rb index 2e235e3b..835561c5 100644 --- a/lib/erb_lint.rb +++ b/lib/erb_lint.rb @@ -12,8 +12,15 @@ require 'erb_lint/runner_config' require 'erb_lint/runner' require 'erb_lint/version' +require 'erb_lint/stats' +require 'erb_lint/reporter' # Load linters Dir[File.expand_path('erb_lint/linters/**/*.rb', File.dirname(__FILE__))].each do |file| require file end + +# Load reporters +Dir[File.expand_path('erb_lint/reporters/**/*.rb', File.dirname(__FILE__))].each do |file| + require file +end diff --git a/lib/erb_lint/cli.rb b/lib/erb_lint/cli.rb index 46c204f6..f26d212a 100644 --- a/lib/erb_lint/cli.rb +++ b/lib/erb_lint/cli.rb @@ -16,15 +16,6 @@ class CLI class ExitWithFailure < RuntimeError; end class ExitWithSuccess < RuntimeError; end - class Stats - attr_accessor :found, :corrected, :exceptions - def initialize - @found = 0 - @corrected = 0 - @exceptions = 0 - end - end - def initialize @options = {} @config = nil @@ -51,9 +42,12 @@ def run(args = ARGV) failure!('no linter available with current configuration') end - puts "Linting #{lint_files.size} files with "\ - "#{enabled_linter_classes.size} #{'autocorrectable ' if autocorrect?}linters..." - puts + @options[:format] ||= :multiline + @stats.files = lint_files.size + @stats.linters = enabled_linter_classes.size + + reporter = Reporter.create_reporter(@options[:format], @stats, autocorrect?) + reporter.preview runner = ERBLint::Runner.new(file_loader, @config) @@ -72,20 +66,7 @@ def run(args = ARGV) end end - if @stats.corrected > 0 - corrected_found_diff = @stats.found - @stats.corrected - if corrected_found_diff > 0 - warn(Rainbow( - "#{@stats.corrected} error(s) corrected and #{corrected_found_diff} error(s) remaining in ERB files" - ).red) - else - puts Rainbow("#{@stats.corrected} error(s) corrected in ERB files").green - end - elsif @stats.found > 0 - warn(Rainbow("#{@stats.found} error(s) were found in ERB files").red) - else - puts Rainbow("No errors were found in ERB files").green - end + reporter.show @stats.found == 0 && @stats.exceptions == 0 rescue OptionParser::InvalidOption, OptionParser::InvalidArgument, ExitWithFailure => e @@ -126,15 +107,12 @@ def run_with_corrections(runner, filename) file_content = corrector.corrected_content runner.clear_offenses end + offenses_filename = relative_filename(filename) + offenses = runner.offenses || [] - @stats.found += runner.offenses.size - runner.offenses.each do |offense| - puts <<~EOF - #{offense.message}#{Rainbow(' (not autocorrected)').red if autocorrect?} - In file: #{relative_filename(filename)}:#{offense.line_range.begin} - - EOF - end + @stats.found += offenses.size + @stats.processed_files[offenses_filename] ||= [] + @stats.processed_files[offenses_filename] |= offenses end def correct(processed_source, offenses) @@ -258,6 +236,15 @@ def option_parser end end + opts.on("--format FORMAT", format_options_help) do |format| + unless Reporter.available_format?(format) + error_message = invalid_format_error_message(format) + failure!(error_message) + end + + @options[:format] = format + end + opts.on("--lint-all", "Lint all files matching configured glob [default: #{DEFAULT_LINT_ALL_GLOB}]") do |config| @options[:lint_all] = config end @@ -289,5 +276,15 @@ def option_parser end end end + + def format_options_help + "Report offenses in the given format: "\ + "(#{Reporter.available_formats.join(', ')}) (default: multiline)" + end + + def invalid_format_error_message(given_format) + formats = Reporter.available_formats.map { |format| " - #{format}\n" } + "#{given_format}: is not a valid format. Available formats:\n#{formats.join}" + end end end diff --git a/lib/erb_lint/offense.rb b/lib/erb_lint/offense.rb index a39ca567..fa3341e3 100644 --- a/lib/erb_lint/offense.rb +++ b/lib/erb_lint/offense.rb @@ -31,5 +31,13 @@ def ==(other) def line_range Range.new(source_range.line, source_range.last_line) end + + def line_number + line_range.begin + end + + def column + source_range.column + end end end diff --git a/lib/erb_lint/reporter.rb b/lib/erb_lint/reporter.rb new file mode 100644 index 00000000..ff5e484d --- /dev/null +++ b/lib/erb_lint/reporter.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'active_support/core_ext/class' + +module ERBLint + class Reporter + def self.create_reporter(format, *args) + reporter_klass = "#{ERBLint::Reporters}::#{format.to_s.camelize}Reporter".constantize + reporter_klass.new(*args) + end + + def self.available_format?(format) + available_formats.include?(format.to_s) + end + + def self.available_formats + descendants + .map(&:to_s) + .map(&:demodulize) + .map(&:underscore) + .map { |klass_name| klass_name.sub("_reporter", "") } + .sort + end + + def initialize(stats, autocorrect) + @stats = stats + @autocorrect = autocorrect + end + + def preview; end + + def show; end + + private + + attr_reader :stats, :autocorrect + delegate :processed_files, to: :stats + end +end diff --git a/lib/erb_lint/reporters/compact_reporter.rb b/lib/erb_lint/reporters/compact_reporter.rb new file mode 100644 index 00000000..434100b5 --- /dev/null +++ b/lib/erb_lint/reporters/compact_reporter.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module ERBLint + module Reporters + class CompactReporter < Reporter + def preview + puts "Linting #{stats.files} files with "\ + "#{stats.linters} #{'autocorrectable ' if autocorrect}linters..." + end + + def show + processed_files.each do |filename, offenses| + offenses.each do |offense| + puts format_offense(filename, offense) + end + end + + footer + summary + end + + private + + def format_offense(filename, offense) + [ + "#{filename}:", + "#{offense.line_number}:", + "#{offense.column}: ", + offense.message.to_s, + ].join + end + + def footer; end + + def summary + if stats.corrected > 0 + report_corrected_offenses + elsif stats.found > 0 + warn(Rainbow("#{stats.found} error(s) were found in ERB files").red) + else + puts Rainbow("No errors were found in ERB files").green + end + end + + def report_corrected_offenses + corrected_found_diff = stats.found - stats.corrected + + if corrected_found_diff > 0 + message = Rainbow( + "#{stats.corrected} error(s) corrected and #{corrected_found_diff} error(s) remaining in ERB files" + ).red + + warn(message) + else + puts Rainbow("#{stats.corrected} error(s) corrected in ERB files").green + end + end + end + end +end diff --git a/lib/erb_lint/reporters/multiline_reporter.rb b/lib/erb_lint/reporters/multiline_reporter.rb new file mode 100644 index 00000000..3e8d014d --- /dev/null +++ b/lib/erb_lint/reporters/multiline_reporter.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require_relative "compact_reporter" + +module ERBLint + module Reporters + class MultilineReporter < CompactReporter + private + + def format_offense(filename, offense) + <<~EOF + + #{offense.message}#{Rainbow(' (not autocorrected)').red if autocorrect} + In file: #{filename}:#{offense.line_number} + EOF + end + + def footer + puts + end + end + end +end diff --git a/lib/erb_lint/stats.rb b/lib/erb_lint/stats.rb new file mode 100644 index 00000000..3b4952c7 --- /dev/null +++ b/lib/erb_lint/stats.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +module ERBLint + class Stats + attr_accessor :found, + :corrected, + :exceptions, + :linters, + :files, + :processed_files + + def initialize( + found: 0, + corrected: 0, + exceptions: 0, + linters: 0, + files: 0, + processed_files: {} + ) + @found = found + @corrected = corrected + @exceptions = exceptions + @linters = linters + @files = files + @processed_files = processed_files + end + end +end diff --git a/spec/erb_lint/cli_spec.rb b/spec/erb_lint/cli_spec.rb index 794e0cb7..52a98c0a 100644 --- a/spec/erb_lint/cli_spec.rb +++ b/spec/erb_lint/cli_spec.rb @@ -73,6 +73,13 @@ def run(_processed_source) it 'shows usage' do expect { subject }.to(output(/erblint \[options\] \[file1, file2, ...\]/).to_stdout) end + + it 'shows format instructions' do + expect { subject }.to( + output(/Report offenses in the given format: \(compact, multiline\) \(default: multiline\)/).to_stdout + ) + end + it 'is successful' do expect(subject).to(be(true)) end @@ -132,11 +139,13 @@ def run(_processed_source) context 'when errors are found' do it 'shows all error messages and line numbers' do expect { subject }.to(output(Regexp.new(Regexp.escape(<<~EOF))).to_stdout) + fake message from a fake linter In file: /app/views/template.html.erb:1 Missing a trailing newline at the end of the file. In file: /app/views/template.html.erb:1 + EOF end @@ -241,11 +250,13 @@ def run(_processed_source) context 'when errors are found' do it 'shows all error messages and line numbers' do expect { subject }.to(output(Regexp.new(Regexp.escape(<<~EOF))).to_stdout) + fake message from a fake linter In file: /app/views/template.html.erb:1 Missing a trailing newline at the end of the file. In file: /app/views/template.html.erb:1 + EOF end @@ -258,6 +269,49 @@ def run(_processed_source) end end + context 'with --format compact' do + let(:args) do + [ + '--enable-linter', 'linter_with_errors,final_newline', + '--format', 'compact', + linted_dir + ] + end + + it 'shows all error messages and line numbers' do + expect { subject }.to(output(Regexp.new(Regexp.escape(<<~EOF))).to_stdout) + /app/views/template.html.erb:1:1: fake message from a fake linter + /app/views/template.html.erb:1:19: Missing a trailing newline at the end of the file. + EOF + end + + it 'is not successful' do + expect(subject).to(be(false)) + end + end + + context 'with invalid --format option' do + let(:args) do + [ + '--enable-linter', 'linter_with_errors,final_newline', + '--format', 'nonexistentformat', + linted_dir + ] + end + + it 'shows all error messages and line numbers' do + expect { subject }.to(output(Regexp.new(Regexp.escape(<<~EOF.strip))).to_stderr) + nonexistentformat: is not a valid format. Available formats: + - compact + - multiline + EOF + end + + it 'is not successful' do + expect(subject).to(be(false)) + end + end + context 'when no errors are found' do let(:args) { ['--enable-linter', 'linter_without_errors', linted_dir] } diff --git a/spec/erb_lint/reporters/compact_reporter_spec.rb b/spec/erb_lint/reporters/compact_reporter_spec.rb new file mode 100644 index 00000000..d51092cf --- /dev/null +++ b/spec/erb_lint/reporters/compact_reporter_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ERBLint::Reporters::CompactReporter do + describe '.show' do + subject { described_class.new(stats, false).show } + + let(:stats) do + ERBLint::Stats.new( + found: 4, + processed_files: { + 'app/views/users/show.html.erb' => show_file_offenses, + 'app/views/shared/_notifications.html.erb' => notification_file_offenses, + } + ) + end + + let(:show_file_offenses) do + [ + instance_double(ERBLint::Offense, + message: 'Extra space detected where there should be no space.', + line_number: 61, + column: 10), + instance_double(ERBLint::Offense, + message: 'Remove multiple trailing newline at the end of the file.', + line_number: 125, + column: 1), + ] + end + + let(:notification_file_offenses) do + [ + instance_double(ERBLint::Offense, + message: 'Indent with spaces instead of tabs.', + line_number: 3, + column: 1), + instance_double(ERBLint::Offense, + message: 'Extra space detected where there should be no space.', + line_number: 7, + column: 1), + ] + end + + it "displays formatted offenses output" do + expect { subject }.to(output(<<~MESSAGE).to_stdout) + app/views/users/show.html.erb:61:10: Extra space detected where there should be no space. + app/views/users/show.html.erb:125:1: Remove multiple trailing newline at the end of the file. + app/views/shared/_notifications.html.erb:3:1: Indent with spaces instead of tabs. + app/views/shared/_notifications.html.erb:7:1: Extra space detected where there should be no space. + MESSAGE + end + end +end diff --git a/spec/erb_lint/reporters/multiline_reporter_spec.rb b/spec/erb_lint/reporters/multiline_reporter_spec.rb new file mode 100644 index 00000000..f783021d --- /dev/null +++ b/spec/erb_lint/reporters/multiline_reporter_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ERBLint::Reporters::MultilineReporter do + describe '.show' do + subject { described_class.new(stats, autocorrect).show } + + let(:stats) do + ERBLint::Stats.new( + found: 2, + processed_files: { + 'app/views/subscriptions/_loader.html.erb' => offenses, + } + ) + end + + let(:offenses) do + [ + instance_double(ERBLint::Offense, + message: 'Extra space detected where there should be no space.', + line_number: 1, + column: 7), + instance_double(ERBLint::Offense, + message: 'Remove newline before `%>` to match start of tag.', + line_number: 52, + column: 10), + ] + end + + context 'when autocorrect is false' do + let(:autocorrect) { false } + + it 'displays formatted offenses output' do + expect { subject }.to(output(<<~MESSAGE).to_stdout) + + Extra space detected where there should be no space. + In file: app/views/subscriptions/_loader.html.erb:1 + + Remove newline before `%>` to match start of tag. + In file: app/views/subscriptions/_loader.html.erb:52 + + MESSAGE + end + end + + context 'when autocorrect is true' do + let(:autocorrect) { true } + + it 'displays not autocorrected warning' do + expect { subject }.to(output(/(not autocorrected)/).to_stdout) + end + end + end +end