diff --git a/.rubocop.yml b/.rubocop.yml
index 15d9d64f4ac..564fe88d901 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -95,6 +95,10 @@ Metrics/ModuleLength:
Exclude:
- 'spec/**/*.rb'
+RSpec/FilePath:
+ Exclude:
+ - spec/rubocop/formatter/junit_formatter_spec.rb
+
RSpec/PredicateMatcher:
EnforcedStyle: explicit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a016e5b57e..6bc3a17418b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
* [#7659](https://github.com/rubocop-hq/rubocop/pull/7659): Layout/LineLength autocorrect now breaks up long lines with blocks. ([@maxh][])
* [#7677](https://github.com/rubocop-hq/rubocop/pull/7677): Add a cop for `Hash#each_key` and `Hash#each_value`. ([@jemmaissroff][])
* Add `BracesRequiredMethods` parameter to `Style/BlockDelimiters` to require braces for specific methods such as Sorbet's `sig`. ([@maxh][])
+* [#7686](https://github.com/rubocop-hq/rubocop/pull/7686): Add new `JUnitFormatter` formatter based on rubocop-junit-formatter gem. ([@koic][])
### Bug fixes
diff --git a/Gemfile b/Gemfile
index b6a4041ba96..0551942c2b0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,11 +18,6 @@ gem 'test-queue'
gem 'yard', '~> 0.9'
group :test do
- # Workaround for crack 0.4.3 or lower.
- # Depends on `rexml` until the release that includes
- # the following changes:
- # https://github.com/jnunemaker/crack/pull/62
- gem 'rexml'
gem 'safe_yaml', require: false
gem 'webmock', require: false
end
diff --git a/lib/rubocop.rb b/lib/rubocop.rb
index 53502594be6..a88668a7d9c 100644
--- a/lib/rubocop.rb
+++ b/lib/rubocop.rb
@@ -588,6 +588,7 @@
require_relative 'rubocop/formatter/fuubar_style_formatter'
require_relative 'rubocop/formatter/html_formatter'
require_relative 'rubocop/formatter/json_formatter'
+require_relative 'rubocop/formatter/junit_formatter'
require_relative 'rubocop/formatter/offense_count_formatter'
require_relative 'rubocop/formatter/progress_formatter'
require_relative 'rubocop/formatter/quiet_formatter'
diff --git a/lib/rubocop/formatter/formatter_set.rb b/lib/rubocop/formatter/formatter_set.rb
index 1375beac201..08e83de3357 100644
--- a/lib/rubocop/formatter/formatter_set.rb
+++ b/lib/rubocop/formatter/formatter_set.rb
@@ -17,6 +17,7 @@ class FormatterSet < Array
'[fu]ubar' => FuubarStyleFormatter,
'[h]tml' => HTMLFormatter,
'[j]son' => JSONFormatter,
+ '[ju]nit' => JUnitFormatter,
'[o]ffenses' => OffenseCountFormatter,
'[pa]cman' => PacmanFormatter,
'[p]rogress' => ProgressFormatter,
diff --git a/lib/rubocop/formatter/junit_formatter.rb b/lib/rubocop/formatter/junit_formatter.rb
new file mode 100644
index 00000000000..3bfc5f6567a
--- /dev/null
+++ b/lib/rubocop/formatter/junit_formatter.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'rexml/document'
+
+#
+# This code is based on https://github.com/mikian/rubocop-junit-formatter.
+#
+# Copyright (c) 2015 Mikko Kokkonen
+#
+# MIT License
+#
+# https://github.com/mikian/rubocop-junit-formatter/blob/master/LICENSE.txt
+#
+module RuboCop
+ module Formatter
+ # This formatter formats the report data in JUnit format.
+ class JUnitFormatter < BaseFormatter
+ def initialize(output, options = {})
+ super
+
+ @document = REXML::Document.new.tap do |document|
+ document << REXML::XMLDecl.new
+ end
+ testsuites = REXML::Element.new('testsuites', @document)
+ testsuite = REXML::Element.new('testsuite', testsuites)
+ @testsuite = testsuite.tap do |element|
+ element.add_attributes('name' => 'rubocop')
+ end
+ end
+
+ def file_finished(file, offenses)
+ offenses.group_by(&:cop_name).each do |cop_name, grouped_offenses|
+ REXML::Element.new('testcase', @testsuite).tap do |testcase|
+ testcase.attributes['classname'] = file.gsub(
+ /\.rb\Z/, ''
+ ).gsub("#{Dir.pwd}/", '').tr('/', '.')
+ testcase.attributes['name'] = cop_name
+
+ add_failure_to(testcase, grouped_offenses, cop_name)
+ end
+ end
+ end
+
+ def finished(_inspected_files)
+ @document.write(output, 2)
+ end
+
+ private
+
+ def add_failure_to(testcase, offenses, cop_name)
+ # One failure per offense. Zero failures is a passing test case,
+ # for most surefire/nUnit parsers.
+ offenses.each do |offense|
+ REXML::Element.new('failure', testcase).tap do |failure|
+ failure.attributes['type'] = cop_name
+ failure.attributes['message'] = offense.message
+ failure.add_text(offense.location.to_s)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/manual/formatters.md b/manual/formatters.md
index 433ec96d646..15f77d77a1b 100644
--- a/manual/formatters.md
+++ b/manual/formatters.md
@@ -237,6 +237,37 @@ The JSON structure is like the following example:
}
```
+### JUnit Style Formatter
+
+**Machine-parsable**
+
+The `junit` style formatter provides the JUnit formatting.
+This formatter is based on [rubocop-junit-formatter gem](https://github.com/mikian/rubocop-junit-formatter).
+
+```sh
+$ rubocop --format junit
+
+
+
+
+
+ /tmp/src/example.rb:1:1
+
+
+
+
+ /tmp/src/example.rb:1:5
+
+
+
+
+ /tmp/src/example.rb:2:8
+
+
+
+
+```
+
### Offense Count Formatter
Sometimes when first applying RuboCop to a codebase, it's nice to be able to
diff --git a/rubocop.gemspec b/rubocop.gemspec
index ccd7b4fb98a..ee6aefba430 100644
--- a/rubocop.gemspec
+++ b/rubocop.gemspec
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency('parallel', '~> 1.10')
s.add_runtime_dependency('parser', '>= 2.7.0.1')
s.add_runtime_dependency('rainbow', '>= 2.2.2', '< 4.0')
+ s.add_runtime_dependency('rexml')
s.add_runtime_dependency('ruby-progressbar', '~> 1.7')
s.add_runtime_dependency('unicode-display_width', '>= 1.4.0', '< 1.7')
diff --git a/spec/rubocop/formatter/junit_formatter_spec.rb b/spec/rubocop/formatter/junit_formatter_spec.rb
new file mode 100644
index 00000000000..4057a5604cc
--- /dev/null
+++ b/spec/rubocop/formatter/junit_formatter_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.describe RuboCop::Formatter::JUnitFormatter do
+ subject(:formatter) { described_class.new(output) }
+
+ let(:output) { StringIO.new }
+
+ describe '#file_finished' do
+ it 'displays parsable text' do
+ cop = RuboCop::Cop::Cop.new
+ source_buffer = Parser::Source::Buffer.new('test', 1)
+ source_buffer.source = %w[foo bar baz].join("\n")
+
+ cop.add_offense(
+ nil,
+ location: Parser::Source::Range.new(source_buffer, 0, 1),
+ message: 'message 1'
+ )
+ cop.add_offense(
+ nil,
+ location: Parser::Source::Range.new(source_buffer, 9, 10),
+ message: 'message 2'
+ )
+
+ formatter.file_finished('test_1', cop.offenses)
+ formatter.file_finished('test_2', cop.offenses)
+
+ formatter.finished(nil)
+
+ expect(output.string).to eq(<<~XML.chop)
+
+
+
+
+
+ test:1:1
+
+
+ test:3:2
+
+
+
+
+ test:1:1
+
+
+ test:3:2
+
+
+
+
+ XML
+ end
+ end
+end
diff --git a/spec/rubocop/options_spec.rb b/spec/rubocop/options_spec.rb
index dcdb544e233..505367cf633 100644
--- a/spec/rubocop/options_spec.rb
+++ b/spec/rubocop/options_spec.rb
@@ -77,6 +77,7 @@ def abs(path)
[fu]ubar
[h]tml
[j]son
+ [ju]nit
[o]ffenses
[pa]cman
[p]rogress