import Foundation import SourceKittenFramework import TestHelpers import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintFramework @Suite(.rulesRegistered) struct ReporterTests { private static var violations: [StyleViolation] { [ StyleViolation( ruleDescription: LineLengthRule.description, location: Location(file: URL.cwd.appending(path: "filename"), line: 1, character: 1), reason: "Violation Reason 1" ), StyleViolation( ruleDescription: LineLengthRule.description, severity: .error, location: Location(file: URL.cwd.appending(path: "filename"), line: 1), reason: "Violation Reason 2" ), StyleViolation( ruleDescription: SyntacticSugarRule.description, severity: .error, location: Location( file: URL.cwd .appending(path: "path", directoryHint: .isDirectory) .appending(path: "file.swift", directoryHint: .notDirectory), line: 1, character: 2 ), reason: "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array"), StyleViolation( ruleDescription: ColonRule.description, severity: .error, location: Location(file: nil), reason: nil ), ] } @Test func reporterFromString() { for reporter in reportersList { #expect(reporter.identifier == reporterFrom(identifier: reporter.identifier).identifier) } } private func stringFromFile(_ filename: String) -> String { let path = TestResources.path().appending(path: filename, directoryHint: .notDirectory) return SwiftLintFile(path: path)!.contents } @Test func xcodeReporter() throws { try assertEqualContent( referenceFile: "CannedXcodeReporterOutput.txt", reporterType: XcodeReporter.self ) } @Test func emojiReporter() throws { try assertEqualContent( referenceFile: "CannedEmojiReporterOutput.txt", reporterType: EmojiReporter.self ) } @Test func gitHubActionsLoggingReporter() throws { try assertEqualContent( referenceFile: "CannedGitHubActionsLoggingReporterOutput.txt", reporterType: GitHubActionsLoggingReporter.self ) } @Test func gitLabJUnitReporter() throws { try assertEqualContent( referenceFile: "CannedGitLabJUnitReporterOutput.xml", reporterType: GitLabJUnitReporter.self ) } private func jsonValue(_ jsonString: String) throws -> NSObject { let data = jsonString.data(using: .utf8)! let result = try JSONSerialization.jsonObject(with: data, options: []) if let dict = (result as? [String: Any])?.bridge() { return dict } if let array = (result as? [Any])?.bridge() { return array } queuedFatalError("Unexpected value in JSON: \(result)") } @Test func jsonReporter() throws { try assertEqualContent( referenceFile: "CannedJSONReporterOutput.json", reporterType: JSONReporter.self, stringConverter: { try jsonValue($0) } ) } @Test func cSVReporter() throws { try assertEqualContent( referenceFile: "CannedCSVReporterOutput.csv", reporterType: CSVReporter.self ) } @Test func checkstyleReporter() throws { try assertEqualContent( referenceFile: "CannedCheckstyleReporterOutput.xml", reporterType: CheckstyleReporter.self ) } @Test func codeClimateReporter() throws { try assertEqualContent( referenceFile: "CannedCodeClimateReporterOutput.json", reporterType: CodeClimateReporter.self ) } @Test func sarifReporter() throws { try assertEqualContent( referenceFile: "CannedSARIFReporterOutput.sarif", reporterType: SARIFReporter.self, stringConverter: { try sarifValueWithoutRules($0) } ) } private func sarifValueWithoutRules(_ jsonString: String) throws -> NSObject { let data = jsonString.data(using: .utf8)! guard var json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { queuedFatalError("Expected dictionary in SARIF JSON") } // Extract and verify rules before removing them if var runs = json["runs"] as? [[String: Any]], var firstRun = runs.first, var tool = firstRun["tool"] as? [String: Any], var driver = tool["driver"] as? [String: Any], let rules = driver["rules"] as? [[String: Any]] { // Verify rules array is not empty #expect(!rules.isEmpty) // Verify rule structure if let firstRule = rules.first { #expect(firstRule["id"] != nil) #expect(firstRule["shortDescription"] != nil) #expect(firstRule["fullDescription"] != nil) #expect(firstRule["helpUri"] != nil) } // Remove rules array for comparison driver.removeValue(forKey: "rules") tool["driver"] = driver firstRun["tool"] = tool runs[0] = firstRun json["runs"] = runs } return json.bridge() } @Test func junitReporter() throws { try assertEqualContent( referenceFile: "CannedJunitReporterOutput.xml", reporterType: JUnitReporter.self ) } @Test func htmlReporter() throws { try assertEqualContent( referenceFile: "CannedHTMLReporterOutput.html", reporterType: HTMLReporter.self ) } @Test func sonarQubeReporter() throws { try assertEqualContent( referenceFile: "CannedSonarQubeReporterOutput.json", reporterType: SonarQubeReporter.self, stringConverter: { try jsonValue($0) } ) } @Test func markdownReporter() throws { try assertEqualContent( referenceFile: "CannedMarkdownReporterOutput.md", reporterType: MarkdownReporter.self ) } @Test func relativePathReporter() throws { try assertEqualContent( referenceFile: "CannedRelativePathReporterOutput.txt", reporterType: RelativePathReporter.self ) } @Test func relativePathReporterPaths() { let relativePath = "filename".url() let location = Location(file: relativePath, line: 1, character: 2) let violation = StyleViolation(ruleDescription: LineLengthRule.description, location: location, reason: "Violation Reason") let result = RelativePathReporter.generateReport([violation]) #expect(!result.contains(relativePath.filepath)) #expect(result.contains(relativePath.relativeDisplayPath)) } @Test func summaryReporter() { let expectedOutput = stringFromFile("CannedSummaryReporterOutput.txt") .trimmingTrailingCharacters(in: .whitespacesAndNewlines) let correctableViolation = StyleViolation( ruleDescription: VerticalWhitespaceOpeningBracesRule.description, location: Location(file: URL.cwd.appending(path: "filename"), line: 1, character: 2), reason: "Violation Reason" ) let result = SummaryReporter.generateReport(Self.violations + [correctableViolation]) #expect(result == expectedOutput) } @Test func summaryReporterWithNoViolations() { let expectedOutput = stringFromFile("CannedSummaryReporterNoViolationsOutput.txt") .trimmingTrailingCharacters(in: .whitespacesAndNewlines) let result = SummaryReporter.generateReport([]) #expect(result == expectedOutput) } private func assertEqualContent( referenceFile: String, reporterType: any Reporter.Type, stringConverter: (String) throws -> some Equatable = \.self, sourceLocation: Testing.SourceLocation = #_sourceLocation ) throws { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short let reference = stringFromFile(referenceFile).replacingOccurrences( of: "${CURRENT_WORKING_DIRECTORY}", with: URL.cwd.path ).replacingOccurrences( of: "${SWIFTLINT_VERSION}", with: SwiftLintFramework.Version.current.value ).replacingOccurrences( of: "${TODAYS_DATE}", with: dateFormatter.string(from: Date()) ) let reporterOutput = reporterType.generateReport(Self.violations) let convertedReference = try stringConverter(reference) let convertedReporterOutput = try stringConverter(reporterOutput) if convertedReference != convertedReporterOutput { let referenceURL = TestResources.path().appending(path: referenceFile, directoryHint: .notDirectory) try reporterOutput.replacingOccurrences( of: URL.cwd.path, with: "${CURRENT_WORKING_DIRECTORY}" ).replacingOccurrences( of: SwiftLintFramework.Version.current.value, with: "${SWIFTLINT_VERSION}" ).replacingOccurrences( of: dateFormatter.string(from: Date()), with: "${TODAYS_DATE}" ) .write(to: referenceURL, atomically: true, encoding: .utf8) } #expect( convertedReference == convertedReporterOutput, sourceLocation: sourceLocation ) } }