diff --git a/spec/ameba/formatter/github_actions_formatter_spec.cr b/spec/ameba/formatter/github_actions_formatter_spec.cr new file mode 100644 index 000000000..12988d606 --- /dev/null +++ b/spec/ameba/formatter/github_actions_formatter_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +module Ameba::Formatter + describe GitHubActionsFormatter do + describe "#source_finished" do + it "writes valid source" do + output = IO::Memory.new + subject = GitHubActionsFormatter.new(output) + + source = Source.new "", "/path/to/file.cr" + + subject.source_finished(source) + output.to_s.should be_empty + end + + it "writes invalid source" do + output = IO::Memory.new + subject = GitHubActionsFormatter.new(output) + + source = Source.new "", "/path/to/file.cr" + location = Crystal::Location.new("/path/to/file.cr", 1, 2) + + source.add_issue DummyRule.new, location, location, "message\n2nd line" + + subject.source_finished(source) + output.to_s.should eq("::notice file=/path/to/file.cr,line=1,col=2,endLine=1,endColumn=2,title=Ameba/DummyRule::message%0A2nd line\n") + end + end + end +end diff --git a/src/ameba/config.cr b/src/ameba/config.cr index 2c0b84f8e..89a710e53 100644 --- a/src/ameba/config.cr +++ b/src/ameba/config.cr @@ -39,12 +39,13 @@ class Ameba::Config include GlobUtils AVAILABLE_FORMATTERS = { - progress: Formatter::DotFormatter, - todo: Formatter::TODOFormatter, - flycheck: Formatter::FlycheckFormatter, - silent: Formatter::BaseFormatter, - disabled: Formatter::DisabledFormatter, - json: Formatter::JSONFormatter, + progress: Formatter::DotFormatter, + todo: Formatter::TODOFormatter, + flycheck: Formatter::FlycheckFormatter, + silent: Formatter::BaseFormatter, + disabled: Formatter::DisabledFormatter, + json: Formatter::JSONFormatter, + "github-actions": Formatter::GitHubActionsFormatter, } XDG_CONFIG_HOME = ENV.fetch("XDG_CONFIG_HOME", "~/.config") diff --git a/src/ameba/formatter/github_actions_formatter.cr b/src/ameba/formatter/github_actions_formatter.cr new file mode 100644 index 000000000..dac1a9df4 --- /dev/null +++ b/src/ameba/formatter/github_actions_formatter.cr @@ -0,0 +1,69 @@ +module Ameba::Formatter + # A formatter that outputs issues in a GitHub Actions compatible format. + # + # See [GitHub Actions documentation](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions) for details. + class GitHubActionsFormatter < BaseFormatter + @mutex = Mutex.new + + # Reports a result of the inspection of a corresponding source. + def source_finished(source : Source) : Nil + source.issues.each do |issue| + next if issue.disabled? + + @mutex.synchronize do + output << "::" + output << command_name(issue.rule.severity) + output << " " + output << "file=" + output << escape_property(source.path) + if location = issue.location + output << ",line=" + output << location.line_number + output << ",col=" + output << location.column_number + end + if end_location = issue.end_location + output << ",endLine=" + output << end_location.line_number + output << ",endColumn=" + output << end_location.column_number + end + output << ",title=" + output << escape_property(issue.rule.name) + output << "::" + output << escape_data(issue.message) + output << "\n" + end + end + end + + private def command_name(severity : Severity) : String + case severity + in .error? then "error" + in .warning? then "warning" + in .convention? then "notice" + end + end + + # See for details: + # - https://github.com/actions/toolkit/blob/74906bea83a0dbf6aaba2d00b732deb0c3aefd2d/packages/core/src/command.ts#L92-L97 + # - https://github.com/actions/toolkit/issues/193 + private def escape_data(string : String) : String + string + .gsub('%', "%25") + .gsub('\r', "%0D") + .gsub('\n', "%0A") + end + + # See for details: + # - https://github.com/actions/toolkit/blob/74906bea83a0dbf6aaba2d00b732deb0c3aefd2d/packages/core/src/command.ts#L99-L106 + private def escape_property(string : String) : String + string + .gsub('%', "%25") + .gsub('\r', "%0D") + .gsub('\n', "%0A") + .gsub(':', "%3A") + .gsub(',', "%2C") + end + end +end