diff --git a/CHANGELOG.md b/CHANGELOG.md index d1411fde7d..87d1abf378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,15 @@ [Denis Lebedev](https://github.com/garnett) [#544](https://github.com/realm/SwiftLint/issues/544) +* Add `EmojiReporter`: a human friendly reporter. + [Michał Kałużny](https://github.com/justMaku) + ##### Bug Fixes * Fix `weak_delegate` rule reporting a violation for variables containing but not ending in `delegate`. [Phil Webster](https://github.com/philwebster) - + * Fix `weak_delegate` rule reporting a violation for variables in protocols declarations. [Marcelo Fabri](https://github.com/marcelofabri) diff --git a/README.md b/README.md index 8e92e9bfaa..1e4b34000e 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ variable_name: - id - URL - GlobalAPIKey -reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit) +reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, emoji) ``` #### Defining Custom Rules diff --git a/README_CN.md b/README_CN.md index dc15e8dbfd..89bf608fe6 100644 --- a/README_CN.md +++ b/README_CN.md @@ -162,7 +162,7 @@ variable_name: - id - URL - GlobalAPIKey -reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle) +reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, junit, emoji) ``` #### 定义自定义规则 diff --git a/Source/SwiftLintFramework/Extensions/Array+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/Array+SwiftLint.swift index 5868fa7900..55b34e05aa 100644 --- a/Source/SwiftLintFramework/Extensions/Array+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/Array+SwiftLint.swift @@ -24,3 +24,27 @@ extension Array where Element: NSTextCheckingResult { return map { $0.range } } } + +extension Array where Element: Equatable { + var unique: [Element] { + var uniqueValues: [Element] = [] + forEach { item in + if !uniqueValues.contains(item) { + uniqueValues += [item] + } + } + return uniqueValues + } +} + +extension Array { + // swiftlint:disable:next line_length + func group(by transform: (Element) -> U) -> [U: [Element]] { + var dictionary: [U: [Element]] = [:] + for element in self { + let key = transform(element) + if case nil = dictionary[key]?.append(element) { dictionary[key] = [element] } + } + return dictionary + } +} diff --git a/Source/SwiftLintFramework/Protocols/Reporter.swift b/Source/SwiftLintFramework/Protocols/Reporter.swift index 83aac374a5..70e0c48be2 100644 --- a/Source/SwiftLintFramework/Protocols/Reporter.swift +++ b/Source/SwiftLintFramework/Protocols/Reporter.swift @@ -26,6 +26,8 @@ public func reporterFromString(_ string: String) -> Reporter.Type { return JUnitReporter.self case HTMLReporter.identifier: return HTMLReporter.self + case EmojiReporter.identifier: + return EmojiReporter.self default: fatalError("no reporter with identifier '\(string)' available.") } diff --git a/Source/SwiftLintFramework/Reporters/EmojiReporter.swift b/Source/SwiftLintFramework/Reporters/EmojiReporter.swift new file mode 100644 index 0000000000..80aacee86c --- /dev/null +++ b/Source/SwiftLintFramework/Reporters/EmojiReporter.swift @@ -0,0 +1,71 @@ +// +// EmojiReporter.swift +// SwiftLint +// +// Created by Michał Kałużny on 01/12/2016. +// Copyright © 2016 Realm. All rights reserved. +// + +import Foundation + +public struct EmojiReporter: Reporter { + public static let identifier = "emoji" + public static let isRealtime = false + + public var description: String { + return "Reports violations in the format that's both fun and easy to read." + } + + public static func generateReport(_ violations: [StyleViolation]) -> String { + return violations.group { (violation) in + violation.location.file ?? "Other" + }.map { (filename, violations) in + return reportFor(file: filename, with: violations) + }.joined(separator: "\n") + } + + private static func reportFor(file: String, with violations: [StyleViolation]) -> String { + var lines: [String] = [] + + let sortedViolatons = violations.sorted { (lhs, rhs) -> Bool in + switch (lhs.severity, rhs.severity) { + case (.warning, .error): return false + case (.error, .warning): return true + case (_, _): + switch (lhs.location.line, rhs.location.line) { + case (.some(let lhs), .some(let rhs)): return lhs < rhs + case (.some, .none): return true + case (.none, .some): return false + case (.none, .none): return false + } + } + } + + lines.append(file) + + for violation in sortedViolatons { + var line = "" + line += emojiFor(violationSeverity: violation.severity) + line += " " + + if let locationLine = violation.location.line { + line += "Line \(locationLine): " + } + + line += violation.reason + + lines.append(line) + } + + return lines.joined(separator: "\n") + } + + private static func emojiFor(violationSeverity: ViolationSeverity) -> String { + switch violationSeverity { + case .error: + return "⛔️" + case .warning: + return "⚠️" + } + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index a7aefb9ba7..85096feee1 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 24B4DF0D1D6DFDE90097803B /* RedundantNilCoalescingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */; }; 24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E17F701B1481FF008195BE /* File+Cache.swift */; }; 2E02005F1C54BF680024D09D /* CyclomaticComplexityRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */; }; + 2E336D1B1DF08BFB00CCFE77 /* EmojiReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E336D191DF08AF200CCFE77 /* EmojiReporter.swift */; }; 2E5761AA1C573B83003271AF /* FunctionParameterCountRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5761A91C573B83003271AF /* FunctionParameterCountRule.swift */; }; 3B0B14541C505D6300BE82F7 /* SeverityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0B14531C505D6300BE82F7 /* SeverityConfiguration.swift */; }; 3B1150CA1C31FC3F00D83B1E /* Yaml+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1150C91C31FC3F00D83B1E /* Yaml+SwiftLint.swift */; }; @@ -203,6 +204,7 @@ 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantNilCoalescingRule.swift; sourceTree = ""; }; 24E17F701B1481FF008195BE /* File+Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "File+Cache.swift"; sourceTree = ""; }; 2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityRule.swift; sourceTree = ""; }; + 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 = ""; }; 3B0B14531C505D6300BE82F7 /* SeverityConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeverityConfiguration.swift; sourceTree = ""; }; 3B1150C91C31FC3F00D83B1E /* Yaml+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Yaml+SwiftLint.swift"; sourceTree = ""; }; @@ -620,6 +622,7 @@ E86396C81BADB2B9002C9E88 /* JSONReporter.swift */, E86396C41BADAC15002C9E88 /* XcodeReporter.swift */, 4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */, + 2E336D191DF08AF200CCFE77 /* EmojiReporter.swift */, ); path = Reporters; sourceTree = ""; @@ -973,6 +976,7 @@ D44254271DB9C15C00492EA4 /* SyntacticSugarRule.swift in Sources */, E88198441BEA93D200333A11 /* ColonRule.swift in Sources */, E809EDA11B8A71DF00399043 /* Configuration.swift in Sources */, + 2E336D1B1DF08BFB00CCFE77 /* EmojiReporter.swift in Sources */, E8EA41171C2D1DBE004F9930 /* CheckstyleReporter.swift in Sources */, 006ECFC41C44E99E00EF6364 /* LegacyConstantRule.swift in Sources */, E88DEA731B0984C400A66CB0 /* String+SwiftLint.swift in Sources */, diff --git a/Tests/SwiftLintFrameworkTests/ReporterTests.swift b/Tests/SwiftLintFrameworkTests/ReporterTests.swift index 15025bce35..15a403818f 100644 --- a/Tests/SwiftLintFrameworkTests/ReporterTests.swift +++ b/Tests/SwiftLintFrameworkTests/ReporterTests.swift @@ -18,7 +18,8 @@ class ReporterTests: XCTestCase { CSVReporter.self, CheckstyleReporter.self, JUnitReporter.self, - HTMLReporter.self + HTMLReporter.self, + EmojiReporter.self ] for reporter in reporters { XCTAssertEqual(reporter.identifier, reporterFromString(reporter.identifier).identifier) @@ -46,6 +47,15 @@ class ReporterTests: XCTestCase { ) } + func testEmojiReporter() { + XCTAssertEqual( + EmojiReporter.generateReport(generateViolations()), + "filename\n" + + "⛔️ Line 1: Violation Reason.\n" + + "⚠️ Line 1: Violation Reason." + ) + } + func testJSONReporter() { XCTAssertEqual( JSONReporter.generateReport(generateViolations()),