diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c1cdcd173..5a3557f411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ #### Enhancements +* Add custom markdown reporter + Markdown reporter (formatted with tables for Gitlab and Github) + [Dani Vela](https://github.com/madcato) + * Add `SWIFTLINT_DISABLE_SOURCEKIT` environment variable to allow running SwiftLint without connecting to SourceKit. This will run a subset of rules that don't require SourceKit, which is useful when running in a sandboxed diff --git a/README.md b/README.md index d1e1b0f3c7..172b1c218d 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ identifier_name: - id - URL - GlobalAPIKey -reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube) +reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown) ``` You can also use environment variables in your configuration file, diff --git a/README_KR.md b/README_KR.md index ba3ae1742d..568d200905 100644 --- a/README_KR.md +++ b/README_KR.md @@ -258,7 +258,7 @@ identifier_name: - id - URL - GlobalAPIKey -reporter: "xcode" # 보고 유형 (xcode, json, csv, checkstyle, junit, html, emoji) +reporter: "xcode" # 보고 유형 (xcode, json, csv, checkstyle, junit, html, emoji, markdown) ``` #### 커스텀 룰 정의 diff --git a/Source/SwiftLintFramework/Protocols/Reporter.swift b/Source/SwiftLintFramework/Protocols/Reporter.swift index ec6fd49ccf..03c7635f2b 100644 --- a/Source/SwiftLintFramework/Protocols/Reporter.swift +++ b/Source/SwiftLintFramework/Protocols/Reporter.swift @@ -23,6 +23,8 @@ public func reporterFrom(identifier: String) -> Reporter.Type { return EmojiReporter.self case SonarQubeReporter.identifier: return SonarQubeReporter.self + case MarkdownReporter.identifier: + return MarkdownReporter.self default: queuedFatalError("no reporter with identifier '\(identifier)' available.") } diff --git a/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift b/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift new file mode 100644 index 0000000000..7f984034d3 --- /dev/null +++ b/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift @@ -0,0 +1,52 @@ +import Foundation + +private extension String { + func escapedForMarkdown() -> String { + let escapedString = replacingOccurrences(of: "\"", with: "\"\"") + if escapedString.contains("|") || escapedString.contains("\n") { + return "\"\(escapedString)\"" + } + return escapedString + } +} + +public struct MarkdownReporter: Reporter { + public static let identifier = "markdown" + public static let isRealtime = false + + public var description: String { + return "Reports violations as markdown formated (with tables)" + } + + public static func generateReport(_ violations: [StyleViolation]) -> String { + let keys = [ + "file", + "line", + "severity", + "reason", + "rule_id" + ].joined(separator: " | ") + + let rows = [keys, "--- | --- | --- | --- | ---"] + violations.map(markdownRow(for:)) + return rows.joined(separator: "\n") + } + + fileprivate static func markdownRow(for violation: StyleViolation) -> String { + return [ + violation.location.file?.escapedForMarkdown() ?? "", + violation.location.line?.description ?? "", + severity(for: violation.severity), + violation.ruleDescription.name.escapedForMarkdown() + ": " + violation.reason.escapedForMarkdown(), + violation.ruleDescription.identifier + ].joined(separator: " | ") + } + + fileprivate static func severity(for severity: ViolationSeverity) -> String { + switch severity { + case .error: + return ":stop\\_sign:" + case .warning: + return ":warning:" + } + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 96411c00fb..d8941cb652 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ 2E336D1B1DF08BFB00CCFE77 /* EmojiReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E336D191DF08AF200CCFE77 /* EmojiReporter.swift */; }; 2E5761AA1C573B83003271AF /* FunctionParameterCountRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5761A91C573B83003271AF /* FunctionParameterCountRule.swift */; }; 31F1B6CC1F60BF4500A57456 /* SwitchCaseAlignmentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F1B6CB1F60BF4500A57456 /* SwitchCaseAlignmentRule.swift */; }; + 341FDB1F21AD66550022E8E9 /* CannedMarkdownReporterOutput.md in Resources */ = {isa = PBXBuildFile; fileRef = 341FDB1E21AD66550022E8E9 /* CannedMarkdownReporterOutput.md */; }; + 341FDB2021AD69970022E8E9 /* MarkdownReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 341FDB1C21AD61B20022E8E9 /* MarkdownReporter.swift */; }; 37B3FA8B1DFD45A700AD30D2 /* Dictionary+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B3FA8A1DFD45A700AD30D2 /* Dictionary+SwiftLint.swift */; }; 3A915E5B20A1543700519F3A /* ClosureEndIndentationRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A915E5920A1543000519F3A /* ClosureEndIndentationRuleExamples.swift */; }; 3ABE19CF20B7CE32009C2EC2 /* MultilineFunctionChainsRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABE19CD20B7CDE0009C2EC2 /* MultilineFunctionChainsRule.swift */; }; @@ -459,6 +461,8 @@ 2E336D191DF08AF200CCFE77 /* EmojiReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiReporter.swift; sourceTree = ""; }; 2E5761A91C573B83003271AF /* FunctionParameterCountRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionParameterCountRule.swift; sourceTree = ""; }; 31F1B6CB1F60BF4500A57456 /* SwitchCaseAlignmentRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCaseAlignmentRule.swift; sourceTree = ""; }; + 341FDB1C21AD61B20022E8E9 /* MarkdownReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownReporter.swift; sourceTree = ""; }; + 341FDB1E21AD66550022E8E9 /* CannedMarkdownReporterOutput.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CannedMarkdownReporterOutput.md; sourceTree = ""; }; 37B3FA8A1DFD45A700AD30D2 /* Dictionary+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+SwiftLint.swift"; sourceTree = ""; }; 3A915E5920A1543000519F3A /* ClosureEndIndentationRuleExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureEndIndentationRuleExamples.swift; sourceTree = ""; }; 3ABE19CD20B7CDE0009C2EC2 /* MultilineFunctionChainsRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineFunctionChainsRule.swift; sourceTree = ""; }; @@ -867,6 +871,7 @@ B3935001033261E5A70CE101 /* CannedEmojiReporterOutput.txt */, B39350463894A3FC1338E0AF /* CannedJSONReporterOutput.json */, 584B0D3B2112E8FB002F7E25 /* CannedSonarQubeReporterOutput.json */, + 341FDB1E21AD66550022E8E9 /* CannedMarkdownReporterOutput.md */, ); path = Resources; sourceTree = ""; @@ -1396,6 +1401,7 @@ 4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */, E86396C81BADB2B9002C9E88 /* JSONReporter.swift */, 57ED82791CF65183002B3513 /* JUnitReporter.swift */, + 341FDB1C21AD61B20022E8E9 /* MarkdownReporter.swift */, E86396C41BADAC15002C9E88 /* XcodeReporter.swift */, 584B0D392112BA78002F7E25 /* SonarQubeReporter.swift */, ); @@ -1620,6 +1626,7 @@ files = ( 3B12C9C11C3209CB000B423F /* test.yml in Resources */, F9D73F031D0CF15E00222FC4 /* test.txt in Resources */, + 341FDB1F21AD66550022E8E9 /* CannedMarkdownReporterOutput.md in Resources */, 3BDB224B1C345B4900473680 /* ProjectMock in Resources */, B3935797FF80C7F97953D375 /* CannedHTMLReporterOutput.html in Resources */, B3935371E92E0CF3F7668303 /* CannedJunitReporterOutput.xml in Resources */, @@ -1782,6 +1789,7 @@ D4E92D1F2137B4C9002EDD48 /* IdenticalOperandsRule.swift in Sources */, 6250D32A1ED4DFEB00735129 /* MultilineParametersRule.swift in Sources */, 009E092A1DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift in Sources */, + 341FDB2021AD69970022E8E9 /* MarkdownReporter.swift in Sources */, 181D9E172038343D001F6887 /* UntypedErrorInCatchRule.swift in Sources */, 47FF3BE11E7C75B600187E6D /* ImplicitlyUnwrappedOptionalRule.swift in Sources */, 623E36F01F3DB1B1002E5B71 /* QuickDiscouragedCallRule.swift in Sources */, diff --git a/SwiftLint.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SwiftLint.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/SwiftLint.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 5cb52ef72b..f6b59a41e0 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -963,7 +963,8 @@ extension ReporterTests { ("testCheckstyleReporter", testCheckstyleReporter), ("testJunitReporter", testJunitReporter), ("testHTMLReporter", testHTMLReporter), - ("testSonarQubeReporter", testSonarQubeReporter) + ("testSonarQubeReporter", testSonarQubeReporter), + ("testMarkdownReporter", testMarkdownReporter) ] } diff --git a/Tests/SwiftLintFrameworkTests/ReporterTests.swift b/Tests/SwiftLintFrameworkTests/ReporterTests.swift index 4ec81d6c8d..69918738d5 100644 --- a/Tests/SwiftLintFrameworkTests/ReporterTests.swift +++ b/Tests/SwiftLintFrameworkTests/ReporterTests.swift @@ -14,7 +14,8 @@ class ReporterTests: XCTestCase { JUnitReporter.self, HTMLReporter.self, EmojiReporter.self, - SonarQubeReporter.self + SonarQubeReporter.self, + MarkdownReporter.self ] for reporter in reporters { XCTAssertEqual(reporter.identifier, reporterFrom(identifier: reporter.identifier).identifier) @@ -109,4 +110,10 @@ class ReporterTests: XCTestCase { let result = SonarQubeReporter.generateReport(generateViolations()) XCTAssertEqual(try jsonValue(result), try jsonValue(expectedOutput)) } + + func testMarkdownReporter() { + let expectedOutput = stringFromFile("CannedMarkdownReporterOutput.md") + let result = MarkdownReporter.generateReport(generateViolations()) + XCTAssertEqual(result, expectedOutput) + } } diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md b/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md new file mode 100644 index 0000000000..3401c8b608 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md @@ -0,0 +1,6 @@ +file | line | severity | reason | rule_id +--- | --- | --- | --- | --- +filename | 1 | :warning: | Line Length: Violation Reason. | line_length +filename | 1 | :stop\_sign: | Line Length: Violation Reason. | line_length +filename | 1 | :stop\_sign: | Syntactic Sugar: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array. | syntactic_sugar + | | :stop\_sign: | Colon: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. | colon \ No newline at end of file