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, ''); + }); +});