diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index d26739c..6284be7 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -131,26 +131,46 @@ public enum Lint { /// Run custom scripts as checks. /// - /// - 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. + /// - 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(check: Check, command: String) throws -> LintResults { let tempScriptFileUrl = URL(fileURLWithPath: "_\(check.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 [check.severity: [check: violations]] + do { + let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) + try FileManager.default.removeItem(at: tempScriptFileUrl) + + 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 [check.severity: [check: violations]] + } + else { + // if the command fails, a ShellOutError will be thrown – here, none is thrown, so no violations + return [check.severity: [check: []]] + } } - else { - return [check.severity: [check: [Violation()]]] + catch { + if let shellOutError = error as? ShellOutError, shellOutError.terminationStatus != 0 { + return [ + check.severity: [ + check: [ + Violation(message: shellOutError.output.components(separatedBy: .newlines).last) + ] + ] + ] + } + + throw error } } diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index a932c99..b585a59 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -14,17 +14,22 @@ public struct Violation: Codable, Equatable { /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? + /// A custom violation message. + public let message: String? + /// Initializes a violation object. public init( discoverDate: Date = Date(), matchedString: String? = nil, location: Location? = nil, - appliedAutoCorrection: AutoCorrection? = nil + appliedAutoCorrection: AutoCorrection? = nil, + message: String? = nil ) { self.discoverDate = discoverDate self.matchedString = matchedString self.location = location self.appliedAutoCorrection = appliedAutoCorrection + self.message = message } } diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index d4e043a..c50c720 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -122,17 +122,18 @@ final class LintTests: XCTestCase { func testRunCustomScript() throws { var lintResults: LintResults = try Lint.runCustomScript( check: .init(id: "1", hint: "hint #1"), - command: """ + command: #""" if which echo > /dev/null; then echo 'Executed custom checks with following result: { "warning": { "A@warning: hint for A": [ - {}, - { "matchedString": "A" }, + { "discoverDate": "2001-01-01T00:00:00Z" }, + { "discoverDate" : "2001-01-01T01:00:00Z", "matchedString": "A" }, { + "discoverDate" : "2001-01-01T02:00:00Z", "matchedString": "AAA", - "location": { "filePath": "/some/path", "row": 5 }, + "location": { "filePath": "\/some\/path", "row": 5 }, "appliedAutoCorrection": { "before": "AAA", "after": "A" } } ] @@ -145,17 +146,76 @@ final class LintTests: XCTestCase { 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") + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), [nil, "A", "AAA"]) + XCTAssertEqual(lintResults.allFoundViolations[2].location?.filePath, "/some/path") + XCTAssertEqual(lintResults.allFoundViolations[2].location?.row, 5) + XCTAssertEqual(lintResults.allFoundViolations[2].appliedAutoCorrection?.after, "A") XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "B") + + lintResults = try Lint.runCustomScript( + check: .init(id: "1", hint: "hint #1", severity: .info), + command: #""" + if which echo > /dev/null; then + echo 'Executed custom check with following violations: + [ + { "discoverDate": "2001-01-01T00:00:00Z" }, + { "discoverDate" : "2001-01-01T01:00:00Z", "matchedString": "A" }, + { + "discoverDate" : "2001-01-01T02:00:00Z", + "matchedString": "AAA", + "location": { "filePath": "\/some\/path", "row": 5 }, + "appliedAutoCorrection": { "before": "AAA", "after": "A" } + } + ] + + Total: 0 errors, 3 warnings, 0 info.' + fi + + """# + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 3) + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), [nil, "A", "AAA"]) + XCTAssertEqual(lintResults.allFoundViolations[2].location?.filePath, "/some/path") + XCTAssertEqual(lintResults.allFoundViolations[2].location?.row, 5) + XCTAssertEqual(lintResults.allFoundViolations[2].appliedAutoCorrection?.after, "A") + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "1") + + lintResults = try Lint.runCustomScript( + check: .init(id: "1", hint: "hint #1", severity: .info), + command: + "echo 'Executed custom check with 100 files.\nCustom check failed, please check file at path /some/path.' && exit 1" + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 1) + XCTAssertNoDifference( + lintResults.allFoundViolations.map(\.message), + ["Custom check failed, please check file at path /some/path."] + ) + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "1") + + lintResults = try Lint.runCustomScript( + check: .init(id: "1", hint: "hint #1", severity: .info), + command: #""" + echo 'Executed custom check with 100 files.\nNo issues found.' && exit 0 + """# + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 0) + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), []) + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "1") } func testValidateParameterCombinations() {