From e7cc71138bb1cb57318fd8a8aaad4c672a837a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 7 Sep 2021 14:41:13 +0200 Subject: [PATCH] Write custom script tests + some refactoring --- Package.swift | 26 ++-- README.md | 2 +- Sources/Checkers/FileContentsChecker.swift | 8 +- Sources/Checkers/FilePathsChecker.swift | 4 +- Sources/Checkers/Lint.swift | 48 +++++++- Sources/Commands/LintCommand.swift | 4 +- Sources/Configuration/Templates/Blank.yml | 2 +- .../Configuration/Templates/OpenSource.yml | 2 +- Sources/Core/AutoCorrection.swift | 2 +- Sources/Core/CheckInfo.swift | 31 +++-- Sources/Core/Location.swift | 2 +- Sources/Core/Logging/ConsoleLogger.swift | 2 +- Sources/Core/Logging/Loggable.swift | 4 +- Sources/Core/Logging/XcodeLogger.swift | 6 +- Sources/Core/Violation.swift | 8 +- .../Reporting/Extensions/JSONDecoderExt.swift | 2 +- .../Reporting/Extensions/JSONEncoderExt.swift | 3 +- Sources/Reporting/LintResults.swift | 12 +- Sources/TestSupport/TestLogger.swift | 6 +- .../FileContentsCheckerTests.swift | 114 +++++++++--------- .../CheckersTests/FilePathsCheckerTests.swift | 8 +- Tests/CheckersTests/LintTests.swift | 69 ++++++++++- Tests/CoreTests/ViolationTests.swift | 10 +- Tests/ReportingTests/LintResultsTests.swift | 87 ++++++++++--- 24 files changed, 316 insertions(+), 146 deletions(-) diff --git a/Package.swift b/Package.swift index 9bcce97..010ef1d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( name: "AnyLint", - platforms: [.macOS(.v10_12)], + platforms: [.macOS(.v10_15)], products: [ .executable(name: "anylint", targets: ["Commands"]), ], @@ -36,7 +36,14 @@ let package = Package( .product(name: "Rainbow", package: "Rainbow"), ] ), - .target(name: "Checkers", dependencies: ["Core"]), + .target( + name: "Checkers", + dependencies: [ + "Core", + "Reporting", + .product(name: "ShellOut", package: "ShellOut"), + ] + ), .target( name: "Configuration", dependencies: [ @@ -68,18 +75,17 @@ let package = Package( ), // test targets - .target(name: "TestSupport", dependencies: ["Core"]), - .testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]), - .testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]), - .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), - .testTarget( - name: "ReportingTests", + .target( + name: "TestSupport", dependencies: [ + "Core", .product(name: "CustomDump", package: "swift-custom-dump"), - "Reporting", - "TestSupport", ] ), + .testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]), + .testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]), + .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), + .testTarget(name: "ReportingTests", dependencies: ["Reporting", "TestSupport"]), .testTarget(name: "CommandsTests", dependencies: ["Commands"]), ] ) diff --git a/README.md b/README.md index f87af9e..114f898 100644 --- a/README.md +++ b/README.md @@ -529,7 +529,7 @@ multiline2: |+ multiline3: |- This will ignore any trailing newlines and - will end with the lest non-newline character (the following dot in this case -->). + will end with the last non-newline character (the following dot in this case -->). ``` diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift index de8ba3d..98c2f66 100644 --- a/Sources/Checkers/FileContentsChecker.swift +++ b/Sources/Checkers/FileContentsChecker.swift @@ -43,11 +43,11 @@ extension FileContentsChecker: Checker { let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(id)"#) for match in regex.matches(in: fileContents).reversed() { - let fileLocation = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath) + let location = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath) // skip found match if contains `AnyLint.skipHere: ` in same line or one line before guard - !linesInFile.containsLine(at: [fileLocation.row! - 2, fileLocation.row! - 1], matchingRegex: skipHereRegex) + !linesInFile.containsLine(at: [location.row! - 2, location.row! - 1], matchingRegex: skipHereRegex) else { continue } let autoCorrection: AutoCorrection? = { @@ -70,7 +70,7 @@ extension FileContentsChecker: Checker { violations.append( Violation( matchedString: match.string, - fileLocation: fileLocation, + location: location, appliedAutoCorrection: autoCorrection ) ) @@ -93,7 +93,7 @@ extension FileContentsChecker: Checker { if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { // only paths where auto-corrections were applied need to be re-checked let filePathsToReCheck = Array( - Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.fileLocation!.filePath }) + Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.location!.filePath }) ) .sorted() diff --git a/Sources/Checkers/FilePathsChecker.swift b/Sources/Checkers/FilePathsChecker.swift index 9c83e36..729d686 100644 --- a/Sources/Checkers/FilePathsChecker.swift +++ b/Sources/Checkers/FilePathsChecker.swift @@ -33,7 +33,7 @@ extension FilePathsChecker: Checker { let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count if matchingFilePathsCount <= 0 { violations.append( - Violation(fileLocation: nil, appliedAutoCorrection: nil) + Violation(location: nil, appliedAutoCorrection: nil) ) } } @@ -50,7 +50,7 @@ extension FilePathsChecker: Checker { violations.append( Violation( - fileLocation: .init(filePath: filePath), + location: .init(filePath: filePath), appliedAutoCorrection: appliedAutoCorrection ) ) diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index 28bf807..612e023 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -1,6 +1,8 @@ import Foundation import Core import OrderedCollections +import ShellOut +import Reporting /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { @@ -129,9 +131,27 @@ public enum Lint { /// Run custom scripts as checks. /// - /// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. Else, it will report exactly one violation if the command has a non-zero exit code with the last line(s) of output. - public static func runCustomScript(checkInfo: CheckInfo, command: String) throws -> [Violation] { - fatalError() // TODO: [cg_2021-07-09] not yet implemented + /// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. If the output iis an array of ``Violation`` instances, they will be wrapped in a ``LintResults`` object. Else, it will report exactly one violation if the command has a non-zero exit code with the last line(s) of output. + public static func runCustomScript(checkInfo: CheckInfo, command: String) throws -> LintResults { + let tempScriptFileUrl = URL(fileURLWithPath: "_\(checkInfo.id).tempscript") + try command.write(to: tempScriptFileUrl, atomically: true, encoding: .utf8) + + let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) + if let jsonString = output.lintResultsJsonString, + let jsonData = jsonString.data(using: .utf8), + let lintResults: LintResults = try? JSONDecoder.iso.decode(LintResults.self, from: jsonData) + { + return lintResults + } + else if let jsonString = output.violationsArrayJsonString, + let jsonData = jsonString.data(using: .utf8), + let violations: [Violation] = try? JSONDecoder.iso.decode([Violation].self, from: jsonData) + { + return [checkInfo.severity: [checkInfo: violations]] + } + else { + return [checkInfo.severity: [checkInfo: [Violation()]]] + } } static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { @@ -196,10 +216,30 @@ public enum Lint { guard autoCorrectReplacement == nil || violateIfNoMatchesFound != true else { log.message( - "Incompatible options specified for check \(checkInfo.id): autoCorrectReplacement and violateIfNoMatchesFound can't be used together.", + "Incompatible options specified for check \(checkInfo.id): `autoCorrectReplacement` and `violateIfNoMatchesFound` can't be used together.", level: .error ) log.exit(fail: true) } } } + +fileprivate extension String { + var lintResultsJsonString: String? { + try! Regex( + #"\{.*(?:\"error\"\s*:|\"warning\"\s*:|\"info\"\s*:).*\}"#, + options: .dotMatchesLineSeparators + ) + .firstMatch(in: self)? + .string + } + + var violationsArrayJsonString: String? { + try! Regex( + #"\[(?:\s*\{.*\}\s*,)*(?:\s*\{.*\}\s*)?\]"#, + options: .dotMatchesLineSeparators + ) + .firstMatch(in: self)? + .string + } +} diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index eb60415..2b063d8 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -103,12 +103,12 @@ struct LintCommand: ParsableCommand { // run `CustomScripts` checks for customScriptConfig in lintConfig.customScripts { - let violations = try Lint.runCustomScript( + let customScriptLintResults = try Lint.runCustomScript( checkInfo: customScriptConfig.checkInfo, command: customScriptConfig.command ) - lintResults.appendViolations(violations, forCheck: customScriptConfig.checkInfo) + lintResults.mergeResults(customScriptLintResults) } // report violations & exit with right status code diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index c36f66f..8dd7b5d 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -25,5 +25,5 @@ CustomScripts: [] # if which yamllint > /dev/null; then # yamllint anylint.yml # else -# echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint" +# echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' # fi diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 1e9068d..78cf79f 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -57,5 +57,5 @@ CustomScripts: if which yamllint > /dev/null; then yamllint anylint.yml else - echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint" + echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' fi diff --git a/Sources/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift index 7f791ea..426b04b 100644 --- a/Sources/Core/AutoCorrection.swift +++ b/Sources/Core/AutoCorrection.swift @@ -1,7 +1,7 @@ import Foundation /// Information about an autocorrection. -public struct AutoCorrection: Codable { +public struct AutoCorrection: Codable, Equatable { private enum Constants { /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. static let newlinesRequiredForDiffing: Int = 3 diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/CheckInfo.swift index bd3805f..7653e35 100644 --- a/Sources/Core/CheckInfo.swift +++ b/Sources/Core/CheckInfo.swift @@ -35,10 +35,26 @@ extension CheckInfo: Codable { ) throws { let container = try decoder.singleValueContainer() let rawString = try container.decode(String.self) + self.init(rawValue: rawString)! + } - let customSeverityRegex = try Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#) + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} + +extension CheckInfo: RawRepresentable { + public var rawValue: String { + "\(id)@\(severity.rawValue): \(hint)" + } - if let match = customSeverityRegex.firstMatch(in: rawString) { + public init?( + rawValue: String + ) { + let customSeverityRegex = try! Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#) + + if let match = customSeverityRegex.firstMatch(in: rawValue) { let id = match.captures[0]! let severityString = match.captures[1]! let hint = match.captures[2]! @@ -54,11 +70,11 @@ extension CheckInfo: Codable { self = .init(id: id, hint: hint, severity: severity) } else { - let defaultSeverityRegex = try Regex(#"^([^@:]+): ?(.*$)"#) + let defaultSeverityRegex = try! Regex(#"^([^@:]+): ?(.*$)"#) - guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawString) else { + guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawValue) else { log.message( - "Could not convert String literal '\(rawString)' to type CheckInfo. Please check the structure to be: (@): ", + "Could not convert String literal '\(rawValue)' to type CheckInfo. Please check the structure to be: (@): ", level: .error ) log.exit(fail: true) @@ -70,9 +86,4 @@ extension CheckInfo: Codable { self = .init(id: id, hint: hint) } } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode("\(id)@\(severity.rawValue): \(hint)") - } } diff --git a/Sources/Core/Location.swift b/Sources/Core/Location.swift index 347dda0..3e95546 100644 --- a/Sources/Core/Location.swift +++ b/Sources/Core/Location.swift @@ -1,7 +1,7 @@ import Foundation /// Info about the exact location of a character in a given file. -public struct Location: Codable { +public struct Location: Codable, Equatable { /// The path to the file. public let filePath: String diff --git a/Sources/Core/Logging/ConsoleLogger.swift b/Sources/Core/Logging/ConsoleLogger.swift index ea0ecde..98206c9 100644 --- a/Sources/Core/Logging/ConsoleLogger.swift +++ b/Sources/Core/Logging/ConsoleLogger.swift @@ -8,7 +8,7 @@ public final class ConsoleLogger: Loggable { /// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning. /// - level: The level of the print statement. /// - location: The file, line and char in line location string. - public func message(_ message: String, level: PrintLevel, fileLocation: Location?) { + public func message(_ message: String, level: PrintLevel, location: Location?) { switch level { case .success: print(formattedCurrentTime(), "✅", message.green) diff --git a/Sources/Core/Logging/Loggable.swift b/Sources/Core/Logging/Loggable.swift index 2e94009..6baaf60 100644 --- a/Sources/Core/Logging/Loggable.swift +++ b/Sources/Core/Logging/Loggable.swift @@ -4,7 +4,7 @@ import Foundation public var log: Loggable = ConsoleLogger() public protocol Loggable { - func message(_ message: String, level: PrintLevel, fileLocation: Location?) + func message(_ message: String, level: PrintLevel, location: Location?) } extension Loggable { @@ -21,6 +21,6 @@ extension Loggable { /// Convenience overload of `message(:level:fileLocation:)` with `fileLocation` set to `nil`. public func message(_ message: String, level: PrintLevel) { - self.message(message, level: level, fileLocation: nil) + self.message(message, level: level, location: nil) } } diff --git a/Sources/Core/Logging/XcodeLogger.swift b/Sources/Core/Logging/XcodeLogger.swift index 2c76733..1d033c4 100644 --- a/Sources/Core/Logging/XcodeLogger.swift +++ b/Sources/Core/Logging/XcodeLogger.swift @@ -8,11 +8,11 @@ public final class XcodeLogger: Loggable { /// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning. /// - level: The level of the print statement. /// - location: The file, line and char in line location string. - public func message(_ message: String, level: PrintLevel, fileLocation: Location?) { + public func message(_ message: String, level: PrintLevel, location: Location?) { var locationPrefix = "" - if let fileLocation = fileLocation { - locationPrefix = fileLocation.locationMessage(pathType: .absolute) + " " + if let location = location { + locationPrefix = location.locationMessage(pathType: .absolute) + " " } print("\(locationPrefix)\(level.rawValue): AnyLint: \(message)") diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index df0104f..6f99736 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -1,12 +1,12 @@ import Foundation /// A violation found in a check. -public struct Violation: Codable { +public struct Violation: Codable, Equatable { /// The matched string that violates the check. public let matchedString: String? /// The info about the exact location of the violation within the file. Will be ignored if no `filePath` specified. - public let fileLocation: Location? + public let location: Location? /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? @@ -14,11 +14,11 @@ public struct Violation: Codable { /// Initializes a violation object. public init( matchedString: String? = nil, - fileLocation: Location? = nil, + location: Location? = nil, appliedAutoCorrection: AutoCorrection? = nil ) { self.matchedString = matchedString - self.fileLocation = fileLocation + self.location = location self.appliedAutoCorrection = appliedAutoCorrection } } diff --git a/Sources/Reporting/Extensions/JSONDecoderExt.swift b/Sources/Reporting/Extensions/JSONDecoderExt.swift index c5dbc53..7e5bf7d 100644 --- a/Sources/Reporting/Extensions/JSONDecoderExt.swift +++ b/Sources/Reporting/Extensions/JSONDecoderExt.swift @@ -1,7 +1,7 @@ import Foundation extension JSONDecoder { - static var iso: JSONDecoder { + public static var iso: JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder diff --git a/Sources/Reporting/Extensions/JSONEncoderExt.swift b/Sources/Reporting/Extensions/JSONEncoderExt.swift index 7a48077..1e54af8 100644 --- a/Sources/Reporting/Extensions/JSONEncoderExt.swift +++ b/Sources/Reporting/Extensions/JSONEncoderExt.swift @@ -1,9 +1,10 @@ import Foundation extension JSONEncoder { - static var iso: JSONEncoder { + public static var iso: JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .prettyPrinted return encoder } } diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index e8fd046..bbfa606 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -6,11 +6,13 @@ import OrderedCollections public typealias LintResults = OrderedDictionary> extension LintResults { - var allExecutedChecks: [CheckInfo] { + /// Returns a list of all executed checks. + public var allExecutedChecks: [CheckInfo] { values.reduce(into: []) { $0.append(contentsOf: $1.keys) } } - var allFoundViolations: [Violation] { + /// Returns a list of all found violations. + public var allFoundViolations: [Violation] { values.reduce(into: []) { $0.append(contentsOf: $1.values.flatMap { $0 }) } } @@ -102,7 +104,7 @@ extension LintResults { let checkViolations = violations(check: check, excludeAutocorrected: false) if checkViolations.isFilled { - let violationsWithLocationMessage = checkViolations.filter { $0.fileLocation != nil } + let violationsWithLocationMessage = checkViolations.filter { $0.location != nil } if violationsWithLocationMessage.isFilled { log.message( @@ -115,7 +117,7 @@ extension LintResults { let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) let prefix = "> \(violationNumString). " log.message( - prefix + violation.fileLocation!.locationMessage(pathType: .relative), + prefix + violation.location!.locationMessage(pathType: .relative), level: check.severity.logLevel ) @@ -171,7 +173,7 @@ extension LintResults { log.message( "[\(checkInfo.id)] \(checkInfo.hint)", level: severity.logLevel, - fileLocation: violation.fileLocation + location: violation.location ) } } diff --git a/Sources/TestSupport/TestLogger.swift b/Sources/TestSupport/TestLogger.swift index 1d12b57..7925569 100644 --- a/Sources/TestSupport/TestLogger.swift +++ b/Sources/TestSupport/TestLogger.swift @@ -9,10 +9,10 @@ public final class TestLogger: Loggable { loggedMessages = [] } - public func message(_ message: String, level: PrintLevel, fileLocation: Location?) { - if let fileLocation = fileLocation { + public func message(_ message: String, level: PrintLevel, location: Location?) { + if let location = location { loggedMessages.append( - "[\(level.rawValue)] \(fileLocation.locationMessage(pathType: .relative)) \(message)" + "[\(level.rawValue)] \(location.locationMessage(pathType: .relative)) \(message)" ) } else { diff --git a/Tests/CheckersTests/FileContentsCheckerTests.swift b/Tests/CheckersTests/FileContentsCheckerTests.swift index 592cf9d..b2561de 100644 --- a/Tests/CheckersTests/FileContentsCheckerTests.swift +++ b/Tests/CheckersTests/FileContentsCheckerTests.swift @@ -25,14 +25,14 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 2) XCTAssertEqual(violations[0].matchedString, "let x=5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 1) - XCTAssertEqual(violations[0].fileLocation!.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[0].location?.row, 1) + XCTAssertEqual(violations[0].location!.column, 1) XCTAssertEqual(violations[1].matchedString, "var y=10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 2) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].location?.row, 2) + XCTAssertEqual(violations[1].location?.column, 1) } } @@ -61,14 +61,14 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 2) XCTAssertEqual(violations[0].matchedString, "let x=5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 4) - XCTAssertEqual(violations[0].fileLocation?.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[0].location?.row, 4) + XCTAssertEqual(violations[0].location?.column, 1) XCTAssertEqual(violations[1].matchedString, "var y=10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 5) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[1].location?.row, 5) + XCTAssertEqual(violations[1].location?.column, 1) } } @@ -97,34 +97,34 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 6) XCTAssertEqual(violations[0].matchedString, "let x=5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 4) - XCTAssertEqual(violations[0].fileLocation?.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/Hello.swift") + XCTAssertEqual(violations[0].location?.row, 4) + XCTAssertEqual(violations[0].location?.column, 1) XCTAssertEqual(violations[1].matchedString, "var y=10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 5) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/Hello.swift") + XCTAssertEqual(violations[1].location?.row, 5) + XCTAssertEqual(violations[1].location?.column, 1) XCTAssertEqual(violations[2].matchedString, "var y=10") - XCTAssertEqual(violations[2].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[2].fileLocation?.row, 5) - XCTAssertEqual(violations[2].fileLocation?.column, 1) + XCTAssertEqual(violations[2].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[2].location?.row, 5) + XCTAssertEqual(violations[2].location?.column, 1) XCTAssertEqual(violations[3].matchedString, "let x=5") - XCTAssertEqual(violations[3].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[3].fileLocation?.row, 4) - XCTAssertEqual(violations[3].fileLocation?.column, 1) + XCTAssertEqual(violations[3].location?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[3].location?.row, 4) + XCTAssertEqual(violations[3].location?.column, 1) XCTAssertEqual(violations[4].matchedString, "let x=5") - XCTAssertEqual(violations[4].fileLocation?.filePath, "\(tempDir)/Sources/Bar.swift") - XCTAssertEqual(violations[4].fileLocation?.row, 4) - XCTAssertEqual(violations[4].fileLocation?.column, 1) + XCTAssertEqual(violations[4].location?.filePath, "\(tempDir)/Sources/Bar.swift") + XCTAssertEqual(violations[4].location?.row, 4) + XCTAssertEqual(violations[4].location?.column, 1) XCTAssertEqual(violations[5].matchedString, "var y=10") - XCTAssertEqual(violations[5].fileLocation?.filePath, "\(tempDir)/Sources/Bar.swift") - XCTAssertEqual(violations[5].fileLocation?.row, 5) - XCTAssertEqual(violations[5].fileLocation?.column, 1) + XCTAssertEqual(violations[5].location?.filePath, "\(tempDir)/Sources/Bar.swift") + XCTAssertEqual(violations[5].location?.row, 5) + XCTAssertEqual(violations[5].location?.column, 1) } } @@ -149,14 +149,14 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 2) XCTAssertEqual(violations[0].matchedString, "let x =5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 1) - XCTAssertEqual(violations[0].fileLocation?.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[0].location?.row, 1) + XCTAssertEqual(violations[0].location?.column, 1) XCTAssertEqual(violations[1].matchedString, "var y= 10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 2) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].location?.row, 2) + XCTAssertEqual(violations[1].location?.column, 1) } } @@ -181,45 +181,45 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 7) XCTAssertEqual(violations[0].matchedString, "10000") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 2) - XCTAssertEqual(violations[0].fileLocation?.column, 9) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/Hello.swift") + XCTAssertEqual(violations[0].location?.row, 2) + XCTAssertEqual(violations[0].location?.column, 9) XCTAssertEqual(violations[0].appliedAutoCorrection!.after, "10_000") XCTAssertEqual(violations[1].matchedString, "50000000") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 1) - XCTAssertEqual(violations[1].fileLocation?.column, 9) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].location?.row, 1) + XCTAssertEqual(violations[1].location?.column, 9) XCTAssertEqual(violations[1].appliedAutoCorrection!.after, "50000_000") XCTAssertEqual(violations[2].matchedString, "100000000000000") - XCTAssertEqual(violations[2].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[2].fileLocation?.row, 2) - XCTAssertEqual(violations[2].fileLocation?.column, 9) + XCTAssertEqual(violations[2].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[2].location?.row, 2) + XCTAssertEqual(violations[2].location?.column, 9) XCTAssertEqual(violations[2].appliedAutoCorrection!.after, "100000000000_000") XCTAssertEqual(violations[3].matchedString, "50000") - XCTAssertEqual(violations[3].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[3].fileLocation?.row, 1) - XCTAssertEqual(violations[3].fileLocation?.column, 9) + XCTAssertEqual(violations[3].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[3].location?.row, 1) + XCTAssertEqual(violations[3].location?.column, 9) XCTAssertEqual(violations[3].appliedAutoCorrection!.after, "50_000") XCTAssertEqual(violations[4].matchedString, "100000000000") - XCTAssertEqual(violations[4].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[4].fileLocation?.row, 2) - XCTAssertEqual(violations[4].fileLocation?.column, 9) + XCTAssertEqual(violations[4].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[4].location?.row, 2) + XCTAssertEqual(violations[4].location?.column, 9) XCTAssertEqual(violations[4].appliedAutoCorrection!.after, "100000000_000") XCTAssertEqual(violations[5].matchedString, "100000000") - XCTAssertEqual(violations[5].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[5].fileLocation?.row, 2) - XCTAssertEqual(violations[5].fileLocation?.column, 9) + XCTAssertEqual(violations[5].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[5].location?.row, 2) + XCTAssertEqual(violations[5].location?.column, 9) XCTAssertEqual(violations[5].appliedAutoCorrection!.after, "100000_000") XCTAssertEqual(violations[6].matchedString, "100000") - XCTAssertEqual(violations[6].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[6].fileLocation?.row, 2) - XCTAssertEqual(violations[6].fileLocation?.column, 9) + XCTAssertEqual(violations[6].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[6].location?.row, 2) + XCTAssertEqual(violations[6].location?.column, 9) XCTAssertEqual(violations[6].appliedAutoCorrection!.after, "100_000") } } diff --git a/Tests/CheckersTests/FilePathsCheckerTests.swift b/Tests/CheckersTests/FilePathsCheckerTests.swift index 447e23b..1c1bd66 100644 --- a/Tests/CheckersTests/FilePathsCheckerTests.swift +++ b/Tests/CheckersTests/FilePathsCheckerTests.swift @@ -19,7 +19,7 @@ final class FilePathsCheckerTests: XCTestCase { let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 1) - XCTAssertNil(violations[0].fileLocation) + XCTAssertNil(violations[0].location) } withTemporaryFiles( @@ -31,9 +31,9 @@ final class FilePathsCheckerTests: XCTestCase { let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 1) - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertNil(violations[0].fileLocation?.row) - XCTAssertNil(violations[0].fileLocation?.column) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertNil(violations[0].location?.row) + XCTAssertNil(violations[0].location?.column) } } diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index e99f942..2b874ad 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -2,6 +2,8 @@ import XCTest import Core import TestSupport +import CustomDump +import Reporting final class LintTests: XCTestCase { var testLogger: TestLogger = .init() @@ -117,12 +119,73 @@ final class LintTests: XCTestCase { // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) } - func testRunCustomScript() { - // TODO: [cg_2021-09-05] not yet implemented + func testRunCustomScript() throws { + var lintResults: LintResults = try Lint.runCustomScript( + checkInfo: .init(id: "1", hint: "hint #1"), + command: """ + if which echo > /dev/null; then + echo 'Executed custom checks with following result: + { + "warning": { + "A@warning: hint for A": [ + {}, + { "matchedString": "A" }, + { + "matchedString": "AAA", + "location": { "filePath": "/some/path", "row": 5 }, + "appliedAutoCorrection": { "before": "AAA", "after": "A" } + } + ] + }, + "info": { + "B@info: hint for B": [] + } + } + + Total: 0 errors, 3 warnings, 0 info.' + fi + + """ + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["A", "B"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 3) + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), ["A", "AAA"]) + XCTAssertEqual(lintResults.allFoundViolations.dropFirst().first?.location?.filePath, "/some/path") + XCTAssertEqual(lintResults.allFoundViolations.dropFirst().first?.location?.row, 5) + XCTAssertEqual(lintResults.allFoundViolations.dropFirst().first?.appliedAutoCorrection?.after, "A") + XCTAssertNil(lintResults[.error]?.keys.first) + XCTAssertEqual(lintResults[.info]?.keys.first?.id, "B") } func testValidateParameterCombinations() { - // TODO: [cg_2021-09-05] not yet implemented + XCTAssertNoDifference(testLogger.loggedMessages, []) + + Lint.validateParameterCombinations( + checkInfo: .init(id: "1", hint: "hint #1"), + autoCorrectReplacement: nil, + autoCorrectExamples: [.init(before: "abc", after: "cba")], + violateIfNoMatchesFound: false + ) + + XCTAssertNoDifference( + testLogger.loggedMessages, + ["[warning] `autoCorrectExamples` provided for check 1 without specifying an `autoCorrectReplacement`."] + ) + + // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` + // Lint.validateParameterCombinations( + // checkInfo: .init(id: "2", hint: "hint #2"), + // autoCorrectReplacement: "$3$2$1", + // autoCorrectExamples: [.init(before: "abc", after: "cba")], + // violateIfNoMatchesFound: true + // ) + // + // + // XCTAssertEqual( + // testLogger.loggedMessages.last, + // "Incompatible options specified for check 2: `autoCorrectReplacement` and `violateIfNoMatchesFound` can't be used together." + // ) } func testCheckFileContents() { diff --git a/Tests/CoreTests/ViolationTests.swift b/Tests/CoreTests/ViolationTests.swift index ff83337..e9425a8 100644 --- a/Tests/CoreTests/ViolationTests.swift +++ b/Tests/CoreTests/ViolationTests.swift @@ -3,14 +3,14 @@ import XCTest final class ViolationTests: XCTestCase { func testLocationMessage() { - XCTAssertNil(Violation().fileLocation?.locationMessage(pathType: .relative)) + XCTAssertNil(Violation().location?.locationMessage(pathType: .relative)) - let fileViolation = Violation(fileLocation: .init(filePath: "Temp/Sources/Hello.swift")) - XCTAssertEqual(fileViolation.fileLocation?.locationMessage(pathType: .relative), "Temp/Sources/Hello.swift") + let fileViolation = Violation(location: .init(filePath: "Temp/Sources/Hello.swift")) + XCTAssertEqual(fileViolation.location?.locationMessage(pathType: .relative), "Temp/Sources/Hello.swift") - let locationInfoViolation = Violation(fileLocation: .init(filePath: "Temp/Sources/World.swift", row: 5, column: 15)) + let locationInfoViolation = Violation(location: .init(filePath: "Temp/Sources/World.swift", row: 5, column: 15)) XCTAssertEqual( - locationInfoViolation.fileLocation?.locationMessage(pathType: .relative), + locationInfoViolation.location?.locationMessage(pathType: .relative), "Temp/Sources/World.swift:5:15:" ) } diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index 7b31fa1..8e1b077 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -9,27 +9,27 @@ final class LintResultsTests: XCTestCase { [ Severity.error: [ CheckInfo(id: "1", hint: "hint #1", severity: .error): [ - Violation(matchedString: "oink1", fileLocation: .init(filePath: "/sample/path1", row: 4, column: 2)), - Violation(matchedString: "boo1", fileLocation: .init(filePath: "/sample/path2", row: 40, column: 20)), + Violation(matchedString: "oink1", location: .init(filePath: "/sample/path1", row: 4, column: 2)), + Violation(matchedString: "boo1", location: .init(filePath: "/sample/path2", row: 40, column: 20)), Violation( - fileLocation: .init(filePath: "/sample/path2"), + location: .init(filePath: "/sample/path2"), appliedAutoCorrection: .init(before: "foo", after: "bar") ), ] ], Severity.warning: [ CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ - Violation(matchedString: "oink2", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), - Violation(matchedString: "boo2", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), + Violation(matchedString: "oink2", location: .init(filePath: "/sample/path1", row: 5, column: 6)), + Violation(matchedString: "boo2", location: .init(filePath: "/sample/path3", row: 50, column: 60)), Violation( - fileLocation: .init(filePath: "/sample/path4"), + location: .init(filePath: "/sample/path4"), appliedAutoCorrection: .init(before: "fool", after: "barl") ), ] ], Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], ] @@ -45,7 +45,7 @@ final class LintResultsTests: XCTestCase { let allFoundViolations = sampleLintResults.allFoundViolations XCTAssertNoDifference(allFoundViolations.count, 7) XCTAssertNoDifference( - allFoundViolations.map(\.fileLocation).map(\.?.filePath).map(\.?.last), + allFoundViolations.map(\.location).map(\.?.filePath).map(\.?.last), ["1", "2", "2", "1", "3", "4", "0"] ) XCTAssertNoDifference( @@ -58,15 +58,15 @@ final class LintResultsTests: XCTestCase { let otherLintResults: LintResults = [ Severity.error: [ CheckInfo(id: "1", hint: "hint #1", severity: .warning): [ - Violation(matchedString: "muuh", fileLocation: .init(filePath: "/sample/path4", row: 6, column: 3)), + Violation(matchedString: "muuh", location: .init(filePath: "/sample/path4", row: 6, column: 3)), Violation( - fileLocation: .init(filePath: "/sample/path5"), + location: .init(filePath: "/sample/path5"), appliedAutoCorrection: .init(before: "fusion", after: "wario") ), ], CheckInfo(id: "2", hint: "hint #2 (alternative)", severity: .warning): [], CheckInfo(id: "4", hint: "hint #4", severity: .error): [ - Violation(matchedString: "super", fileLocation: .init(filePath: "/sample/path1", row: 2, column: 200)) + Violation(matchedString: "super", location: .init(filePath: "/sample/path1", row: 2, column: 200)) ], ] ] @@ -81,7 +81,7 @@ final class LintResultsTests: XCTestCase { XCTAssertNoDifference(allFoundViolations.count, 10) XCTAssertNoDifference( - allFoundViolations.map(\.fileLocation).map(\.?.filePath).map(\.?.last), + allFoundViolations.map(\.location).map(\.?.filePath).map(\.?.last), ["1", "2", "2", "4", "5", "1", "1", "3", "4", "0"] ) XCTAssertNoDifference( @@ -104,10 +104,10 @@ final class LintResultsTests: XCTestCase { lintResults.appendViolations( [ - Violation(matchedString: "A", fileLocation: .init(filePath: "/sample/path5", row: 7, column: 7)), - Violation(matchedString: "B", fileLocation: .init(filePath: "/sample/path6", row: 70, column: 70)), + Violation(matchedString: "A", location: .init(filePath: "/sample/path5", row: 7, column: 7)), + Violation(matchedString: "B", location: .init(filePath: "/sample/path6", row: 70, column: 70)), Violation( - fileLocation: .init(filePath: "/sample/path7"), + location: .init(filePath: "/sample/path7"), appliedAutoCorrection: .init(before: "C", after: "D") ), ], @@ -239,17 +239,17 @@ final class LintResultsTests: XCTestCase { ], Severity.warning: [ CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ - Violation(matchedString: "oink2", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), - Violation(matchedString: "boo2", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), + Violation(matchedString: "oink2", location: .init(filePath: "/sample/path1", row: 5, column: 6)), + Violation(matchedString: "boo2", location: .init(filePath: "/sample/path3", row: 50, column: 60)), Violation( - fileLocation: .init(filePath: "/sample/path4"), + location: .init(filePath: "/sample/path4"), appliedAutoCorrection: .init(before: "fool", after: "barl") ), ] ], Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], ] @@ -264,7 +264,7 @@ final class LintResultsTests: XCTestCase { ], Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], ] @@ -285,4 +285,51 @@ final class LintResultsTests: XCTestCase { XCTAssertEqual(LintResults().maxViolationSeverity(excludeAutocorrected: false), nil) } + + func testCodable() throws { + let expectedJsonOutput = """ + { + "warning": { + "1@error: hint for #1": [{ + + }, + { + "appliedAutoCorrection": { + "after": "A", + "before": "AAA" + }, + "matchedString": "A" + }, + { + "location": { + "row": 5, + "column": 2, + "filePath": "/some/path" + }, + "matchedString": "AAA" + } + ] + }, + "info": { + + } + } + """ + + let lintResults: LintResults = [ + .warning: [ + .init(id: "1", hint: "hint for #1"): [ + .init(), + .init(matchedString: "A", appliedAutoCorrection: .init(before: "AAA", after: "A")), + .init(matchedString: "AAA", location: .init(filePath: "/some/path", row: 5, column: 2)), + ] + ], + .info: [:], + ] + let encodedData = try JSONEncoder.iso.encode(lintResults) + XCTAssertNoDifference(String(data: encodedData, encoding: .utf8), expectedJsonOutput) + + let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) + XCTAssertNoDifference(decodedLintResults, lintResults) + } }