diff --git a/src/formatters/index.ts b/src/formatters/index.ts
index ba78ad3171b..25655d328a2 100644
--- a/src/formatters/index.ts
+++ b/src/formatters/index.ts
@@ -23,3 +23,4 @@ export { Formatter as StylishFormatter } from "./stylishFormatter";
export { Formatter as FileslistFormatter } from "./fileslistFormatter";
export { Formatter as CodeFrameFormatter } from "./codeFrameFormatter";
export { Formatter as TapFormatter } from "./tapFormatter";
+export { Formatter as JUnitFormatter } from "./junitFormatter";
diff --git a/src/formatters/junitFormatter.ts b/src/formatters/junitFormatter.ts
new file mode 100644
index 00000000000..72e23c1161f
--- /dev/null
+++ b/src/formatters/junitFormatter.ts
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * Copyright 2016 Palantir Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AbstractFormatter } from "../language/formatter/abstractFormatter";
+import { IFormatterMetadata } from "../language/formatter/formatter";
+import { RuleFailure } from "../language/rule/rule";
+
+import * as Utils from "../utils";
+
+export class Formatter extends AbstractFormatter {
+ /* tslint:disable:object-literal-sort-keys */
+ public static metadata: IFormatterMetadata = {
+ formatterName: "junit",
+ description: "Formats errors as through they were JUnit output.",
+ descriptionDetails: Utils.dedent`
+ Imitates the JUnit XML Output`,
+ sample: Utils.dedent`
+
+
+
+
+ Missing semicolon
+
+
+
+ `,
+ consumer: "machine",
+ };
+ /* tslint:enable:object-literal-sort-keys */
+
+ public format(failures: RuleFailure[]): string {
+ let output = '';
+
+ if (failures.length !== 0) {
+ const failuresSorted = failures.sort(
+ (a, b) => a.getFileName().localeCompare(b.getFileName()));
+ let previousFilename: string | null = null;
+ for (const failure of failuresSorted) {
+ const lineAndCharacter = failure.getStartPosition().getLineAndCharacter();
+ const message = this.escapeXml(failure.getFailure());
+ const rule = this.escapeXml(failure.getRuleName());
+ const severity = failure.getRuleSeverity();
+
+ if (failure.getFileName() !== previousFilename) {
+ if (previousFilename !== null) {
+ output += "";
+ }
+ previousFilename = failure.getFileName();
+ output += ``;
+ }
+
+ output += ``;
+ output += `${message}`;
+ output += "";
+ }
+ if (previousFilename !== null) {
+ output += "";
+ }
+ }
+
+ output += "";
+ return output;
+ }
+
+ private escapeXml(str: string): string {
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/'/g, "'")
+ .replace(/"/g, """);
+ }
+}
diff --git a/test/formatters/junitFormatterTests.ts b/test/formatters/junitFormatterTests.ts
new file mode 100644
index 00000000000..4ab129519d3
--- /dev/null
+++ b/test/formatters/junitFormatterTests.ts
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright 2016 Palantir Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { assert } from "chai";
+import * as ts from "typescript";
+
+import { IFormatter, TestUtils } from "../lint";
+import { createFailure } from "./utils";
+
+describe("JUnit Formatter", () => {
+ const TEST_FILE_1 = "formatters/jsonFormatter.test.ts"; // reuse existing sample file
+ const TEST_FILE_2 = "formatters/pmdFormatter.test.ts"; // reuse existing sample file
+ let sourceFile1: ts.SourceFile;
+ let sourceFile2: ts.SourceFile;
+ let formatter: IFormatter;
+
+ before(() => {
+ const Formatter = TestUtils.getFormatter("junit");
+ sourceFile1 = TestUtils.getSourceFile(TEST_FILE_1);
+ sourceFile2 = TestUtils.getSourceFile(TEST_FILE_2);
+ formatter = new Formatter();
+ });
+
+ it("formats failures", () => {
+ const maxPosition1 = sourceFile1.getFullWidth();
+ const maxPosition2 = sourceFile2.getFullWidth();
+
+ const failures = [
+ createFailure(sourceFile1, 0, 1, "first failure", "first-name", undefined, "error"),
+ createFailure(sourceFile1, 2, 3, "&<>'\" should be escaped", "escape", undefined, "error"),
+ createFailure(sourceFile1, maxPosition1 - 1, maxPosition1, "last failure", "last-name", undefined, "error"),
+ createFailure(sourceFile2, 0, 1, "first failure", "first-name", undefined, "error"),
+ createFailure(sourceFile2, 2, 3, "&<>'\" should be escaped", "escape", undefined, "warning"),
+ createFailure(sourceFile2, maxPosition2 - 1, maxPosition2, "last failure", "last-name", undefined, "warning"),
+ ];
+
+ const expectedResult =
+ `
+
+
+
+ first failure
+
+
+ &<>'" should be escaped
+
+
+ last failure
+
+
+
+
+ first failure
+
+
+ &<>'" should be escaped
+
+
+ last failure
+
+
+ `.replace(/>\s+/g, ">"); // Remove whitespace between tags;
+
+ assert.equal(formatter.format(failures), expectedResult);
+ });
+
+ it("handles no failures", () => {
+ const result = formatter.format([]);
+ assert.deepEqual(result, '');
+ });
+});