From 4546b90b546cfb74cd3556aea42e4f724b545d94 Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Fri, 11 May 2018 21:09:15 +0300 Subject: [PATCH] Add JSON formatter --- spec/ameba/formatter/json_formatter_spec.cr | 78 ++++++++++++ src/ameba/config.cr | 1 + src/ameba/formatter/json_formatter.cr | 133 ++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 spec/ameba/formatter/json_formatter_spec.cr create mode 100644 src/ameba/formatter/json_formatter.cr diff --git a/spec/ameba/formatter/json_formatter_spec.cr b/spec/ameba/formatter/json_formatter_spec.cr new file mode 100644 index 00000000..b35d1cbc --- /dev/null +++ b/spec/ameba/formatter/json_formatter_spec.cr @@ -0,0 +1,78 @@ +require "../../spec_helper" + +module Ameba + def get_result(sources = [Source.new ""]) + file = IO::Memory.new + formatter = Formatter::JSONFormatter.new file + + formatter.started sources + sources.each { |source| formatter.source_finished source } + formatter.finished sources + + JSON.parse file.to_s + end + + describe Formatter::JSONFormatter do + context "metadata" do + it "shows ameba version" do + get_result["metadata"]["ameba_version"].should eq Ameba::VERSION + end + + it "shows crystal version" do + get_result["metadata"]["crystal_version"].should eq Crystal::VERSION + end + end + + context "sources" do + it "shows path to the source" do + result = get_result [Source.new "", "source.cr"] + result["sources"].first["path"].should eq "source.cr" + end + + it "shows rule name" do + s = Source.new "" + s.error DummyRule.new, 1, 2, "message1" + + result = get_result [s] + result["sources"].first["errors"].first["rule_name"].should eq DummyRule.name + end + + it "shows a message" do + s = Source.new "" + s.error DummyRule.new, 1, 2, "message" + + result = get_result [s] + result["sources"].first["errors"].first["message"].should eq "message" + end + + it "shows error location" do + s = Source.new "" + s.error DummyRule.new, 1, 2, "message" + + result = get_result [s] + location = result["sources"].first["errors"].first["location"] + location["line"].should eq 1 + location["column"].should eq 2 + end + end + + context "summary" do + it "shows a target sources count" do + result = get_result [Source.new(""), Source.new("")] + result["summary"]["target_sources_count"].should eq 2 + end + + it "shows errors count" do + s1 = Source.new "" + s1.error DummyRule.new, 1, 2, "message1" + s1.error DummyRule.new, 1, 2, "message2" + + s2 = Source.new "" + s2.error DummyRule.new, 1, 2, "message3" + + result = get_result [s1, s2] + result["summary"]["errors_count"].should eq 3 + end + end + end +end diff --git a/src/ameba/config.cr b/src/ameba/config.cr index 7e7a8ae8..5aaed4c7 100644 --- a/src/ameba/config.cr +++ b/src/ameba/config.cr @@ -18,6 +18,7 @@ class Ameba::Config flycheck: Formatter::FlycheckFormatter, silent: Formatter::BaseFormatter, disabled: Formatter::DisabledFormatter, + json: Formatter::JSONFormatter, } PATH = ".ameba.yml" diff --git a/src/ameba/formatter/json_formatter.cr b/src/ameba/formatter/json_formatter.cr new file mode 100644 index 00000000..a1b8e8a6 --- /dev/null +++ b/src/ameba/formatter/json_formatter.cr @@ -0,0 +1,133 @@ +require "json" + +module Ameba::Formatter + # A formatter that produces the result in a json format. + # + # Example: + # + # ``` + # { + # "metadata": { + # "ameba_version": "x.x.x", + # "crystal_version": "x.x.x", + # }, + # "sources": [ + # { + # "errors": [ + # { + # "location": { + # "column": 7, + # "line": 17, + # }, + # "message": "Useless assignment to variable `a`", + # "rule_name": "UselessAssign", + # }, + # { + # "location": { + # "column": 7, + # "line": 18, + # }, + # "message": "Useless assignment to variable `a`", + # "rule_name": "UselessAssign", + # }, + # { + # "location": { + # "column": 7, + # "line": 19, + # }, + # "message": "Useless assignment to variable `a`", + # "rule_name": "UselessAssign", + # }, + # ], + # "path": "src/ameba/formatter/json_formatter.cr", + # }, + # ], + # "summary": { + # "errors_count": 3, + # "target_sources_count": 1, + # }, + # } + # ``` + # + class JSONFormatter < BaseFormatter + def initialize(@output = STDOUT) + @result = AsJSON::Result.new + end + + def started(sources) + @result.summary.target_sources_count = sources.size + end + + def source_finished(source : Source) + json_source = AsJSON::Source.new source.path + + source.errors.each do |e| + next if e.disabled? + json_source.errors << AsJSON::Error.new(e.rule.name, e.location, e.message) + @result.summary.errors_count += 1 + end + + @result.sources << json_source + end + + def finished(sources) + @result.to_json @output + end + end + + private module AsJSON + record Result, + sources = [] of Source, + metadata = Metadata.new, + summary = Summary.new do + def to_json(json) + {sources: sources, metadata: metadata, summary: summary}.to_json(json) + end + end + + record Source, + path : String, + errors = [] of Error do + def to_json(json) + {path: path, errors: errors}.to_json(json) + end + end + + record Error, + rule_name : String, + location : Crystal::Location?, + message : String do + def to_json(json) + json.object do + json.field :rule_name, rule_name + json.field :message, message + json.field :location, + {line: location.try &.line_number, column: location.try &.column_number} + end + end + end + + record Metadata, + ameba_version : String = Ameba::VERSION, + crystal_version : String = Crystal::VERSION do + def to_json(json) + json.object do + json.field :ameba_version, ameba_version + json.field :crystal_version, crystal_version + end + end + end + + class Summary + property target_sources_count = 0 + property errors_count = 0 + + def to_json(json) + json.object do + json.field :target_sources_count, target_sources_count + json.field :errors_count, errors_count + end + end + end + end +end