diff --git a/spec/ameba/presenter/rule_collection_presenter_spec.cr b/spec/ameba/presenter/rule_collection_presenter_spec.cr new file mode 100644 index 00000000..cba4129b --- /dev/null +++ b/spec/ameba/presenter/rule_collection_presenter_spec.cr @@ -0,0 +1,32 @@ +require "../../spec_helper" + +module Ameba + private def with_rule_collection_presenter(&) + with_presenter(Presenter::RuleCollectionPresenter) do |presenter, io| + rules = Config.load.rules + presenter.run(rules) + + output = io.to_s + output = Formatter::Util.deansify(output).to_s + + yield rules, output, presenter + end + end + + describe Presenter::RuleCollectionPresenter do + it "outputs rule collection details" do + with_rule_collection_presenter do |rules, output| + rules.each do |rule| + output.should contain rule.name + output.should contain rule.severity.symbol + + if description = rule.description + output.should contain description + end + end + output.should contain "Total rules: #{rules.size}" + output.should match /\d+ enabled/ + end + end + end +end diff --git a/spec/ameba/presenter/rule_presenter_spec.cr b/spec/ameba/presenter/rule_presenter_spec.cr new file mode 100644 index 00000000..5f76007a --- /dev/null +++ b/spec/ameba/presenter/rule_presenter_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +module Ameba + private def rule_presenter_each_rule(&) + with_presenter(Presenter::RulePresenter) do |presenter, io| + rules = Config.load.rules + rules.each do |rule| + presenter.run(rule) + + output = io.to_s + output = Formatter::Util.deansify(output).to_s + + yield rule, output, presenter + end + end + end + + describe Presenter::RulePresenter do + it "outputs rule details" do + rule_presenter_each_rule do |rule, output| + output.should contain rule.name + output.should contain rule.severity.to_s + + if description = rule.description + output.should contain description + end + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 365febd1..34621852 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -282,6 +282,13 @@ module Ameba end end +def with_presenter(klass, &) + io = IO::Memory.new + presenter = klass.new(io) + + yield presenter, io +end + def as_node(source) Crystal::Parser.new(source).parse end diff --git a/src/ameba.cr b/src/ameba.cr index d2eeffc1..e675939a 100644 --- a/src/ameba.cr +++ b/src/ameba.cr @@ -3,6 +3,7 @@ require "./ameba/ast/**" require "./ameba/ext/**" require "./ameba/rule/**" require "./ameba/formatter/*" +require "./ameba/presenter/*" require "./ameba/source/**" # Ameba's entry module. diff --git a/src/ameba/cli/cmd.cr b/src/ameba/cli/cmd.cr index d8ce4a7e..e9a0ffbd 100644 --- a/src/ameba/cli/cmd.cr +++ b/src/ameba/cli/cmd.cr @@ -28,7 +28,14 @@ module Ameba::Cli configure_rules(config, opts) if opts.rules? - print_rules(config) + print_rules(config.rules) + end + + if describe_rule_name = opts.describe_rule + unless rule = config.rules.find(&.name.== describe_rule_name) + raise "Unknown rule" + end + describe_rule(rule) end runner = Ameba.run(config) @@ -49,6 +56,7 @@ module Ameba::Cli property globs : Array(String)? property only : Array(String)? property except : Array(String)? + property describe_rule : String? property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)? property fail_level : Severity? property? skip_reading_config = false @@ -119,6 +127,11 @@ module Ameba::Cli configure_explain_opts(loc, opts) end + parser.on("-d", "--describe Category/Rule", + "Describe a rule with specified name") do |rule_name| + configure_describe_opts(rule_name, opts) + end + parser.on("--without-affected-code", "Stop showing affected code while using a default formatter") do opts.without_affected_code = true @@ -152,6 +165,11 @@ module Ameba::Cli opts.without_affected_code? end + private def configure_describe_opts(rule_name, opts) + opts.describe_rule = rule_name.presence + opts.formatter = :silent + end + private def configure_explain_opts(loc, opts) location_to_explain = parse_explain_location(loc) opts.location_to_explain = location_to_explain @@ -183,14 +201,13 @@ module Ameba::Cli exit 0 end - private def print_rules(config) - config.rules.each do |rule| - puts "%s [%s] - %s" % { - rule.name.colorize(:white), - rule.severity.symbol.to_s.colorize(:green), - rule.description.colorize(:dark_gray), - } - end + private def describe_rule(rule) + Presenter::RulePresenter.new.run(rule) + exit 0 + end + + private def print_rules(rules) + Presenter::RuleCollectionPresenter.new.run(rules) exit 0 end end diff --git a/src/ameba/formatter/explain_formatter.cr b/src/ameba/formatter/explain_formatter.cr index eb3b79ee..e963d98f 100644 --- a/src/ameba/formatter/explain_formatter.cr +++ b/src/ameba/formatter/explain_formatter.cr @@ -4,8 +4,6 @@ module Ameba::Formatter # A formatter that shows the detailed explanation of the issue at # a specific location. class ExplainFormatter - HEADING_MARKER = "## " - include Util getter output : IO::FileDescriptor | IO::Memory @@ -64,9 +62,8 @@ module Ameba::Formatter rule.name.colorize(:magenta), rule.severity.to_s.colorize(rule.severity.color), } - - if rule.responds_to?(:description) - output_paragraph rule.description + if rule_description = colorize_code_fences(rule.description) + output_paragraph rule_description end rule_doc = colorize_code_fences(rule.class.parsed_doc) @@ -84,7 +81,7 @@ module Ameba::Formatter end private def output_title(title) - output << HEADING_MARKER.colorize(:yellow) + output << "### ".colorize(:yellow) output << title.upcase.colorize(:yellow) output << "\n\n" end @@ -95,7 +92,7 @@ module Ameba::Formatter private def output_paragraph(paragraph : Array) paragraph.each do |line| - output << ' ' << line << '\n' + output << " " << line << '\n' end output << '\n' end diff --git a/src/ameba/formatter/util.cr b/src/ameba/formatter/util.cr index fb3f226e..da8554a0 100644 --- a/src/ameba/formatter/util.cr +++ b/src/ameba/formatter/util.cr @@ -1,5 +1,7 @@ module Ameba::Formatter module Util + extend self + def deansify(message : String?) : String? message.try &.gsub(/\x1b[^m]*m/, "").presence end diff --git a/src/ameba/presenter/base_presenter.cr b/src/ameba/presenter/base_presenter.cr new file mode 100644 index 00000000..73f9ad00 --- /dev/null +++ b/src/ameba/presenter/base_presenter.cr @@ -0,0 +1,12 @@ +module Ameba::Presenter + private ENABLED_MARK = "✓".colorize(:green) + private DISABLED_MARK = "x".colorize(:red) + + class BasePresenter + # TODO: allow other IOs + getter output : IO::FileDescriptor | IO::Memory + + def initialize(@output = STDOUT) + end + end +end diff --git a/src/ameba/presenter/rule_collection_presenter.cr b/src/ameba/presenter/rule_collection_presenter.cr new file mode 100644 index 00000000..e833df11 --- /dev/null +++ b/src/ameba/presenter/rule_collection_presenter.cr @@ -0,0 +1,34 @@ +module Ameba::Presenter + class RuleCollectionPresenter < BasePresenter + def run(rules) + rules = rules.to_h do |rule| + name = rule.name.split('/') + name = "%s/%s" % { + name[0...-1].join('/').colorize(:light_gray), + name.last.colorize(:white), + } + {name, rule} + end + longest_name = rules.max_of(&.first.size) + + rules.group_by(&.last.group).each do |group, group_rules| + output.puts "— %s" % group.colorize(:light_blue).underline + output.puts + group_rules.each do |name, rule| + output.puts " %s [%s] %s %s" % { + rule.enabled? ? ENABLED_MARK : DISABLED_MARK, + rule.severity.symbol.to_s.colorize(:green), + name.ljust(longest_name), + rule.description.colorize(:dark_gray), + } + end + output.puts + end + + output.puts "Total rules: %s / %s enabled" % { + rules.size.to_s.colorize(:light_blue), + rules.count(&.last.enabled?).to_s.colorize(:light_blue), + } + end + end +end diff --git a/src/ameba/presenter/rule_presenter.cr b/src/ameba/presenter/rule_presenter.cr new file mode 100644 index 00000000..a790ac4b --- /dev/null +++ b/src/ameba/presenter/rule_presenter.cr @@ -0,0 +1,43 @@ +module Ameba::Presenter + class RulePresenter < BasePresenter + def run(rule) + output.puts + output_title "Rule info" + output_paragraph "%s of a %s severity [enabled: %s]" % { + rule.name.colorize(:magenta), + rule.severity.to_s.colorize(rule.severity.color), + rule.enabled? ? ENABLED_MARK : DISABLED_MARK, + } + if rule_description = colorize_code_fences(rule.description) + output_paragraph rule_description + end + + if rule_doc = colorize_code_fences(rule.class.parsed_doc) + output_title "Detailed description" + output_paragraph rule_doc + end + end + + private def output_title(title) + output.print "### %s\n\n" % title.upcase.colorize(:yellow) + end + + private def output_paragraph(paragraph : String) + output_paragraph(paragraph.lines) + end + + private def output_paragraph(paragraph : Array) + paragraph.each do |line| + output.puts " #{line}" + end + output.puts + end + + private def colorize_code_fences(string) + return unless string + string + .gsub(/```(.+?)```/m, &.colorize(:dark_gray)) + .gsub(/`(?!`)(.+?)`/, &.colorize(:dark_gray)) + end + end +end