From 8e2244a873bb4460016b402edb7b1e5c6abca25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 27 May 2022 11:37:46 +0200 Subject: [PATCH 01/17] Update formula to version 0.10.1 --- Formula/anylint.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Formula/anylint.rb b/Formula/anylint.rb index 485079b..1e7b376 100644 --- a/Formula/anylint.rb +++ b/Formula/anylint.rb @@ -1,7 +1,7 @@ class Anylint < Formula desc "Lint anything by combining the power of Swift & regular expressions" homepage "https://github.com/FlineDev/AnyLint" - url "https://github.com/FlineDev/AnyLint.git", :tag => "0.10.0", :revision => "5a9c6d2289e1fc4e4f453c04d8a1ec891ea0797d" + url "https://github.com/FlineDev/AnyLint.git", :tag => "0.10.1", :revision => "84ee29f12ae7297e917c9a3339dfb25e5dca6dd5" head "https://github.com/FlineDev/AnyLint.git" depends_on :xcode => ["12.5", :build] From c07f6ecd47af6c1a2b42a3791ccbaa7065ec7245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 3 Jun 2022 19:21:05 +0200 Subject: [PATCH 02/17] Add auto-updating DocC Documentation support via swiftpackageindex.com See here: https://blog.swiftpackageindex.com/posts/auto-generating-auto-hosting-and-auto-updating-docc-documentation/ --- .spi.yml | 5 +++++ Package.swift | 7 +++++++ Sources/AnyLint/Statistics.swift | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .spi.yml diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..9825b4d --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - platform: macos + documentation_targets: [AnyLint] diff --git a/Package.swift b/Package.swift index 70b284a..6749d84 100644 --- a/Package.swift +++ b/Package.swift @@ -39,3 +39,10 @@ let package = Package( ) ] ) + +#if swift(>=5.6) + // Add the documentation compiler plugin if possible + package.dependencies.append( + .package(url: “https://github.com/apple/swift-docc-plugin“, from: “1.0.0“) + ) +#endif diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index c2f4c05..861cda7 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -91,7 +91,7 @@ final class Statistics { for (check, executionTime) in self.executionTimePerCheck.sorted(by: { $0.value > $1.value }) { let milliseconds = Int(executionTime * 1000) - log.message("\(milliseconds)ms\t\t\(check.id)", level: .info) + log.message("\(milliseconds)ms\t\(check.id)", level: .info) } } From ded7ad9d2e24a6e968f2061f25dcdd157ce4c6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 4 Jun 2022 19:10:04 +0200 Subject: [PATCH 03/17] Fix wrong quote marks in Package.swift for DocC plugin support --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 6749d84..e0fcdcf 100644 --- a/Package.swift +++ b/Package.swift @@ -43,6 +43,6 @@ let package = Package( #if swift(>=5.6) // Add the documentation compiler plugin if possible package.dependencies.append( - .package(url: “https://github.com/apple/swift-docc-plugin“, from: “1.0.0“) + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") ) #endif From 48bee534a801cf32db0f597b3c6962353512e7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 4 Jun 2022 19:39:11 +0200 Subject: [PATCH 04/17] Remove explicit platform specification from .spi.yml --- .spi.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.spi.yml b/.spi.yml index 9825b4d..1faca5c 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,5 +1,4 @@ version: 1 builder: configs: - - platform: macos - documentation_targets: [AnyLint] + - documentation_targets: [AnyLint] From 6c2b94073d4faa38554f8e1923161e01ac211608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 9 Jun 2022 14:36:20 +0200 Subject: [PATCH 05/17] Remove DocC compiler plugin section (no longer needed) --- Package.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Package.swift b/Package.swift index e0fcdcf..70b284a 100644 --- a/Package.swift +++ b/Package.swift @@ -39,10 +39,3 @@ let package = Package( ) ] ) - -#if swift(>=5.6) - // Add the documentation compiler plugin if possible - package.dependencies.append( - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") - ) -#endif From 6ea226d2adbe1a3ea2add050deecbbd976373fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 01:15:43 +0200 Subject: [PATCH 06/17] Use 3 spaces for indentation instead of 4 --- Sources/AnyLint/AutoCorrection.swift | 152 ++-- Sources/AnyLint/CheckInfo.swift | 110 +-- Sources/AnyLint/Checkers/Checker.swift | 2 +- .../Checkers/FileContentsChecker.swift | 256 +++---- .../AnyLint/Checkers/FilePathsChecker.swift | 86 +-- Sources/AnyLint/Extensions/ArrayExt.swift | 12 +- .../AnyLint/Extensions/FileManagerExt.swift | 70 +- Sources/AnyLint/Extensions/StringExt.swift | 48 +- Sources/AnyLint/Extensions/URLExt.swift | 8 +- Sources/AnyLint/FilesSearch.swift | 186 ++--- Sources/AnyLint/Lint.swift | 488 ++++++------- Sources/AnyLint/Options.swift | 2 +- Sources/AnyLint/Severity.swift | 80 +-- Sources/AnyLint/Statistics.swift | 320 ++++----- Sources/AnyLint/Violation.swift | 72 +- Sources/AnyLint/ViolationLocationConfig.swift | 58 +- .../AnyLintCLI/Commands/SingleCommand.swift | 158 ++-- .../BlankTemplate.swift | 18 +- .../ConfigurationTemplate.swift | 20 +- Sources/AnyLintCLI/Globals/CLIConstants.swift | 90 +-- .../AnyLintCLI/Globals/ValidateOrFail.swift | 44 +- Sources/AnyLintCLI/Tasks/InitTask.swift | 74 +- Sources/AnyLintCLI/Tasks/LintTask.swift | 100 +-- Sources/AnyLintCLI/Tasks/TaskHandler.swift | 2 +- Sources/AnyLintCLI/Tasks/VersionTask.swift | 6 +- Sources/Utility/Constants.swift | 58 +- .../Utility/Extensions/CollectionExt.swift | 8 +- .../Utility/Extensions/FileManagerExt.swift | 20 +- Sources/Utility/Extensions/RegexExt.swift | 132 ++-- Sources/Utility/Extensions/StringExt.swift | 98 +-- Sources/Utility/Logger.swift | 306 ++++---- Sources/Utility/Regex.swift | 514 +++++++------- Sources/Utility/TestHelper.swift | 34 +- Tests/AnyLintCLITests/AnyLintCLITests.swift | 6 +- Tests/AnyLintTests/AutoCorrectionTests.swift | 64 +- Tests/AnyLintTests/CheckInfoTests.swift | 56 +- .../Checkers/FileContentsCheckerTests.swift | 428 +++++------ .../Checkers/FilePathsCheckerTests.swift | 142 ++-- .../Extensions/ArrayExtTests.swift | 26 +- .../Extensions/XCTestCaseExt.swift | 38 +- Tests/AnyLintTests/FilesSearchTests.swift | 104 +-- Tests/AnyLintTests/LintTests.swift | 224 +++--- Tests/AnyLintTests/RegexExtTests.swift | 24 +- Tests/AnyLintTests/StatisticsTests.swift | 232 +++--- Tests/AnyLintTests/ViolationTests.swift | 42 +- Tests/LinuxMain.swift | 102 +-- .../Extensions/RegexExtTests.swift | 94 +-- Tests/UtilityTests/LoggerTests.swift | 38 +- lint.swift | 672 +++++++++--------- 49 files changed, 2962 insertions(+), 2962 deletions(-) diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift index eff4213..b5e40d2 100644 --- a/Sources/AnyLint/AutoCorrection.swift +++ b/Sources/AnyLint/AutoCorrection.swift @@ -3,89 +3,89 @@ import Utility /// Information about an autocorrection. public struct AutoCorrection { - /// The matching text before applying the autocorrection. - public let before: String - - /// The matching text after applying the autocorrection. - public let after: String - - var appliedMessageLines: [String] { - if useDiffOutput, #available(OSX 10.15, *) { - var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] - - let beforeLines = before.components(separatedBy: .newlines) - let afterLines = after.components(separatedBy: .newlines) - - for difference in afterLines.difference(from: beforeLines).sorted() { - switch difference { - case let .insert(offset, element, _): - lines.append("+ [L\(offset + 1)] \(element)".green) - - case let .remove(offset, element, _): - lines.append("- [L\(offset + 1)] \(element)".red) - } + /// The matching text before applying the autocorrection. + public let before: String + + /// The matching text after applying the autocorrection. + public let after: String + + var appliedMessageLines: [String] { + if useDiffOutput, #available(OSX 10.15, *) { + var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] + + let beforeLines = before.components(separatedBy: .newlines) + let afterLines = after.components(separatedBy: .newlines) + + for difference in afterLines.difference(from: beforeLines).sorted() { + switch difference { + case let .insert(offset, element, _): + lines.append("+ [L\(offset + 1)] \(element)".green) + + case let .remove(offset, element, _): + lines.append("- [L\(offset + 1)] \(element)".red) } - - return lines - } else { - return [ - "Autocorrection applied, the diff is: (+ added, - removed)", - "- \(before.showWhitespacesAndNewlines())".red, - "+ \(after.showWhitespacesAndNewlines())".green, - ] - } - } - - var useDiffOutput: Bool { - before.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing || - after.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing - } - - /// Initializes an autocorrection. - public init(before: String, after: String) { - self.before = before - self.after = after - } + } + + return lines + } else { + return [ + "Autocorrection applied, the diff is: (+ added, - removed)", + "- \(before.showWhitespacesAndNewlines())".red, + "+ \(after.showWhitespacesAndNewlines())".green, + ] + } + } + + var useDiffOutput: Bool { + before.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing || + after.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing + } + + /// Initializes an autocorrection. + public init(before: String, after: String) { + self.before = before + self.after = after + } } extension AutoCorrection: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, String)...) { - guard - let before = elements.first(where: { $0.0 == "before" })?.1, - let after = elements.first(where: { $0.0 == "after" })?.1 - else { - log.message("Failed to convert Dictionary literal '\(elements)' to type AutoCorrection.", level: .error) - log.exit(status: .failure) - exit(EXIT_FAILURE) // only reachable in unit tests - } - - self = AutoCorrection(before: before, after: after) - } + public init(dictionaryLiteral elements: (String, String)...) { + guard + let before = elements.first(where: { $0.0 == "before" })?.1, + let after = elements.first(where: { $0.0 == "after" })?.1 + else { + log.message("Failed to convert Dictionary literal '\(elements)' to type AutoCorrection.", level: .error) + log.exit(status: .failure) + exit(EXIT_FAILURE) // only reachable in unit tests + } + + self = AutoCorrection(before: before, after: after) + } } // TODO: make the autocorrection diff sorted by line number @available(OSX 10.15, *) extension CollectionDifference.Change: Comparable where ChangeElement == String { - public static func < (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case let (.remove(leftOffset, _, _), .remove(rightOffset, _, _)), let (.insert(leftOffset, _, _), .insert(rightOffset, _, _)): - return leftOffset < rightOffset - - case let (.remove(leftOffset, _, _), .insert(rightOffset, _, _)): - return leftOffset < rightOffset || true - - case let (.insert(leftOffset, _, _), .remove(rightOffset, _, _)): - return leftOffset < rightOffset || false - } - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case let (.remove(leftOffset, _, _), .remove(rightOffset, _, _)), let (.insert(leftOffset, _, _), .insert(rightOffset, _, _)): - return leftOffset == rightOffset - - case (.remove, .insert), (.insert, .remove): - return false - } - } + public static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.remove(leftOffset, _, _), .remove(rightOffset, _, _)), let (.insert(leftOffset, _, _), .insert(rightOffset, _, _)): + return leftOffset < rightOffset + + case let (.remove(leftOffset, _, _), .insert(rightOffset, _, _)): + return leftOffset < rightOffset || true + + case let (.insert(leftOffset, _, _), .remove(rightOffset, _, _)): + return leftOffset < rightOffset || false + } + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.remove(leftOffset, _, _), .remove(rightOffset, _, _)), let (.insert(leftOffset, _, _), .insert(rightOffset, _, _)): + return leftOffset == rightOffset + + case (.remove, .insert), (.insert, .remove): + return false + } + } } diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index d1a705a..3bbaf14 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -3,77 +3,77 @@ import Utility /// Provides some basic information needed in each lint check. public struct CheckInfo { - /// The identifier of the check defined here. Can be used when defining exceptions within files for specific lint checks. - public let id: String + /// The identifier of the check defined here. Can be used when defining exceptions within files for specific lint checks. + public let id: String - /// The hint to be shown as guidance on what the issue is and how to fix it. Can reference any capture groups in the first regex parameter (e.g. `contentRegex`). - public let hint: String + /// The hint to be shown as guidance on what the issue is and how to fix it. Can reference any capture groups in the first regex parameter (e.g. `contentRegex`). + public let hint: String - /// The severity level for the report in case the check fails. - public let severity: Severity + /// The severity level for the report in case the check fails. + public let severity: Severity - /// Initializes a new info object for the lint check. - public init(id: String, hint: String, severity: Severity = .warning) { - self.id = id - self.hint = hint - self.severity = severity - } + /// Initializes a new info object for the lint check. + public init(id: String, hint: String, severity: Severity = .warning) { + self.id = id + self.hint = hint + self.severity = severity + } } extension CheckInfo: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } extension CheckInfo: CustomStringConvertible { - public var description: String { - "check '\(id)'" - } + public var description: String { + "check '\(id)'" + } } extension CheckInfo: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - let customSeverityRegex: Regex = [ - "id": #"^[^@:]+"#, - "severitySeparator": #"@"#, - "severity": #"[^:]+"#, - "hintSeparator": #": ?"#, - "hint": #".*$"#, - ] + public init(stringLiteral value: String) { + let customSeverityRegex: Regex = [ + "id": #"^[^@:]+"#, + "severitySeparator": #"@"#, + "severity": #"[^:]+"#, + "hintSeparator": #": ?"#, + "hint": #".*$"#, + ] - if let customSeverityMatch = customSeverityRegex.firstMatch(in: value) { - let id = customSeverityMatch.captures[0]! - let severityString = customSeverityMatch.captures[2]! - let hint = customSeverityMatch.captures[4]! + if let customSeverityMatch = customSeverityRegex.firstMatch(in: value) { + let id = customSeverityMatch.captures[0]! + let severityString = customSeverityMatch.captures[2]! + let hint = customSeverityMatch.captures[4]! - guard let severity = Severity.from(string: severityString) else { - log.message("Specified severity '\(severityString)' for check '\(id)' unknown. Use one of [error, warning, info].", level: .error) - log.exit(status: .failure) - exit(EXIT_FAILURE) // only reachable in unit tests - } + guard let severity = Severity.from(string: severityString) else { + log.message("Specified severity '\(severityString)' for check '\(id)' unknown. Use one of [error, warning, info].", level: .error) + log.exit(status: .failure) + exit(EXIT_FAILURE) // only reachable in unit tests + } - self = CheckInfo(id: id, hint: hint, severity: severity) - } else { - let defaultSeverityRegex: Regex = [ - "id": #"^[^@:]+"#, - "hintSeparator": #": ?"#, - "hint": #".*$"#, - ] + self = CheckInfo(id: id, hint: hint, severity: severity) + } else { + let defaultSeverityRegex: Regex = [ + "id": #"^[^@:]+"#, + "hintSeparator": #": ?"#, + "hint": #".*$"#, + ] - guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: value) else { - log.message( - "Could not convert String literal '\(value)' to type CheckInfo. Please check the structure to be: (@): ", - level: .error - ) - log.exit(status: .failure) - exit(EXIT_FAILURE) // only reachable in unit tests - } + guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: value) else { + log.message( + "Could not convert String literal '\(value)' to type CheckInfo. Please check the structure to be: (@): ", + level: .error + ) + log.exit(status: .failure) + exit(EXIT_FAILURE) // only reachable in unit tests + } - let id = defaultSeverityMatch.captures[0]! - let hint = defaultSeverityMatch.captures[2]! + let id = defaultSeverityMatch.captures[0]! + let hint = defaultSeverityMatch.captures[2]! - self = CheckInfo(id: id, hint: hint) - } - } + self = CheckInfo(id: id, hint: hint) + } + } } diff --git a/Sources/AnyLint/Checkers/Checker.swift b/Sources/AnyLint/Checkers/Checker.swift index d0c0f56..91b8ffe 100644 --- a/Sources/AnyLint/Checkers/Checker.swift +++ b/Sources/AnyLint/Checkers/Checker.swift @@ -1,5 +1,5 @@ import Foundation protocol Checker { - func performCheck() throws -> [Violation] + func performCheck() throws -> [Violation] } diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index e7a1878..822a25c 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -2,136 +2,136 @@ import Foundation import Utility struct FileContentsChecker { - let checkInfo: CheckInfo - let regex: Regex - let violationLocation: ViolationLocationConfig - let filePathsToCheck: [String] - let autoCorrectReplacement: String? - let repeatIfAutoCorrected: Bool + let checkInfo: CheckInfo + let regex: Regex + let violationLocation: ViolationLocationConfig + let filePathsToCheck: [String] + let autoCorrectReplacement: String? + let repeatIfAutoCorrected: Bool } extension FileContentsChecker: Checker { - func performCheck() throws -> [Violation] { // swiftlint:disable:this function_body_length - log.message("Start checking \(checkInfo) ...", level: .debug) - var violations: [Violation] = [] - - for filePath in filePathsToCheck.reversed() { - log.message("Start reading contents of file at \(filePath) ...", level: .debug) - - if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { - var newFileContents: String = fileContents - let linesInFile: [String] = fileContents.components(separatedBy: .newlines) - - // skip check in file if contains `AnyLint.skipInFile: ` - let skipInFileRegex = try Regex(#"AnyLint\.skipInFile:[^\n]*([, ]All[,\s]|[, ]\#(checkInfo.id)[,\s])"#) - guard !skipInFileRegex.matches(fileContents) else { - log.message("Skipping \(checkInfo) in file \(filePath) due to 'AnyLint.skipInFile' instruction ...", level: .debug) - continue - } - - let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(checkInfo.id)"#) - - for match in regex.matches(in: fileContents).reversed() { - let locationInfo: String.LocationInfo - - switch self.violationLocation.range { - case .fullMatch: - switch self.violationLocation.bound { - case .lower: - locationInfo = fileContents.locationInfo(of: match.range.lowerBound) - - case .upper: - locationInfo = fileContents.locationInfo(of: match.range.upperBound) - } - - case .captureGroup(let index): - let capture = match.captures[index]! - let captureRange = NSRange(match.string.range(of: capture)!, in: match.string) - - switch self.violationLocation.bound { - case .lower: - locationInfo = fileContents.locationInfo( - of: fileContents.index(match.range.lowerBound, offsetBy: captureRange.location) - ) - - case .upper: - locationInfo = fileContents.locationInfo( - of: fileContents.index(match.range.lowerBound, offsetBy: captureRange.location + captureRange.length) - ) - } - } - - log.message("Found violating match at \(locationInfo) ...", level: .debug) - - // skip found match if contains `AnyLint.skipHere: ` in same line or one line before - guard !linesInFile.containsLine(at: [locationInfo.line - 2, locationInfo.line - 1], matchingRegex: skipHereRegex) else { - log.message("Skip reporting last match due to 'AnyLint.skipHere' instruction ...", level: .debug) - continue - } - - let autoCorrection: AutoCorrection? = { - guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } - - let newMatchString = regex.replaceAllCaptures(in: match.string, with: autoCorrectReplacement) - return AutoCorrection(before: match.string, after: newMatchString) - }() - - if let autoCorrection = autoCorrection { - guard match.string != autoCorrection.after else { - // can skip auto-correction & violation reporting because auto-correct replacement is equal to matched string - continue - } - - // apply auto correction - newFileContents.replaceSubrange(match.range, with: autoCorrection.after) - log.message("Applied autocorrection for last match ...", level: .debug) - } - - log.message("Reporting violation for \(checkInfo) in file \(filePath) at \(locationInfo) ...", level: .debug) - violations.append( - Violation( - checkInfo: checkInfo, - filePath: filePath, - matchedString: match.string, - locationInfo: locationInfo, - appliedAutoCorrection: autoCorrection - ) - ) - } - - if newFileContents != fileContents { - log.message("Rewriting contents of file \(filePath) due to autocorrection changes ...", level: .debug) - try newFileContents.write(toFile: filePath, atomically: true, encoding: .utf8) - } - } else { - log.message( - "Could not read contents of file at \(filePath). Make sure it is a text file and is formatted as UTF8.", - level: .warning - ) + func performCheck() throws -> [Violation] { // swiftlint:disable:this function_body_length + log.message("Start checking \(checkInfo) ...", level: .debug) + var violations: [Violation] = [] + + for filePath in filePathsToCheck.reversed() { + log.message("Start reading contents of file at \(filePath) ...", level: .debug) + + if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { + var newFileContents: String = fileContents + let linesInFile: [String] = fileContents.components(separatedBy: .newlines) + + // skip check in file if contains `AnyLint.skipInFile: ` + let skipInFileRegex = try Regex(#"AnyLint\.skipInFile:[^\n]*([, ]All[,\s]|[, ]\#(checkInfo.id)[,\s])"#) + guard !skipInFileRegex.matches(fileContents) else { + log.message("Skipping \(checkInfo) in file \(filePath) due to 'AnyLint.skipInFile' instruction ...", level: .debug) + continue } - - Statistics.shared.checkedFiles(at: [filePath]) - } - - violations = violations.reversed() - - if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { - log.message("Repeating check \(checkInfo) because auto-corrections were applied on last run.", level: .debug) - - // only paths where auto-corrections were applied need to be re-checked - let filePathsToReCheck = Array(Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! })).sorted() - - let violationsOnRechecks = try FileContentsChecker( - checkInfo: checkInfo, - regex: regex, - violationLocation: self.violationLocation, - filePathsToCheck: filePathsToReCheck, - autoCorrectReplacement: autoCorrectReplacement, - repeatIfAutoCorrected: repeatIfAutoCorrected - ).performCheck() - violations.append(contentsOf: violationsOnRechecks) - } - - return violations - } + + let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(checkInfo.id)"#) + + for match in regex.matches(in: fileContents).reversed() { + let locationInfo: String.LocationInfo + + switch self.violationLocation.range { + case .fullMatch: + switch self.violationLocation.bound { + case .lower: + locationInfo = fileContents.locationInfo(of: match.range.lowerBound) + + case .upper: + locationInfo = fileContents.locationInfo(of: match.range.upperBound) + } + + case .captureGroup(let index): + let capture = match.captures[index]! + let captureRange = NSRange(match.string.range(of: capture)!, in: match.string) + + switch self.violationLocation.bound { + case .lower: + locationInfo = fileContents.locationInfo( + of: fileContents.index(match.range.lowerBound, offsetBy: captureRange.location) + ) + + case .upper: + locationInfo = fileContents.locationInfo( + of: fileContents.index(match.range.lowerBound, offsetBy: captureRange.location + captureRange.length) + ) + } + } + + log.message("Found violating match at \(locationInfo) ...", level: .debug) + + // skip found match if contains `AnyLint.skipHere: ` in same line or one line before + guard !linesInFile.containsLine(at: [locationInfo.line - 2, locationInfo.line - 1], matchingRegex: skipHereRegex) else { + log.message("Skip reporting last match due to 'AnyLint.skipHere' instruction ...", level: .debug) + continue + } + + let autoCorrection: AutoCorrection? = { + guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } + + let newMatchString = regex.replaceAllCaptures(in: match.string, with: autoCorrectReplacement) + return AutoCorrection(before: match.string, after: newMatchString) + }() + + if let autoCorrection = autoCorrection { + guard match.string != autoCorrection.after else { + // can skip auto-correction & violation reporting because auto-correct replacement is equal to matched string + continue + } + + // apply auto correction + newFileContents.replaceSubrange(match.range, with: autoCorrection.after) + log.message("Applied autocorrection for last match ...", level: .debug) + } + + log.message("Reporting violation for \(checkInfo) in file \(filePath) at \(locationInfo) ...", level: .debug) + violations.append( + Violation( + checkInfo: checkInfo, + filePath: filePath, + matchedString: match.string, + locationInfo: locationInfo, + appliedAutoCorrection: autoCorrection + ) + ) + } + + if newFileContents != fileContents { + log.message("Rewriting contents of file \(filePath) due to autocorrection changes ...", level: .debug) + try newFileContents.write(toFile: filePath, atomically: true, encoding: .utf8) + } + } else { + log.message( + "Could not read contents of file at \(filePath). Make sure it is a text file and is formatted as UTF8.", + level: .warning + ) + } + + Statistics.shared.checkedFiles(at: [filePath]) + } + + violations = violations.reversed() + + if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { + log.message("Repeating check \(checkInfo) because auto-corrections were applied on last run.", level: .debug) + + // only paths where auto-corrections were applied need to be re-checked + let filePathsToReCheck = Array(Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! })).sorted() + + let violationsOnRechecks = try FileContentsChecker( + checkInfo: checkInfo, + regex: regex, + violationLocation: self.violationLocation, + filePathsToCheck: filePathsToReCheck, + autoCorrectReplacement: autoCorrectReplacement, + repeatIfAutoCorrected: repeatIfAutoCorrected + ).performCheck() + violations.append(contentsOf: violationsOnRechecks) + } + + return violations + } } diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index c88c77f..ac2f6e7 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -2,51 +2,51 @@ import Foundation import Utility struct FilePathsChecker { - let checkInfo: CheckInfo - let regex: Regex - let filePathsToCheck: [String] - let autoCorrectReplacement: String? - let violateIfNoMatchesFound: Bool + let checkInfo: CheckInfo + let regex: Regex + let filePathsToCheck: [String] + let autoCorrectReplacement: String? + let violateIfNoMatchesFound: Bool } extension FilePathsChecker: Checker { - func performCheck() throws -> [Violation] { - var violations: [Violation] = [] - - if violateIfNoMatchesFound { - let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count - if matchingFilePathsCount <= 0 { - log.message("Reporting violation for \(checkInfo) as no matching file was found ...", level: .debug) - violations.append( - Violation(checkInfo: checkInfo, filePath: nil, locationInfo: nil, appliedAutoCorrection: nil) - ) + func performCheck() throws -> [Violation] { + var violations: [Violation] = [] + + if violateIfNoMatchesFound { + let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count + if matchingFilePathsCount <= 0 { + log.message("Reporting violation for \(checkInfo) as no matching file was found ...", level: .debug) + violations.append( + Violation(checkInfo: checkInfo, filePath: nil, locationInfo: nil, appliedAutoCorrection: nil) + ) + } + } else { + for filePath in filePathsToCheck where regex.matches(filePath) { + log.message("Found violating match for \(checkInfo) ...", level: .debug) + + let appliedAutoCorrection: AutoCorrection? = try { + guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } + + let newFilePath = regex.replaceAllCaptures(in: filePath, with: autoCorrectReplacement) + try fileManager.moveFileSafely(from: filePath, to: newFilePath) + + return AutoCorrection(before: filePath, after: newFilePath) + }() + + if appliedAutoCorrection != nil { + log.message("Applied autocorrection for last match ...", level: .debug) } - } else { - for filePath in filePathsToCheck where regex.matches(filePath) { - log.message("Found violating match for \(checkInfo) ...", level: .debug) - - let appliedAutoCorrection: AutoCorrection? = try { - guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } - - let newFilePath = regex.replaceAllCaptures(in: filePath, with: autoCorrectReplacement) - try fileManager.moveFileSafely(from: filePath, to: newFilePath) - - return AutoCorrection(before: filePath, after: newFilePath) - }() - - if appliedAutoCorrection != nil { - log.message("Applied autocorrection for last match ...", level: .debug) - } - - log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) - violations.append( - Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil, appliedAutoCorrection: appliedAutoCorrection) - ) - } - - Statistics.shared.checkedFiles(at: filePathsToCheck) - } - - return violations - } + + log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) + violations.append( + Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil, appliedAutoCorrection: appliedAutoCorrection) + ) + } + + Statistics.shared.checkedFiles(at: filePathsToCheck) + } + + return violations + } } diff --git a/Sources/AnyLint/Extensions/ArrayExt.swift b/Sources/AnyLint/Extensions/ArrayExt.swift index 6067e9f..d572be4 100644 --- a/Sources/AnyLint/Extensions/ArrayExt.swift +++ b/Sources/AnyLint/Extensions/ArrayExt.swift @@ -1,10 +1,10 @@ import Foundation extension Array where Element == String { - func containsLine(at indexes: [Int], matchingRegex regex: Regex) -> Bool { - indexes.contains { index in - guard index >= 0, index < count else { return false } - return regex.matches(self[index]) - } - } + func containsLine(at indexes: [Int], matchingRegex regex: Regex) -> Bool { + indexes.contains { index in + guard index >= 0, index < count else { return false } + return regex.matches(self[index]) + } + } } diff --git a/Sources/AnyLint/Extensions/FileManagerExt.swift b/Sources/AnyLint/Extensions/FileManagerExt.swift index b6ec5f2..7e21940 100644 --- a/Sources/AnyLint/Extensions/FileManagerExt.swift +++ b/Sources/AnyLint/Extensions/FileManagerExt.swift @@ -2,39 +2,39 @@ import Foundation import Utility extension FileManager { - /// Moves a file from one path to another, making sure that all directories are created and no files are overwritten. - public func moveFileSafely(from sourcePath: String, to targetPath: String) throws { - guard fileExists(atPath: sourcePath) else { - log.message("No file found at \(sourcePath) to move.", level: .error) - log.exit(status: .failure) - return // only reachable in unit tests - } - - guard !fileExists(atPath: targetPath) || sourcePath.lowercased() == targetPath.lowercased() else { - log.message("File already exists at target path \(targetPath) – can't move from \(sourcePath).", level: .warning) - return - } - - let targetParentDirectoryPath = targetPath.parentDirectoryPath - if !fileExists(atPath: targetParentDirectoryPath) { - try createDirectory(atPath: targetParentDirectoryPath, withIntermediateDirectories: true, attributes: nil) - } - - guard fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { - log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) - log.exit(status: .failure) - return // only reachable in unit tests - } - - if sourcePath.lowercased() == targetPath.lowercased() { - // workaround issues on case insensitive file systems - let temporaryTargetPath = targetPath + UUID().uuidString - try moveItem(atPath: sourcePath, toPath: temporaryTargetPath) - try moveItem(atPath: temporaryTargetPath, toPath: targetPath) - } else { - try moveItem(atPath: sourcePath, toPath: targetPath) - } - - FilesSearch.shared.invalidateCache() - } + /// Moves a file from one path to another, making sure that all directories are created and no files are overwritten. + public func moveFileSafely(from sourcePath: String, to targetPath: String) throws { + guard fileExists(atPath: sourcePath) else { + log.message("No file found at \(sourcePath) to move.", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + guard !fileExists(atPath: targetPath) || sourcePath.lowercased() == targetPath.lowercased() else { + log.message("File already exists at target path \(targetPath) – can't move from \(sourcePath).", level: .warning) + return + } + + let targetParentDirectoryPath = targetPath.parentDirectoryPath + if !fileExists(atPath: targetParentDirectoryPath) { + try createDirectory(atPath: targetParentDirectoryPath, withIntermediateDirectories: true, attributes: nil) + } + + guard fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { + log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + if sourcePath.lowercased() == targetPath.lowercased() { + // workaround issues on case insensitive file systems + let temporaryTargetPath = targetPath + UUID().uuidString + try moveItem(atPath: sourcePath, toPath: temporaryTargetPath) + try moveItem(atPath: temporaryTargetPath, toPath: targetPath) + } else { + try moveItem(atPath: sourcePath, toPath: targetPath) + } + + FilesSearch.shared.invalidateCache() + } } diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 7bcc39d..269eeb3 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -5,28 +5,28 @@ import Utility public typealias Regex = Utility.Regex extension String { - /// Info about the exact location of a character in a given file. - public typealias LocationInfo = (line: Int, charInLine: Int) - - /// Returns the location info for a given line index. - public func locationInfo(of index: String.Index) -> LocationInfo { - let prefix = self[startIndex ..< index] - let prefixLines = prefix.components(separatedBy: .newlines) - guard let lastPrefixLine = prefixLines.last else { return (line: 1, charInLine: 1) } - - let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 - return (line: prefixLines.count, charInLine: charInLine) - } - - func showNewlines() -> String { - components(separatedBy: .newlines).joined(separator: #"\n"#) - } - - func showWhitespaces() -> String { - components(separatedBy: .whitespaces).joined(separator: "␣") - } - - func showWhitespacesAndNewlines() -> String { - showNewlines().showWhitespaces() - } + /// Info about the exact location of a character in a given file. + public typealias LocationInfo = (line: Int, charInLine: Int) + + /// Returns the location info for a given line index. + public func locationInfo(of index: String.Index) -> LocationInfo { + let prefix = self[startIndex ..< index] + let prefixLines = prefix.components(separatedBy: .newlines) + guard let lastPrefixLine = prefixLines.last else { return (line: 1, charInLine: 1) } + + let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 + return (line: prefixLines.count, charInLine: charInLine) + } + + func showNewlines() -> String { + components(separatedBy: .newlines).joined(separator: #"\n"#) + } + + func showWhitespaces() -> String { + components(separatedBy: .whitespaces).joined(separator: "␣") + } + + func showWhitespacesAndNewlines() -> String { + showNewlines().showWhitespaces() + } } diff --git a/Sources/AnyLint/Extensions/URLExt.swift b/Sources/AnyLint/Extensions/URLExt.swift index 67f5394..7f7316f 100644 --- a/Sources/AnyLint/Extensions/URLExt.swift +++ b/Sources/AnyLint/Extensions/URLExt.swift @@ -2,8 +2,8 @@ import Foundation import Utility extension URL { - /// Returns the relative path of from the current path. - public var relativePathFromCurrent: String { - String(path.replacingOccurrences(of: fileManager.currentDirectoryPath, with: "").dropFirst()) - } + /// Returns the relative path of from the current path. + public var relativePathFromCurrent: String { + String(path.replacingOccurrences(of: fileManager.currentDirectoryPath, with: "").dropFirst()) + } } diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index e100fca..1c11efa 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -3,101 +3,101 @@ import Utility /// Helper to search for files and filter using Regexes. public final class FilesSearch { - struct SearchOptions: Equatable, Hashable { - let pathToSearch: String - let includeFilters: [Regex] - let excludeFilters: [Regex] - } - - /// The shared instance. - public static let shared = FilesSearch() - - private var cachedFilePaths: [SearchOptions: [String]] = [:] - - private init() {} - - /// Should be called whenever files within the current directory are renamed, moved, added or deleted. - func invalidateCache() { - cachedFilePaths = [:] - } - - /// Returns all file paths within given `path` matching the given `include` and `exclude` filters. - public func allFiles( // swiftlint:disable:this function_body_length - within path: String, - includeFilters: [Regex], - excludeFilters: [Regex] = [] - ) -> [String] { - log.message( - "Start searching for matching files in path \(path) with includeFilters \(includeFilters) and excludeFilters \(excludeFilters) ...", - level: .debug - ) - - let searchOptions = SearchOptions(pathToSearch: path, includeFilters: includeFilters, excludeFilters: excludeFilters) - if let cachedFilePaths: [String] = cachedFilePaths[searchOptions] { - log.message("A file search with exactly the above search options was already done and was not invalidated, using cached results ...", level: .debug) - return cachedFilePaths - } - - guard let url = URL(string: path, relativeTo: fileManager.currentDirectoryUrl) else { - log.message("Could not convert path '\(path)' to type URL.", level: .error) + struct SearchOptions: Equatable, Hashable { + let pathToSearch: String + let includeFilters: [Regex] + let excludeFilters: [Regex] + } + + /// The shared instance. + public static let shared = FilesSearch() + + private var cachedFilePaths: [SearchOptions: [String]] = [:] + + private init() {} + + /// Should be called whenever files within the current directory are renamed, moved, added or deleted. + func invalidateCache() { + cachedFilePaths = [:] + } + + /// Returns all file paths within given `path` matching the given `include` and `exclude` filters. + public func allFiles( // swiftlint:disable:this function_body_length + within path: String, + includeFilters: [Regex], + excludeFilters: [Regex] = [] + ) -> [String] { + log.message( + "Start searching for matching files in path \(path) with includeFilters \(includeFilters) and excludeFilters \(excludeFilters) ...", + level: .debug + ) + + let searchOptions = SearchOptions(pathToSearch: path, includeFilters: includeFilters, excludeFilters: excludeFilters) + if let cachedFilePaths: [String] = cachedFilePaths[searchOptions] { + log.message("A file search with exactly the above search options was already done and was not invalidated, using cached results ...", level: .debug) + return cachedFilePaths + } + + guard let url = URL(string: path, relativeTo: fileManager.currentDirectoryUrl) else { + log.message("Could not convert path '\(path)' to type URL.", level: .error) + log.exit(status: .failure) + return [] // only reachable in unit tests + } + + let propKeys = [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey] + guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: propKeys, options: [], errorHandler: nil) else { + log.message("Couldn't create enumerator for path '\(path)'.", level: .error) + log.exit(status: .failure) + return [] // only reachable in unit tests + } + + var filePaths: [String] = [] + + for case let fileUrl as URL in enumerator { + guard + let resourceValues = try? fileUrl.resourceValues(forKeys: [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey]), + let isHiddenFilePath = resourceValues.isHidden, + let isRegularFilePath = resourceValues.isRegularFile + else { + log.message("Could not read resource values for file at \(fileUrl.path)", level: .error) log.exit(status: .failure) return [] // only reachable in unit tests - } - - let propKeys = [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey] - guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: propKeys, options: [], errorHandler: nil) else { - log.message("Couldn't create enumerator for path '\(path)'.", level: .error) - log.exit(status: .failure) - return [] // only reachable in unit tests - } - - var filePaths: [String] = [] - - for case let fileUrl as URL in enumerator { - guard - let resourceValues = try? fileUrl.resourceValues(forKeys: [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey]), - let isHiddenFilePath = resourceValues.isHidden, - let isRegularFilePath = resourceValues.isRegularFile - else { - log.message("Could not read resource values for file at \(fileUrl.path)", level: .error) - log.exit(status: .failure) - return [] // only reachable in unit tests + } + + // skip if any exclude filter applies + if excludeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { + if !isRegularFilePath { + enumerator.skipDescendants() } - - // skip if any exclude filter applies - if excludeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { - if !isRegularFilePath { - enumerator.skipDescendants() - } - - continue + + continue + } + + // skip hidden files and directories +#if os(Linux) + if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { + if !isRegularFilePath { + enumerator.skipDescendants() } - - // skip hidden files and directories - #if os(Linux) - if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { - if !isRegularFilePath { - enumerator.skipDescendants() - } - - continue - } - #else - if isHiddenFilePath { - if !isRegularFilePath { - enumerator.skipDescendants() - } - - continue - } - #endif - - guard isRegularFilePath, includeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) else { continue } - - filePaths.append(fileUrl.relativePathFromCurrent) - } - - cachedFilePaths[searchOptions] = filePaths - return filePaths - } + + continue + } +#else + if isHiddenFilePath { + if !isRegularFilePath { + enumerator.skipDescendants() + } + + continue + } +#endif + + guard isRegularFilePath, includeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) else { continue } + + filePaths.append(fileUrl.relativePathFromCurrent) + } + + cachedFilePaths[searchOptions] = filePaths + return filePaths + } } diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 0a4c77b..d840e3c 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -3,229 +3,229 @@ import Utility /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { - /// Checks the contents of files. - /// - /// - Parameters: - /// - checkInfo: The info object providing some general information on the lint check. - /// - regex: The regex to use for matching the contents of files. By defaults points to the start of the regex, unless you provide the named group 'pointer'. - /// - violationlocation: Specifies the position of the violation marker violations should be reported. Can be the `lower` or `upper` end of a `fullMatch` or `captureGroup(index:)`. - /// - matchingExamples: An array of example contents where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. - /// - nonMatchingExamples: An array of example contents where the `regex` is expected not to trigger. - /// - includeFilters: An array of regexes defining which files should be incuded in the check. Will check all files matching any of the given regexes. - /// - excludeFilters: An array of regexes defining which files should be excluded from the check. Will ignore all files matching any of the given regexes. Takes precedence over includes. - /// - autoCorrectReplacement: A replacement string which can reference any capture groups in the `regex` to use for autocorrection. - /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly. - /// - repeatIfAutoCorrected: Repeat check if at least one auto-correction was applied in last run. Defaults to `false`. - public static func checkFileContents( - checkInfo: CheckInfo, - regex: Regex, - violationLocation: ViolationLocationConfig = .init(range: .fullMatch, bound: .lower), - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [], - includeFilters: [Regex] = [#".*"#], - excludeFilters: [Regex] = [], - autoCorrectReplacement: String? = nil, - autoCorrectExamples: [AutoCorrection] = [], - repeatIfAutoCorrected: Bool = false - ) throws { - try Statistics.shared.measureTime(check: checkInfo) { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - - validateParameterCombinations( - checkInfo: checkInfo, - autoCorrectReplacement: autoCorrectReplacement, - autoCorrectExamples: autoCorrectExamples, - violateIfNoMatchesFound: nil - ) - - if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( - checkInfo: checkInfo, - examples: autoCorrectExamples, - regex: regex, - autocorrectReplacement: autoCorrectReplacement - ) - } - - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters + /// Checks the contents of files. + /// + /// - Parameters: + /// - checkInfo: The info object providing some general information on the lint check. + /// - regex: The regex to use for matching the contents of files. By defaults points to the start of the regex, unless you provide the named group 'pointer'. + /// - violationlocation: Specifies the position of the violation marker violations should be reported. Can be the `lower` or `upper` end of a `fullMatch` or `captureGroup(index:)`. + /// - matchingExamples: An array of example contents where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. + /// - nonMatchingExamples: An array of example contents where the `regex` is expected not to trigger. + /// - includeFilters: An array of regexes defining which files should be incuded in the check. Will check all files matching any of the given regexes. + /// - excludeFilters: An array of regexes defining which files should be excluded from the check. Will ignore all files matching any of the given regexes. Takes precedence over includes. + /// - autoCorrectReplacement: A replacement string which can reference any capture groups in the `regex` to use for autocorrection. + /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly. + /// - repeatIfAutoCorrected: Repeat check if at least one auto-correction was applied in last run. Defaults to `false`. + public static func checkFileContents( + checkInfo: CheckInfo, + regex: Regex, + violationLocation: ViolationLocationConfig = .init(range: .fullMatch, bound: .lower), + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [], + includeFilters: [Regex] = [#".*"#], + excludeFilters: [Regex] = [], + autoCorrectReplacement: String? = nil, + autoCorrectExamples: [AutoCorrection] = [], + repeatIfAutoCorrected: Bool = false + ) throws { + try Statistics.shared.measureTime(check: checkInfo) { + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + + validateParameterCombinations( + checkInfo: checkInfo, + autoCorrectReplacement: autoCorrectReplacement, + autoCorrectExamples: autoCorrectExamples, + violateIfNoMatchesFound: nil + ) + + if let autoCorrectReplacement = autoCorrectReplacement { + validateAutocorrectsAll( + checkInfo: checkInfo, + examples: autoCorrectExamples, + regex: regex, + autocorrectReplacement: autoCorrectReplacement ) - - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: regex, - violationLocation: violationLocation, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: autoCorrectReplacement, - repeatIfAutoCorrected: repeatIfAutoCorrected - ).performCheck() - - Statistics.shared.found(violations: violations, in: checkInfo) - } - } - - /// Checks the names of files. - /// - /// - Parameters: - /// - checkInfo: The info object providing some general information on the lint check. - /// - regex: The regex to use for matching the paths of files. By defaults points to the start of the regex, unless you provide the named group 'pointer'. - /// - matchingExamples: An array of example paths where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. - /// - nonMatchingExamples: An array of example paths where the `regex` is expected not to trigger. - /// - includeFilters: Defines which files should be incuded in check. Checks all files matching any of the given regexes. - /// - excludeFilters: Defines which files should be excluded from check. Ignores all files matching any of the given regexes. Takes precedence over includes. - /// - autoCorrectReplacement: A replacement string which can reference any capture groups in the `regex` to use for autocorrection. - /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly. - /// - violateIfNoMatchesFound: Inverts the violation logic to report a single violation if no matches are found instead of reporting a violation for each match. - public static func checkFilePaths( - checkInfo: CheckInfo, - regex: Regex, - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [], - includeFilters: [Regex] = [#".*"#], - excludeFilters: [Regex] = [], - autoCorrectReplacement: String? = nil, - autoCorrectExamples: [AutoCorrection] = [], - violateIfNoMatchesFound: Bool = false - ) throws { - try Statistics.shared.measureTime(check: checkInfo) { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - validateParameterCombinations( - checkInfo: checkInfo, - autoCorrectReplacement: autoCorrectReplacement, - autoCorrectExamples: autoCorrectExamples, - violateIfNoMatchesFound: violateIfNoMatchesFound + } + + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } + + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + let violations = try FileContentsChecker( + checkInfo: checkInfo, + regex: regex, + violationLocation: violationLocation, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, + repeatIfAutoCorrected: repeatIfAutoCorrected + ).performCheck() + + Statistics.shared.found(violations: violations, in: checkInfo) + } + } + + /// Checks the names of files. + /// + /// - Parameters: + /// - checkInfo: The info object providing some general information on the lint check. + /// - regex: The regex to use for matching the paths of files. By defaults points to the start of the regex, unless you provide the named group 'pointer'. + /// - matchingExamples: An array of example paths where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. + /// - nonMatchingExamples: An array of example paths where the `regex` is expected not to trigger. + /// - includeFilters: Defines which files should be incuded in check. Checks all files matching any of the given regexes. + /// - excludeFilters: Defines which files should be excluded from check. Ignores all files matching any of the given regexes. Takes precedence over includes. + /// - autoCorrectReplacement: A replacement string which can reference any capture groups in the `regex` to use for autocorrection. + /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly. + /// - violateIfNoMatchesFound: Inverts the violation logic to report a single violation if no matches are found instead of reporting a violation for each match. + public static func checkFilePaths( + checkInfo: CheckInfo, + regex: Regex, + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [], + includeFilters: [Regex] = [#".*"#], + excludeFilters: [Regex] = [], + autoCorrectReplacement: String? = nil, + autoCorrectExamples: [AutoCorrection] = [], + violateIfNoMatchesFound: Bool = false + ) throws { + try Statistics.shared.measureTime(check: checkInfo) { + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + validateParameterCombinations( + checkInfo: checkInfo, + autoCorrectReplacement: autoCorrectReplacement, + autoCorrectExamples: autoCorrectExamples, + violateIfNoMatchesFound: violateIfNoMatchesFound + ) + + if let autoCorrectReplacement = autoCorrectReplacement { + validateAutocorrectsAll( + checkInfo: checkInfo, + examples: autoCorrectExamples, + regex: regex, + autocorrectReplacement: autoCorrectReplacement ) - - if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( - checkInfo: checkInfo, - examples: autoCorrectExamples, - regex: regex, - autocorrectReplacement: autoCorrectReplacement - ) - } - - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters + } + + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } + + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + let violations = try FilePathsChecker( + checkInfo: checkInfo, + regex: regex, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, + violateIfNoMatchesFound: violateIfNoMatchesFound + ).performCheck() + + Statistics.shared.found(violations: violations, in: checkInfo) + } + } + + /// Run custom logic as checks. + /// + /// - Parameters: + /// - checkInfo: The info object providing some general information on the lint check. + /// - customClosure: The custom logic to run which produces an array of `Violation` objects for any violations. + public static func customCheck(checkInfo: CheckInfo, customClosure: (CheckInfo) throws -> [Violation]) rethrows { + try Statistics.shared.measureTime(check: checkInfo) { + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } + + Statistics.shared.found(violations: try customClosure(checkInfo), in: checkInfo) + } + } + + /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. + public static func logSummaryAndExit(arguments: [String] = [], afterPerformingChecks checksToPerform: () throws -> Void = {}) throws { + let failOnWarnings = arguments.contains(Constants.strictArgument) + let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) + let measure = arguments.contains(Constants.measureArgument) + + if targetIsXcode { + log = Logger(outputType: .xcode) + } + + log.logDebugLevel = arguments.contains(Constants.debugArgument) + Options.validateOnly = arguments.contains(Constants.validateArgument) + + try checksToPerform() + + guard !Options.validateOnly else { + Statistics.shared.logValidationSummary() + log.exit(status: .success) + return // only reachable in unit tests + } + + Statistics.shared.logCheckSummary(printExecutionTime: measure) + + if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { + log.exit(status: .failure) + } else if failOnWarnings && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled { + log.exit(status: .failure) + } else { + log.exit(status: .success) + } + } + + static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { + if matchingExamples.isFilled { + log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) + } + + for example in matchingExamples { + if !regex.matches(example) { + log.message( + "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", + level: .error ) - - let violations = try FilePathsChecker( - checkInfo: checkInfo, - regex: regex, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: autoCorrectReplacement, - violateIfNoMatchesFound: violateIfNoMatchesFound - ).performCheck() - - Statistics.shared.found(violations: violations, in: checkInfo) - } - } - - /// Run custom logic as checks. - /// - /// - Parameters: - /// - checkInfo: The info object providing some general information on the lint check. - /// - customClosure: The custom logic to run which produces an array of `Violation` objects for any violations. - public static func customCheck(checkInfo: CheckInfo, customClosure: (CheckInfo) throws -> [Violation]) rethrows { - try Statistics.shared.measureTime(check: checkInfo) { - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - Statistics.shared.found(violations: try customClosure(checkInfo), in: checkInfo) - } - } - - /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. - public static func logSummaryAndExit(arguments: [String] = [], afterPerformingChecks checksToPerform: () throws -> Void = {}) throws { - let failOnWarnings = arguments.contains(Constants.strictArgument) - let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) - let measure = arguments.contains(Constants.measureArgument) - - if targetIsXcode { - log = Logger(outputType: .xcode) - } - - log.logDebugLevel = arguments.contains(Constants.debugArgument) - Options.validateOnly = arguments.contains(Constants.validateArgument) - - try checksToPerform() - - guard !Options.validateOnly else { - Statistics.shared.logValidationSummary() - log.exit(status: .success) - return // only reachable in unit tests - } - - Statistics.shared.logCheckSummary(printExecutionTime: measure) - - if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { log.exit(status: .failure) - } else if failOnWarnings && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled { + } + } + } + + static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { + if nonMatchingExamples.isFilled { + log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) + } + + for example in nonMatchingExamples { + if regex.matches(example) { + log.message( + "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", + level: .error + ) log.exit(status: .failure) - } else { - log.exit(status: .success) - } - } - - static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { - if matchingExamples.isFilled { - log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) - } - - for example in matchingExamples { - if !regex.matches(example) { - log.message( - "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { - if nonMatchingExamples.isFilled { - log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) - } - - for example in nonMatchingExamples { - if regex.matches(example) { - log.message( - "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validateAutocorrectsAll(checkInfo: CheckInfo, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { - if examples.isFilled { - log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) - } - - for autocorrect in examples { - let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) - if autocorrected != autocorrect.after { - log.message( + } + } + } + + static func validateAutocorrectsAll(checkInfo: CheckInfo, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { + if examples.isFilled { + log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) + } + + for autocorrect in examples { + let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) + if autocorrected != autocorrect.after { + log.message( """ Autocorrecting example for \(checkInfo.id) did not result in expected output. Before: '\(autocorrect.before.showWhitespacesAndNewlines())' @@ -233,32 +233,32 @@ public enum Lint { Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' """, level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validateParameterCombinations( - checkInfo: CheckInfo, - autoCorrectReplacement: String?, - autoCorrectExamples: [AutoCorrection], - violateIfNoMatchesFound: Bool? - ) { - if autoCorrectExamples.isFilled && autoCorrectReplacement == nil { - log.message( - "`autoCorrectExamples` provided for check \(checkInfo.id) without specifying an `autoCorrectReplacement`.", - level: .warning - ) - } - - guard autoCorrectReplacement == nil || violateIfNoMatchesFound != true else { - log.message( - "Incompatible options specified for check \(checkInfo.id): autoCorrectReplacement and violateIfNoMatchesFound can't be used together.", - level: .error ) log.exit(status: .failure) - return // only reachable in unit tests - } - } + } + } + } + + static func validateParameterCombinations( + checkInfo: CheckInfo, + autoCorrectReplacement: String?, + autoCorrectExamples: [AutoCorrection], + violateIfNoMatchesFound: Bool? + ) { + if autoCorrectExamples.isFilled && autoCorrectReplacement == nil { + log.message( + "`autoCorrectExamples` provided for check \(checkInfo.id) without specifying an `autoCorrectReplacement`.", + level: .warning + ) + } + + guard autoCorrectReplacement == nil || violateIfNoMatchesFound != true else { + log.message( + "Incompatible options specified for check \(checkInfo.id): autoCorrectReplacement and violateIfNoMatchesFound can't be used together.", + level: .error + ) + log.exit(status: .failure) + return // only reachable in unit tests + } + } } diff --git a/Sources/AnyLint/Options.swift b/Sources/AnyLint/Options.swift index db20a7d..d8d10d2 100644 --- a/Sources/AnyLint/Options.swift +++ b/Sources/AnyLint/Options.swift @@ -1,5 +1,5 @@ import Foundation enum Options { - static var validateOnly: Bool = false + static var validateOnly: Bool = false } diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift index 195c8a0..21992f8 100644 --- a/Sources/AnyLint/Severity.swift +++ b/Sources/AnyLint/Severity.swift @@ -3,47 +3,47 @@ import Utility /// Defines the severity of a lint check. public enum Severity: Int, CaseIterable { - /// Use for checks that are mostly informational and not necessarily problematic. - case info - - /// Use for checks that might potentially be problematic. - case warning - - /// Use for checks that probably are problematic. - case error - - var logLevel: Logger.PrintLevel { - switch self { - case .info: - return .info - - case .warning: - return .warning - - case .error: - return .error - } - } - - static func from(string: String) -> Severity? { - switch string { - case "info", "i": - return .info - - case "warning", "w": - return .warning - - case "error", "e": - return .error - - default: - return nil - } - } + /// Use for checks that are mostly informational and not necessarily problematic. + case info + + /// Use for checks that might potentially be problematic. + case warning + + /// Use for checks that probably are problematic. + case error + + var logLevel: Logger.PrintLevel { + switch self { + case .info: + return .info + + case .warning: + return .warning + + case .error: + return .error + } + } + + static func from(string: String) -> Severity? { + switch string { + case "info", "i": + return .info + + case "warning", "w": + return .warning + + case "error", "e": + return .error + + default: + return nil + } + } } extension Severity: Comparable { - public static func < (lhs: Severity, rhs: Severity) -> Bool { - lhs.rawValue < rhs.rawValue - } + public static func < (lhs: Severity, rhs: Severity) -> Bool { + lhs.rawValue < rhs.rawValue + } } diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index 861cda7..a4eb4b3 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -2,166 +2,166 @@ import Foundation import Utility final class Statistics { - static let shared = Statistics() - - var executedChecks: [CheckInfo] = [] - var violationsPerCheck: [CheckInfo: [Violation]] = [:] - var violationsBySeverity: [Severity: [Violation]] = [.info: [], .warning: [], .error: []] - var filesChecked: Set = [] - var executionTimePerCheck: [CheckInfo: TimeInterval] = [:] - - var maxViolationSeverity: Severity? { - violationsBySeverity.keys.filter { !violationsBySeverity[$0]!.isEmpty }.max { $0.rawValue < $1.rawValue } - } - - private init() {} - - func checkedFiles(at filePaths: [String]) { - filePaths.forEach { filesChecked.insert($0) } - } - - func found(violations: [Violation], in check: CheckInfo) { - executedChecks.append(check) - violationsPerCheck[check] = violations - violationsBySeverity[check.severity]!.append(contentsOf: violations) - } - - func measureTime(check: CheckInfo, lintTaskClosure: () throws -> Void) rethrows { - let startedAt = Date() - try lintTaskClosure() - self.executionTimePerCheck[check] = Date().timeIntervalSince(startedAt) - } - - /// Use for unit testing only. - func reset() { - executedChecks = [] - violationsPerCheck = [:] - violationsBySeverity = [.info: [], .warning: [], .error: []] - filesChecked = [] - } - - func logValidationSummary() { - guard log.outputType != .xcode else { - log.message("Performing validations only while reporting for Xcode is probably misuse of the `-l` / `--validate` option.", level: .warning) - return - } - - if executedChecks.isEmpty { - log.message("No checks found to validate.", level: .warning) - } else { - log.message( - "Performed \(executedChecks.count) validation(s) in \(filesChecked.count) file(s) without any issues.", - level: .success - ) - } - } - - func logCheckSummary(printExecutionTime: Bool) { - // make sure first violation reports in a new line when e.g. 'swift-driver version: 1.45.2' is printed - print("\n") // AnyLint.skipHere: Logger - - if executedChecks.isEmpty { - log.message("No checks found to perform.", level: .warning) - } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { - if printExecutionTime { - self.logExecutionTimes() - } - - switch log.outputType { - case .console, .test: - logViolationsToConsole() - - case .xcode: - showViolationsInXcode() + static let shared = Statistics() + + var executedChecks: [CheckInfo] = [] + var violationsPerCheck: [CheckInfo: [Violation]] = [:] + var violationsBySeverity: [Severity: [Violation]] = [.info: [], .warning: [], .error: []] + var filesChecked: Set = [] + var executionTimePerCheck: [CheckInfo: TimeInterval] = [:] + + var maxViolationSeverity: Severity? { + violationsBySeverity.keys.filter { !violationsBySeverity[$0]!.isEmpty }.max { $0.rawValue < $1.rawValue } + } + + private init() {} + + func checkedFiles(at filePaths: [String]) { + filePaths.forEach { filesChecked.insert($0) } + } + + func found(violations: [Violation], in check: CheckInfo) { + executedChecks.append(check) + violationsPerCheck[check] = violations + violationsBySeverity[check.severity]!.append(contentsOf: violations) + } + + func measureTime(check: CheckInfo, lintTaskClosure: () throws -> Void) rethrows { + let startedAt = Date() + try lintTaskClosure() + self.executionTimePerCheck[check] = Date().timeIntervalSince(startedAt) + } + + /// Use for unit testing only. + func reset() { + executedChecks = [] + violationsPerCheck = [:] + violationsBySeverity = [.info: [], .warning: [], .error: []] + filesChecked = [] + } + + func logValidationSummary() { + guard log.outputType != .xcode else { + log.message("Performing validations only while reporting for Xcode is probably misuse of the `-l` / `--validate` option.", level: .warning) + return + } + + if executedChecks.isEmpty { + log.message("No checks found to validate.", level: .warning) + } else { + log.message( + "Performed \(executedChecks.count) validation(s) in \(filesChecked.count) file(s) without any issues.", + level: .success + ) + } + } + + func logCheckSummary(printExecutionTime: Bool) { + // make sure first violation reports in a new line when e.g. 'swift-driver version: 1.45.2' is printed + print("\n") // AnyLint.skipHere: Logger + + if executedChecks.isEmpty { + log.message("No checks found to perform.", level: .warning) + } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { + if printExecutionTime { + self.logExecutionTimes() + } + + switch log.outputType { + case .console, .test: + logViolationsToConsole() + + case .xcode: + showViolationsInXcode() + } + } else { + if printExecutionTime { + self.logExecutionTimes() + } + + log.message( + "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", + level: .success + ) + } + } + + func logExecutionTimes() { + log.message("⏱ Executed checks sorted by their execution time:", level: .info) + + for (check, executionTime) in self.executionTimePerCheck.sorted(by: { $0.value > $1.value }) { + let milliseconds = Int(executionTime * 1000) + log.message("\(milliseconds)ms\t\(check.id)", level: .info) + } + } + + func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { + let violations: [Violation] = violationsBySeverity[severity]! + guard excludeAutocorrected else { return violations } + return violations.filter { $0.appliedAutoCorrection == nil } + } + + private func logViolationsToConsole() { + for check in executedChecks { + if let checkViolations = violationsPerCheck[check], checkViolations.isFilled { + let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage(pathType: .relative) != nil } + + if violationsWithLocationMessage.isFilled { + log.message( + "\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:", + level: check.severity.logLevel + ) + let numerationDigits = String(violationsWithLocationMessage.count).count + + for (index, violation) in violationsWithLocationMessage.enumerated() { + let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) + let prefix = "> \(violationNumString). " + log.message(prefix + violation.locationMessage(pathType: .relative)!, level: check.severity.logLevel) + + let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined() + if let appliedAutoCorrection = violation.appliedAutoCorrection { + for messageLine in appliedAutoCorrection.appliedMessageLines { + log.message(prefixLengthWhitespaces + messageLine, level: .info) + } + } else if let matchedString = violation.matchedString { + log.message(prefixLengthWhitespaces + "Matching string:".bold + " (trimmed & reduced whitespaces)", level: .info) + let matchedStringOutput = matchedString + .showNewlines() + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: " ", with: " ") + .replacingOccurrences(of: " ", with: " ") + .replacingOccurrences(of: " ", with: " ") + log.message(prefixLengthWhitespaces + "> " + matchedStringOutput, level: .info) + } + } + } else { + log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", level: check.severity.logLevel) } - } else { - if printExecutionTime { - self.logExecutionTimes() - } - - log.message( - "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", - level: .success + + log.message(">> Hint: \(check.hint)".bold.italic, level: check.severity.logLevel) + } + } + + let errors = "\(violationsBySeverity[.error]!.count) error(s)" + let warnings = "\(violationsBySeverity[.warning]!.count) warning(s)" + + log.message( + "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) and found \(errors) & \(warnings).", + level: maxViolationSeverity!.logLevel + ) + } + + private func showViolationsInXcode() { + for severity in violationsBySeverity.keys.sorted().reversed() { + let severityViolations = violationsBySeverity[severity]! + for violation in severityViolations where violation.appliedAutoCorrection == nil { + let check = violation.checkInfo + log.xcodeMessage( + "[\(check.id)] \(check.hint)", + level: check.severity.logLevel, + location: violation.locationMessage(pathType: .absolute) ) - } - } - - func logExecutionTimes() { - log.message("⏱ Executed checks sorted by their execution time:", level: .info) - - for (check, executionTime) in self.executionTimePerCheck.sorted(by: { $0.value > $1.value }) { - let milliseconds = Int(executionTime * 1000) - log.message("\(milliseconds)ms\t\(check.id)", level: .info) - } - } - - func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { - let violations: [Violation] = violationsBySeverity[severity]! - guard excludeAutocorrected else { return violations } - return violations.filter { $0.appliedAutoCorrection == nil } - } - - private func logViolationsToConsole() { - for check in executedChecks { - if let checkViolations = violationsPerCheck[check], checkViolations.isFilled { - let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage(pathType: .relative) != nil } - - if violationsWithLocationMessage.isFilled { - log.message( - "\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:", - level: check.severity.logLevel - ) - let numerationDigits = String(violationsWithLocationMessage.count).count - - for (index, violation) in violationsWithLocationMessage.enumerated() { - let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) - let prefix = "> \(violationNumString). " - log.message(prefix + violation.locationMessage(pathType: .relative)!, level: check.severity.logLevel) - - let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined() - if let appliedAutoCorrection = violation.appliedAutoCorrection { - for messageLine in appliedAutoCorrection.appliedMessageLines { - log.message(prefixLengthWhitespaces + messageLine, level: .info) - } - } else if let matchedString = violation.matchedString { - log.message(prefixLengthWhitespaces + "Matching string:".bold + " (trimmed & reduced whitespaces)", level: .info) - let matchedStringOutput = matchedString - .showNewlines() - .trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: " ", with: " ") - .replacingOccurrences(of: " ", with: " ") - .replacingOccurrences(of: " ", with: " ") - log.message(prefixLengthWhitespaces + "> " + matchedStringOutput, level: .info) - } - } - } else { - log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", level: check.severity.logLevel) - } - - log.message(">> Hint: \(check.hint)".bold.italic, level: check.severity.logLevel) - } - } - - let errors = "\(violationsBySeverity[.error]!.count) error(s)" - let warnings = "\(violationsBySeverity[.warning]!.count) warning(s)" - - log.message( - "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) and found \(errors) & \(warnings).", - level: maxViolationSeverity!.logLevel - ) - } - - private func showViolationsInXcode() { - for severity in violationsBySeverity.keys.sorted().reversed() { - let severityViolations = violationsBySeverity[severity]! - for violation in severityViolations where violation.appliedAutoCorrection == nil { - let check = violation.checkInfo - log.xcodeMessage( - "[\(check.id)] \(check.hint)", - level: check.severity.logLevel, - location: violation.locationMessage(pathType: .absolute) - ) - } - } - } + } + } + } } diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index 6eec576..f04de0a 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -4,40 +4,40 @@ import Utility /// A violation found in a check. public struct Violation { - /// The info about the chack that caused this violation. - public let checkInfo: CheckInfo - - /// The file path the violation is related to. - public let filePath: String? - - /// 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 locationInfo: String.LocationInfo? - - /// The autocorrection applied to fix this violation. - public let appliedAutoCorrection: AutoCorrection? - - /// Initializes a violation object. - public init( - checkInfo: CheckInfo, - filePath: String? = nil, - matchedString: String? = nil, - locationInfo: String.LocationInfo? = nil, - appliedAutoCorrection: AutoCorrection? = nil - ) { - self.checkInfo = checkInfo - self.filePath = filePath - self.matchedString = matchedString - self.locationInfo = locationInfo - self.appliedAutoCorrection = appliedAutoCorrection - } - - /// Returns a string representation of a violations filled with path and line information if available. - public func locationMessage(pathType: String.PathType) -> String? { - guard let filePath = filePath else { return nil } - guard let locationInfo = locationInfo else { return filePath.path(type: pathType) } - return "\(filePath.path(type: pathType)):\(locationInfo.line):\(locationInfo.charInLine):" - } + /// The info about the chack that caused this violation. + public let checkInfo: CheckInfo + + /// The file path the violation is related to. + public let filePath: String? + + /// 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 locationInfo: String.LocationInfo? + + /// The autocorrection applied to fix this violation. + public let appliedAutoCorrection: AutoCorrection? + + /// Initializes a violation object. + public init( + checkInfo: CheckInfo, + filePath: String? = nil, + matchedString: String? = nil, + locationInfo: String.LocationInfo? = nil, + appliedAutoCorrection: AutoCorrection? = nil + ) { + self.checkInfo = checkInfo + self.filePath = filePath + self.matchedString = matchedString + self.locationInfo = locationInfo + self.appliedAutoCorrection = appliedAutoCorrection + } + + /// Returns a string representation of a violations filled with path and line information if available. + public func locationMessage(pathType: String.PathType) -> String? { + guard let filePath = filePath else { return nil } + guard let locationInfo = locationInfo else { return filePath.path(type: pathType) } + return "\(filePath.path(type: pathType)):\(locationInfo.line):\(locationInfo.charInLine):" + } } diff --git a/Sources/AnyLint/ViolationLocationConfig.swift b/Sources/AnyLint/ViolationLocationConfig.swift index 0bb5f89..82fb566 100644 --- a/Sources/AnyLint/ViolationLocationConfig.swift +++ b/Sources/AnyLint/ViolationLocationConfig.swift @@ -2,33 +2,33 @@ import Foundation /// Configuration for the position of the violation marker violations should be reported at. public struct ViolationLocationConfig { - /// The range to use for pointer reporting. One of `.fullMatch` or `.captureGroup(index:)`. - public enum Range { - /// Uses the full matched range of the Regex. - case fullMatch - - /// Uses the capture group range of the provided index. - case captureGroup(index: Int) - } - - /// The bound to use for pionter reporting. One of `.lower` or `.upper`. - public enum Bound { - /// Uses the lower end of the provided range. - case lower - - /// Uses the upper end of the provided range. - case upper - } - - let range: Range - let bound: Bound - - /// Initializes a new instance with given range and bound. - /// - Parameters: - /// - range: The range to use for pointer reporting. One of `.fullMatch` or `.captureGroup(index:)`. - /// - bound: The bound to use for pionter reporting. One of `.lower` or `.upper`. - public init(range: Range, bound: Bound) { - self.range = range - self.bound = bound - } + /// The range to use for pointer reporting. One of `.fullMatch` or `.captureGroup(index:)`. + public enum Range { + /// Uses the full matched range of the Regex. + case fullMatch + + /// Uses the capture group range of the provided index. + case captureGroup(index: Int) + } + + /// The bound to use for pionter reporting. One of `.lower` or `.upper`. + public enum Bound { + /// Uses the lower end of the provided range. + case lower + + /// Uses the upper end of the provided range. + case upper + } + + let range: Range + let bound: Bound + + /// Initializes a new instance with given range and bound. + /// - Parameters: + /// - range: The range to use for pointer reporting. One of `.fullMatch` or `.captureGroup(index:)`. + /// - bound: The bound to use for pionter reporting. One of `.lower` or `.upper`. + public init(range: Range, bound: Bound) { + self.range = range + self.bound = bound + } } diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index 3c139a0..204ff49 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -3,83 +3,83 @@ import SwiftCLI import Utility class SingleCommand: Command { - // MARK: - Basics - var name: String = CLIConstants.commandName - var shortDescription: String = "Lint anything by combining the power of Swift & regular expressions." - - // MARK: - Subcommands - @Flag("-v", "--version", description: "Prints the current tool version") - var version: Bool - - @Flag("-x", "--xcode", description: "Prints warnings & errors in a format to be reported right within Xcodes left sidebar") - var xcode: Bool - - @Flag("-d", "--debug", description: "Logs much more detailed information about what AnyLint is doing for debugging purposes") - var debug: Bool - - @Flag("-s", "--strict", description: "Fails on warnings as well - by default, the command only fails on errors)") - var strict: Bool - - @Flag("-l", "--validate", description: "Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`.") - var validate: Bool - - @Flag("-m", "--measure", description: "Prints the time it took to execute each check for performance optimizations") - var measure: Bool - - @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(CLIConstants.initTemplateCases)]") - var initTemplateName: String? - - // MARK: - Options - @VariadicKey("-p", "--path", description: "Provide a custom path to the config file (multiple usage supported)") - var customPaths: [String] - - // MARK: - Execution - func execute() throws { - if xcode { - log = Logger(outputType: .xcode) - } - - log.logDebugLevel = debug - - // version subcommand - if version { - try VersionTask().perform() - log.exit(status: .success) - } - - let configurationPaths = customPaths.isEmpty - ? [fileManager.currentDirectoryPath.appendingPathComponent(CLIConstants.defaultConfigFileName)] - : customPaths - - // init subcommand - if let initTemplateName = initTemplateName { - guard let initTemplate = InitTask.Template(rawValue: initTemplateName) else { - log.message("Unknown default template '\(initTemplateName)' – use one of: [\(CLIConstants.initTemplateCases)]", level: .error) - log.exit(status: .failure) - return // only reachable in unit tests - } - - for configPath in configurationPaths { - try InitTask(configFilePath: configPath, template: initTemplate).perform() - } - log.exit(status: .success) - } - - // lint main command - var anyConfigFileFailed = false - for configPath in configurationPaths { - do { - try LintTask( - configFilePath: configPath, - logDebugLevel: self.debug, - failOnWarnings: self.strict, - validateOnly: self.validate, - measure: self.measure - ).perform() - } catch LintTask.LintError.configFileFailed { - anyConfigFileFailed = true - } - } - exit(anyConfigFileFailed ? EXIT_FAILURE : EXIT_SUCCESS) - } + // MARK: - Basics + var name: String = CLIConstants.commandName + var shortDescription: String = "Lint anything by combining the power of Swift & regular expressions." + + // MARK: - Subcommands + @Flag("-v", "--version", description: "Prints the current tool version") + var version: Bool + + @Flag("-x", "--xcode", description: "Prints warnings & errors in a format to be reported right within Xcodes left sidebar") + var xcode: Bool + + @Flag("-d", "--debug", description: "Logs much more detailed information about what AnyLint is doing for debugging purposes") + var debug: Bool + + @Flag("-s", "--strict", description: "Fails on warnings as well - by default, the command only fails on errors)") + var strict: Bool + + @Flag("-l", "--validate", description: "Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`.") + var validate: Bool + + @Flag("-m", "--measure", description: "Prints the time it took to execute each check for performance optimizations") + var measure: Bool + + @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(CLIConstants.initTemplateCases)]") + var initTemplateName: String? + + // MARK: - Options + @VariadicKey("-p", "--path", description: "Provide a custom path to the config file (multiple usage supported)") + var customPaths: [String] + + // MARK: - Execution + func execute() throws { + if xcode { + log = Logger(outputType: .xcode) + } + + log.logDebugLevel = debug + + // version subcommand + if version { + try VersionTask().perform() + log.exit(status: .success) + } + + let configurationPaths = customPaths.isEmpty + ? [fileManager.currentDirectoryPath.appendingPathComponent(CLIConstants.defaultConfigFileName)] + : customPaths + + // init subcommand + if let initTemplateName = initTemplateName { + guard let initTemplate = InitTask.Template(rawValue: initTemplateName) else { + log.message("Unknown default template '\(initTemplateName)' – use one of: [\(CLIConstants.initTemplateCases)]", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + for configPath in configurationPaths { + try InitTask(configFilePath: configPath, template: initTemplate).perform() + } + log.exit(status: .success) + } + + // lint main command + var anyConfigFileFailed = false + for configPath in configurationPaths { + do { + try LintTask( + configFilePath: configPath, + logDebugLevel: self.debug, + failOnWarnings: self.strict, + validateOnly: self.validate, + measure: self.measure + ).perform() + } catch LintTask.LintError.configFileFailed { + anyConfigFileFailed = true + } + } + exit(anyConfigFileFailed ? EXIT_FAILURE : EXIT_SUCCESS) + } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 13b2d97..960a9c0 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -4,11 +4,11 @@ import Utility // swiftlint:disable function_body_length enum BlankTemplate: ConfigurationTemplate { - static func fileContents() -> String { - commonPrefix + #""" + static func fileContents() -> String { + commonPrefix + #""" // MARK: - Variables let readmeFile: Regex = #"^README\.md$"# - + // MARK: - Checks // MARK: Readme try Lint.checkFilePaths( @@ -18,7 +18,7 @@ enum BlankTemplate: ConfigurationTemplate { nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], violateIfNoMatchesFound: true ) - + // MARK: ReadmePath try Lint.checkFilePaths( checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", @@ -32,7 +32,7 @@ enum BlankTemplate: ConfigurationTemplate { ["before": "README.markdown", "after": "README.md"], ] ) - + // MARK: ReadmeTopLevelTitle try Lint.checkFileContents( checkInfo: "ReadmeTopLevelTitle: The README.md file should only contain a single top level title.", @@ -42,7 +42,7 @@ enum BlankTemplate: ConfigurationTemplate { # Title ## Subtitle Lorem ipsum - + # Other Title ## Other Subtitle """, @@ -52,14 +52,14 @@ enum BlankTemplate: ConfigurationTemplate { # Title ## Subtitle Lorem ipsum #1 and # 2. - + ## Other Subtitle ### Other Subsubtitle """, ], includeFilters: [readmeFile] ) - + // MARK: ReadmeTypoLicense try Lint.checkFileContents( checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.", @@ -74,5 +74,5 @@ enum BlankTemplate: ConfigurationTemplate { ] ) """# + commonSuffix - } + } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index 46099c8..724f32a 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -2,25 +2,25 @@ import Foundation import Utility protocol ConfigurationTemplate { - static func fileContents() -> String + static func fileContents() -> String } extension ConfigurationTemplate { - static var commonPrefix: String { + static var commonPrefix: String { """ #!\(CLIConstants.swiftShPath) import AnyLint // @FlineDev - + try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { - + """ - } - - static var commonSuffix: String { + } + + static var commonSuffix: String { """ - + } - + """ - } + } } diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift index d07761c..2cb1511 100644 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -1,52 +1,52 @@ import Foundation enum CLIConstants { - static let commandName: String = "anylint" - static let defaultConfigFileName: String = "lint.swift" - static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") - static var swiftShPath: String { - switch self.getPlatform() { - case .intel: - return "/usr/local/bin/swift-sh" - - case .appleSilicon: - return "/opt/homebrew/bin/swift-sh" - - case .linux: - return "/home/linuxbrew/.linuxbrew/bin/swift-sh" - } - } + static let commandName: String = "anylint" + static let defaultConfigFileName: String = "lint.swift" + static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") + static var swiftShPath: String { + switch self.getPlatform() { + case .intel: + return "/usr/local/bin/swift-sh" + + case .appleSilicon: + return "/opt/homebrew/bin/swift-sh" + + case .linux: + return "/home/linuxbrew/.linuxbrew/bin/swift-sh" + } + } } extension CLIConstants { - fileprivate enum Platform { - case intel - case appleSilicon - case linux - } - - fileprivate static func getPlatform() -> Platform { - #if os(Linux) - return .linux - #else - // Source: https://stackoverflow.com/a/69624732 - var systemInfo = utsname() - let exitCode = uname(&systemInfo) - - let fallbackPlatform: Platform = .appleSilicon - guard exitCode == EXIT_SUCCESS else { return fallbackPlatform } - - let cpuArchitecture = String(cString: &systemInfo.machine.0, encoding: .utf8) - switch cpuArchitecture { - case "x86_64": - return .intel - - case "arm64": - return .appleSilicon - - default: - return fallbackPlatform - } - #endif - } + fileprivate enum Platform { + case intel + case appleSilicon + case linux + } + + fileprivate static func getPlatform() -> Platform { +#if os(Linux) + return .linux +#else + // Source: https://stackoverflow.com/a/69624732 + var systemInfo = utsname() + let exitCode = uname(&systemInfo) + + let fallbackPlatform: Platform = .appleSilicon + guard exitCode == EXIT_SUCCESS else { return fallbackPlatform } + + let cpuArchitecture = String(cString: &systemInfo.machine.0, encoding: .utf8) + switch cpuArchitecture { + case "x86_64": + return .intel + + case "arm64": + return .appleSilicon + + default: + return fallbackPlatform + } +#endif + } } diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index f4ac06c..6d25443 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -3,26 +3,26 @@ import SwiftCLI import Utility enum ValidateOrFail { - /// Fails if swift-sh is not installed. - static func swiftShInstalled() { - guard fileManager.fileExists(atPath: CLIConstants.swiftShPath) else { - log.message( - "swift-sh not installed – please try `brew install swift-sh` or follow instructions on https://github.com/mxcl/swift-sh#installation", - level: .error - ) - log.exit(status: .failure) - return // only reachable in unit tests - } - } - - static func configFileExists(at configFilePath: String) throws { - guard fileManager.fileExists(atPath: configFilePath) else { - log.message( - "No configuration file found at \(configFilePath) – consider running `--init` with a template, e.g.`\(CLIConstants.commandName) --init blank`.", - level: .error - ) - log.exit(status: .failure) - return // only reachable in unit tests - } - } + /// Fails if swift-sh is not installed. + static func swiftShInstalled() { + guard fileManager.fileExists(atPath: CLIConstants.swiftShPath) else { + log.message( + "swift-sh not installed – please try `brew install swift-sh` or follow instructions on https://github.com/mxcl/swift-sh#installation", + level: .error + ) + log.exit(status: .failure) + return // only reachable in unit tests + } + } + + static func configFileExists(at configFilePath: String) throws { + guard fileManager.fileExists(atPath: configFilePath) else { + log.message( + "No configuration file found at \(configFilePath) – consider running `--init` with a template, e.g.`\(CLIConstants.commandName) --init blank`.", + level: .error + ) + log.exit(status: .failure) + return // only reachable in unit tests + } + } } diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index fcf4ee7..5077dbe 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -3,44 +3,44 @@ import SwiftCLI import Utility struct InitTask { - enum Template: String, CaseIterable { - case blank - - var configFileContents: String { - switch self { - case .blank: - return BlankTemplate.fileContents() - } - } - } - - let configFilePath: String - let template: Template + enum Template: String, CaseIterable { + case blank + + var configFileContents: String { + switch self { + case .blank: + return BlankTemplate.fileContents() + } + } + } + + let configFilePath: String + let template: Template } extension InitTask: TaskHandler { - func perform() throws { - guard !fileManager.fileExists(atPath: configFilePath) else { - log.message("Configuration file already exists at path '\(configFilePath)'.", level: .error) - log.exit(status: .failure) - return // only reachable in unit tests - } - - ValidateOrFail.swiftShInstalled() - - log.message("Making sure config file directory exists ...", level: .info) - try Task.run(bash: "mkdir -p '\(configFilePath.parentDirectoryPath)'") - - log.message("Creating config file using template '\(template.rawValue)' ...", level: .info) - fileManager.createFile( - atPath: configFilePath, - contents: template.configFileContents.data(using: .utf8), - attributes: nil - ) - - log.message("Making config file executable ...", level: .info) - try Task.run(bash: "chmod +x '\(configFilePath)'") - - log.message("Successfully created config file at \(configFilePath)", level: .success) - } + func perform() throws { + guard !fileManager.fileExists(atPath: configFilePath) else { + log.message("Configuration file already exists at path '\(configFilePath)'.", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + ValidateOrFail.swiftShInstalled() + + log.message("Making sure config file directory exists ...", level: .info) + try Task.run(bash: "mkdir -p '\(configFilePath.parentDirectoryPath)'") + + log.message("Creating config file using template '\(template.rawValue)' ...", level: .info) + fileManager.createFile( + atPath: configFilePath, + contents: template.configFileContents.data(using: .utf8), + attributes: nil + ) + + log.message("Making config file executable ...", level: .info) + try Task.run(bash: "chmod +x '\(configFilePath)'") + + log.message("Successfully created config file at \(configFilePath)", level: .success) + } } diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index e4a48be..5b86f6e 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -3,57 +3,57 @@ import SwiftCLI import Utility struct LintTask { - let configFilePath: String - let logDebugLevel: Bool - let failOnWarnings: Bool - let validateOnly: Bool - let measure: Bool + let configFilePath: String + let logDebugLevel: Bool + let failOnWarnings: Bool + let validateOnly: Bool + let measure: Bool } extension LintTask: TaskHandler { - enum LintError: Error { - case configFileFailed - } - - /// - Throws: `LintError.configFileFailed` if running a configuration file fails - func perform() throws { - try ValidateOrFail.configFileExists(at: configFilePath) - - if !fileManager.isExecutableFile(atPath: configFilePath) { - try Task.run(bash: "chmod +x '\(configFilePath)'") - } - - ValidateOrFail.swiftShInstalled() - - do { - log.message("Start linting using config file at \(configFilePath) ...", level: .info) - - var command = "\(configFilePath.absolutePath) \(log.outputType.rawValue)" - - if logDebugLevel { - command += " \(Constants.debugArgument)" - } - - if failOnWarnings { - command += " \(Constants.strictArgument)" - } - - if validateOnly { - command += " \(Constants.validateArgument)" - } - - if measure { - command += " \(Constants.measureArgument)" - } - - try Task.run(bash: command) - log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success) - } catch is RunError { - if log.outputType != .xcode { - log.message("Linting failed using config file at \(configFilePath).", level: .error) - } - - throw LintError.configFileFailed - } - } + enum LintError: Error { + case configFileFailed + } + + /// - Throws: `LintError.configFileFailed` if running a configuration file fails + func perform() throws { + try ValidateOrFail.configFileExists(at: configFilePath) + + if !fileManager.isExecutableFile(atPath: configFilePath) { + try Task.run(bash: "chmod +x '\(configFilePath)'") + } + + ValidateOrFail.swiftShInstalled() + + do { + log.message("Start linting using config file at \(configFilePath) ...", level: .info) + + var command = "\(configFilePath.absolutePath) \(log.outputType.rawValue)" + + if logDebugLevel { + command += " \(Constants.debugArgument)" + } + + if failOnWarnings { + command += " \(Constants.strictArgument)" + } + + if validateOnly { + command += " \(Constants.validateArgument)" + } + + if measure { + command += " \(Constants.measureArgument)" + } + + try Task.run(bash: command) + log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success) + } catch is RunError { + if log.outputType != .xcode { + log.message("Linting failed using config file at \(configFilePath).", level: .error) + } + + throw LintError.configFileFailed + } + } } diff --git a/Sources/AnyLintCLI/Tasks/TaskHandler.swift b/Sources/AnyLintCLI/Tasks/TaskHandler.swift index 9986c03..39e293e 100644 --- a/Sources/AnyLintCLI/Tasks/TaskHandler.swift +++ b/Sources/AnyLintCLI/Tasks/TaskHandler.swift @@ -1,5 +1,5 @@ import Foundation protocol TaskHandler { - func perform() throws + func perform() throws } diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift index e043f26..0e580cf 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -4,7 +4,7 @@ import Utility struct VersionTask { /* for extension purposes only */ } extension VersionTask: TaskHandler { - func perform() throws { - log.message(Constants.currentVersion, level: .info) - } + func perform() throws { + log.message(Constants.currentVersion, level: .info) + } } diff --git a/Sources/Utility/Constants.swift b/Sources/Utility/Constants.swift index b9747ea..5b72cb9 100644 --- a/Sources/Utility/Constants.swift +++ b/Sources/Utility/Constants.swift @@ -8,33 +8,33 @@ public var log = Logger(outputType: .console) /// Constants to reference across the project. public enum Constants { - /// The current tool version string. Conforms to SemVer 2.0. - public static let currentVersion: String = "0.10.1" - - /// The name of this tool. - public static let toolName: String = "AnyLint" - - /// The debug mode argument for command line pass-through. - public static let debugArgument: String = "debug" - - /// The strict mode argument for command-line pass-through. - public static let strictArgument: String = "strict" - - /// The validate-only mode argument for command-line pass-through. - public static let validateArgument: String = "validate" - - /// The measure mode to see how long each check took to execute - public static let measureArgument: String = "measure" - - /// The separator indicating that next come regex options. - public static let regexOptionsSeparator: String = #"\"# - - /// Hint that the case insensitive option should be active on a Regex. - public static let caseInsensitiveRegexOption: String = "i" - - /// Hint that the case dot matches newline option should be active on a Regex. - public static let dotMatchesNewlinesRegexOption: String = "m" - - /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. - public static let newlinesRequiredForDiffing: Int = 3 + /// The current tool version string. Conforms to SemVer 2.0. + public static let currentVersion: String = "0.10.1" + + /// The name of this tool. + public static let toolName: String = "AnyLint" + + /// The debug mode argument for command line pass-through. + public static let debugArgument: String = "debug" + + /// The strict mode argument for command-line pass-through. + public static let strictArgument: String = "strict" + + /// The validate-only mode argument for command-line pass-through. + public static let validateArgument: String = "validate" + + /// The measure mode to see how long each check took to execute + public static let measureArgument: String = "measure" + + /// The separator indicating that next come regex options. + public static let regexOptionsSeparator: String = #"\"# + + /// Hint that the case insensitive option should be active on a Regex. + public static let caseInsensitiveRegexOption: String = "i" + + /// Hint that the case dot matches newline option should be active on a Regex. + public static let dotMatchesNewlinesRegexOption: String = "m" + + /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. + public static let newlinesRequiredForDiffing: Int = 3 } diff --git a/Sources/Utility/Extensions/CollectionExt.swift b/Sources/Utility/Extensions/CollectionExt.swift index 7d9a099..cb96240 100644 --- a/Sources/Utility/Extensions/CollectionExt.swift +++ b/Sources/Utility/Extensions/CollectionExt.swift @@ -1,8 +1,8 @@ import Foundation extension Collection { - /// A Boolean value indicating whether the collection is not empty. - public var isFilled: Bool { - !isEmpty - } + /// A Boolean value indicating whether the collection is not empty. + public var isFilled: Bool { + !isEmpty + } } diff --git a/Sources/Utility/Extensions/FileManagerExt.swift b/Sources/Utility/Extensions/FileManagerExt.swift index 05e3701..89bccc7 100644 --- a/Sources/Utility/Extensions/FileManagerExt.swift +++ b/Sources/Utility/Extensions/FileManagerExt.swift @@ -1,14 +1,14 @@ import Foundation extension FileManager { - /// The current directory `URL`. - public var currentDirectoryUrl: URL { - URL(string: currentDirectoryPath)! - } - - /// Checks if a file exists and the given paths and is a directory. - public func fileExistsAndIsDirectory(atPath path: String) -> Bool { - var isDirectory: ObjCBool = false - return fileExists(atPath: path, isDirectory: &isDirectory) && isDirectory.boolValue - } + /// The current directory `URL`. + public var currentDirectoryUrl: URL { + URL(string: currentDirectoryPath)! + } + + /// Checks if a file exists and the given paths and is a directory. + public func fileExistsAndIsDirectory(atPath path: String) -> Bool { + var isDirectory: ObjCBool = false + return fileExists(atPath: path, isDirectory: &isDirectory) && isDirectory.boolValue + } } diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/Utility/Extensions/RegexExt.swift index 183abce..608ed93 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -1,76 +1,76 @@ import Foundation extension Regex: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - var pattern = value - let options: Options = { - if - value.hasSuffix(Constants.regexOptionsSeparator + Constants.caseInsensitiveRegexOption + Constants.dotMatchesNewlinesRegexOption) - || value.hasSuffix(Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption + Constants.caseInsensitiveRegexOption) - { - pattern.removeLast((Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption + Constants.caseInsensitiveRegexOption).count) - return Regex.defaultOptions.union([.ignoreCase, .dotMatchesLineSeparators]) - } else if value.hasSuffix(Constants.regexOptionsSeparator + Constants.caseInsensitiveRegexOption) { - pattern.removeLast((Constants.regexOptionsSeparator + Constants.caseInsensitiveRegexOption).count) - return Regex.defaultOptions.union([.ignoreCase]) - } else if value.hasSuffix(Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption) { - pattern.removeLast((Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption).count) - return Regex.defaultOptions.union([.dotMatchesLineSeparators]) - } else { - return Regex.defaultOptions - } - }() - - do { - self = try Regex(pattern, options: options) - } catch { - log.message("Failed to convert String literal '\(value)' to type Regex.", level: .error) - log.exit(status: .failure) - exit(EXIT_FAILURE) // only reachable in unit tests - } - } + public init(stringLiteral value: String) { + var pattern = value + let options: Options = { + if + value.hasSuffix(Constants.regexOptionsSeparator + Constants.caseInsensitiveRegexOption + Constants.dotMatchesNewlinesRegexOption) + || value.hasSuffix(Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption + Constants.caseInsensitiveRegexOption) + { + pattern.removeLast((Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption + Constants.caseInsensitiveRegexOption).count) + return Regex.defaultOptions.union([.ignoreCase, .dotMatchesLineSeparators]) + } else if value.hasSuffix(Constants.regexOptionsSeparator + Constants.caseInsensitiveRegexOption) { + pattern.removeLast((Constants.regexOptionsSeparator + Constants.caseInsensitiveRegexOption).count) + return Regex.defaultOptions.union([.ignoreCase]) + } else if value.hasSuffix(Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption) { + pattern.removeLast((Constants.regexOptionsSeparator + Constants.dotMatchesNewlinesRegexOption).count) + return Regex.defaultOptions.union([.dotMatchesLineSeparators]) + } else { + return Regex.defaultOptions + } + }() + + do { + self = try Regex(pattern, options: options) + } catch { + log.message("Failed to convert String literal '\(value)' to type Regex.", level: .error) + log.exit(status: .failure) + exit(EXIT_FAILURE) // only reachable in unit tests + } + } } extension Regex: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, String)...) { - var patternElements = elements - var options: Options = Regex.defaultOptions - - if let regexOptionsValue = elements.last(where: { $0.0 == Constants.regexOptionsSeparator })?.1 { - patternElements.removeAll { $0.0 == Constants.regexOptionsSeparator } - - if regexOptionsValue.contains(Constants.caseInsensitiveRegexOption) { - options.insert(.ignoreCase) - } - - if regexOptionsValue.contains(Constants.dotMatchesNewlinesRegexOption) { - options.insert(.dotMatchesLineSeparators) - } - } - - do { - let pattern: String = patternElements.reduce(into: "") { result, element in result.append("(?<\(element.0)>\(element.1))") } - self = try Regex(pattern, options: options) - } catch { - log.message("Failed to convert Dictionary literal '\(elements)' to type Regex.", level: .error) - log.exit(status: .failure) - exit(EXIT_FAILURE) // only reachable in unit tests - } - } + public init(dictionaryLiteral elements: (String, String)...) { + var patternElements = elements + var options: Options = Regex.defaultOptions + + if let regexOptionsValue = elements.last(where: { $0.0 == Constants.regexOptionsSeparator })?.1 { + patternElements.removeAll { $0.0 == Constants.regexOptionsSeparator } + + if regexOptionsValue.contains(Constants.caseInsensitiveRegexOption) { + options.insert(.ignoreCase) + } + + if regexOptionsValue.contains(Constants.dotMatchesNewlinesRegexOption) { + options.insert(.dotMatchesLineSeparators) + } + } + + do { + let pattern: String = patternElements.reduce(into: "") { result, element in result.append("(?<\(element.0)>\(element.1))") } + self = try Regex(pattern, options: options) + } catch { + log.message("Failed to convert Dictionary literal '\(elements)' to type Regex.", level: .error) + log.exit(status: .failure) + exit(EXIT_FAILURE) // only reachable in unit tests + } + } } extension Regex { - /// Replaces all captures groups with the given capture references. References can be numbers like `$1` and capture names like `$prefix`. - public func replaceAllCaptures(in input: String, with template: String) -> String { - replacingMatches(in: input, with: numerizedNamedCaptureRefs(in: template)) - } - - /// Numerizes references to named capture groups to work around missing named capture group replacement in `NSRegularExpression` APIs. - func numerizedNamedCaptureRefs(in replacementString: String) -> String { - let captureGroupNameRegex = Regex(#"\(\?\<([a-zA-Z0-9_-]+)\>[^\)]+\)"#) - let captureGroupNames: [String] = captureGroupNameRegex.matches(in: pattern).map { $0.captures[0]! } - return captureGroupNames.enumerated().reduce(replacementString) { result, enumeratedGroupName in - result.replacingOccurrences(of: "$\(enumeratedGroupName.element)", with: "$\(enumeratedGroupName.offset + 1)") - } - } + /// Replaces all captures groups with the given capture references. References can be numbers like `$1` and capture names like `$prefix`. + public func replaceAllCaptures(in input: String, with template: String) -> String { + replacingMatches(in: input, with: numerizedNamedCaptureRefs(in: template)) + } + + /// Numerizes references to named capture groups to work around missing named capture group replacement in `NSRegularExpression` APIs. + func numerizedNamedCaptureRefs(in replacementString: String) -> String { + let captureGroupNameRegex = Regex(#"\(\?\<([a-zA-Z0-9_-]+)\>[^\)]+\)"#) + let captureGroupNames: [String] = captureGroupNameRegex.matches(in: pattern).map { $0.captures[0]! } + return captureGroupNames.enumerated().reduce(replacementString) { result, enumeratedGroupName in + result.replacingOccurrences(of: "$\(enumeratedGroupName.element)", with: "$\(enumeratedGroupName.offset + 1)") + } + } } diff --git a/Sources/Utility/Extensions/StringExt.swift b/Sources/Utility/Extensions/StringExt.swift index ebbe3f9..6f4d5a0 100644 --- a/Sources/Utility/Extensions/StringExt.swift +++ b/Sources/Utility/Extensions/StringExt.swift @@ -1,53 +1,53 @@ import Foundation extension String { - /// The type of a given file path. - public enum PathType { - /// The relative path. - case relative - - /// The absolute path. - case absolute - } - - /// Returns the absolute path for a path given relative to the current directory. - public var absolutePath: String { - guard !self.starts(with: fileManager.currentDirectoryUrl.path) else { return self } - return fileManager.currentDirectoryUrl.appendingPathComponent(self).path - } - - /// Returns the relative path for a path given relative to the current directory. - public var relativePath: String { - guard self.starts(with: fileManager.currentDirectoryUrl.path) else { return self } - return replacingOccurrences(of: fileManager.currentDirectoryUrl.path, with: "") - } - - /// Returns the parent directory path. - public var parentDirectoryPath: String { - let url = URL(fileURLWithPath: self) - guard url.pathComponents.count > 1 else { return fileManager.currentDirectoryPath } - return url.deletingLastPathComponent().absoluteString - } - - /// Returns the path with the given type related to the current directory. - public func path(type: PathType) -> String { - switch type { - case .absolute: - return absolutePath - - case .relative: - return relativePath - } - } - - /// Returns the path with a components appended at it. - public func appendingPathComponent(_ pathComponent: String) -> String { - guard let pathUrl = URL(string: self) else { - log.message("Could not convert path '\(self)' to type URL.", level: .error) - log.exit(status: .failure) - return "" // only reachable in unit tests - } - - return pathUrl.appendingPathComponent(pathComponent).absoluteString - } + /// The type of a given file path. + public enum PathType { + /// The relative path. + case relative + + /// The absolute path. + case absolute + } + + /// Returns the absolute path for a path given relative to the current directory. + public var absolutePath: String { + guard !self.starts(with: fileManager.currentDirectoryUrl.path) else { return self } + return fileManager.currentDirectoryUrl.appendingPathComponent(self).path + } + + /// Returns the relative path for a path given relative to the current directory. + public var relativePath: String { + guard self.starts(with: fileManager.currentDirectoryUrl.path) else { return self } + return replacingOccurrences(of: fileManager.currentDirectoryUrl.path, with: "") + } + + /// Returns the parent directory path. + public var parentDirectoryPath: String { + let url = URL(fileURLWithPath: self) + guard url.pathComponents.count > 1 else { return fileManager.currentDirectoryPath } + return url.deletingLastPathComponent().absoluteString + } + + /// Returns the path with the given type related to the current directory. + public func path(type: PathType) -> String { + switch type { + case .absolute: + return absolutePath + + case .relative: + return relativePath + } + } + + /// Returns the path with a components appended at it. + public func appendingPathComponent(_ pathComponent: String) -> String { + guard let pathUrl = URL(string: self) else { + log.message("Could not convert path '\(self)' to type URL.", level: .error) + log.exit(status: .failure) + return "" // only reachable in unit tests + } + + return pathUrl.appendingPathComponent(pathComponent).absoluteString + } } diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index 877c120..e2b5775 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -3,157 +3,157 @@ import Rainbow /// Helper to log output to console or elsewhere. public final class Logger { - /// The print level type. - public enum PrintLevel: String { - /// Print success information. - case success - - /// Print any kind of information potentially interesting to users. - case info - - /// Print information that might potentially be problematic. - case warning - - /// Print information that probably is problematic. - case error - - /// Print detailed information for debugging purposes. - case debug - - var color: Color { - switch self { - case .success: - return Color.lightGreen - - case .info: - return Color.lightBlue - - case .warning: - return Color.yellow - - case .error: - return Color.red - - case .debug: - return Color.default - } - } - } - - /// The output type. - public enum OutputType: String { - /// Output is targeted to a console to be read by developers. - case console - - /// Output is targeted to Xcodes left pane to be interpreted by it to mark errors & warnings. - case xcode - - /// Output is targeted for unit tests. Collect into globally accessible TestHelper. - case test - } - - /// The exit status. - public enum ExitStatus { - /// Successfully finished task. - case success - - /// Failed to finish task. - case failure - - var statusCode: Int32 { - switch self { - case .success: - return EXIT_SUCCESS - - case .failure: - return EXIT_FAILURE - } - } - } - - /// The output type of the logger. - public let outputType: OutputType - - /// Defines if the log should include debug logs. - public var logDebugLevel: Bool = false - - /// Initializes a new Logger object with a given output type. - public init(outputType: OutputType) { - self.outputType = outputType - } - - /// Communicates a message to the chosen output target with proper formatting based on level & source. - /// - /// - Parameters: - /// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning. - /// - level: The level of the print statement. - public func message(_ message: String, level: PrintLevel) { - guard level != .debug || logDebugLevel else { return } - - switch outputType { - case .console: - consoleMessage(message, level: level) - - case .xcode: - xcodeMessage(message, level: level) - - case .test: - TestHelper.shared.consoleOutputs.append((message, level)) - } - } - - /// Exits the current program with the given status. - public func exit(status: ExitStatus) { - switch outputType { - case .console, .xcode: - #if os(Linux) - Glibc.exit(status.statusCode) - #else - Darwin.exit(status.statusCode) - #endif - - case .test: - TestHelper.shared.exitStatus = status - } - } - - private func consoleMessage(_ message: String, level: PrintLevel) { - switch level { - case .success: - print(formattedCurrentTime(), "✅", message.green) - - case .info: - print(formattedCurrentTime(), "ℹ️ ", message.lightCyan) - - case .warning: - print(formattedCurrentTime(), "⚠️ ", message.yellow) - - case .error: - print(formattedCurrentTime(), "❌", message.red) - - case .debug: - print(formattedCurrentTime(), "💬", message) - } - } - - /// Reports a message in an Xcode compatible format to be shown in the left pane. - /// - /// - Parameters: - /// - 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 xcodeMessage(_ message: String, level: PrintLevel, location: String? = nil) { - if let location = location { - print("\(location) \(level.rawValue): \(Constants.toolName): \(message)") - } else { - print("\(level.rawValue): \(Constants.toolName): \(message)") - } - } - - private func formattedCurrentTime() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss.SSS" - let dateTime = dateFormatter.string(from: Date()) - return "\(dateTime):" - } + /// The print level type. + public enum PrintLevel: String { + /// Print success information. + case success + + /// Print any kind of information potentially interesting to users. + case info + + /// Print information that might potentially be problematic. + case warning + + /// Print information that probably is problematic. + case error + + /// Print detailed information for debugging purposes. + case debug + + var color: Color { + switch self { + case .success: + return Color.lightGreen + + case .info: + return Color.lightBlue + + case .warning: + return Color.yellow + + case .error: + return Color.red + + case .debug: + return Color.default + } + } + } + + /// The output type. + public enum OutputType: String { + /// Output is targeted to a console to be read by developers. + case console + + /// Output is targeted to Xcodes left pane to be interpreted by it to mark errors & warnings. + case xcode + + /// Output is targeted for unit tests. Collect into globally accessible TestHelper. + case test + } + + /// The exit status. + public enum ExitStatus { + /// Successfully finished task. + case success + + /// Failed to finish task. + case failure + + var statusCode: Int32 { + switch self { + case .success: + return EXIT_SUCCESS + + case .failure: + return EXIT_FAILURE + } + } + } + + /// The output type of the logger. + public let outputType: OutputType + + /// Defines if the log should include debug logs. + public var logDebugLevel: Bool = false + + /// Initializes a new Logger object with a given output type. + public init(outputType: OutputType) { + self.outputType = outputType + } + + /// Communicates a message to the chosen output target with proper formatting based on level & source. + /// + /// - Parameters: + /// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning. + /// - level: The level of the print statement. + public func message(_ message: String, level: PrintLevel) { + guard level != .debug || logDebugLevel else { return } + + switch outputType { + case .console: + consoleMessage(message, level: level) + + case .xcode: + xcodeMessage(message, level: level) + + case .test: + TestHelper.shared.consoleOutputs.append((message, level)) + } + } + + /// Exits the current program with the given status. + public func exit(status: ExitStatus) { + switch outputType { + case .console, .xcode: +#if os(Linux) + Glibc.exit(status.statusCode) +#else + Darwin.exit(status.statusCode) +#endif + + case .test: + TestHelper.shared.exitStatus = status + } + } + + private func consoleMessage(_ message: String, level: PrintLevel) { + switch level { + case .success: + print(formattedCurrentTime(), "✅", message.green) + + case .info: + print(formattedCurrentTime(), "ℹ️ ", message.lightCyan) + + case .warning: + print(formattedCurrentTime(), "⚠️ ", message.yellow) + + case .error: + print(formattedCurrentTime(), "❌", message.red) + + case .debug: + print(formattedCurrentTime(), "💬", message) + } + } + + /// Reports a message in an Xcode compatible format to be shown in the left pane. + /// + /// - Parameters: + /// - 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 xcodeMessage(_ message: String, level: PrintLevel, location: String? = nil) { + if let location = location { + print("\(location) \(level.rawValue): \(Constants.toolName): \(message)") + } else { + print("\(level.rawValue): \(Constants.toolName): \(message)") + } + } + + private func formattedCurrentTime() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss.SSS" + let dateTime = dateFormatter.string(from: Date()) + return "\(dateTime):" + } } diff --git a/Sources/Utility/Regex.swift b/Sources/Utility/Regex.swift index 8c52b30..cf4f0d1 100644 --- a/Sources/Utility/Regex.swift +++ b/Sources/Utility/Regex.swift @@ -4,289 +4,289 @@ import Foundation /// `Regex` is a swifty regex engine built on top of the NSRegularExpression api. public struct Regex { - /// The recommended default options passed to any Regex if not otherwise specified. - public static let defaultOptions: Options = [.anchorsMatchLines] - - // MARK: - Properties - private let regularExpression: NSRegularExpression - - /// The regex patterns string. - public let pattern: String - - /// The regex options. - public let options: Options - - // MARK: - Initializers - /// Create a `Regex` based on a pattern string. - /// - /// If `pattern` is not a valid regular expression, an error is thrown - /// describing the failure. - /// - /// - parameters: - /// - pattern: A pattern string describing the regex. - /// - options: Configure regular expression matching options. - /// For details, see `Regex.Options`. - /// - /// - throws: A value of `ErrorType` describing the invalid regular expression. - public init(_ pattern: String, options: Options = defaultOptions) throws { - self.pattern = pattern - self.options = options - regularExpression = try NSRegularExpression( - pattern: pattern, - options: options.toNSRegularExpressionOptions - ) - } - - // MARK: - Methods: Matching - /// Returns `true` if the regex matches `string`, otherwise returns `false`. - /// - /// - parameter string: The string to test. - /// - /// - returns: `true` if the regular expression matches, otherwise `false`. - public func matches(_ string: String) -> Bool { - firstMatch(in: string) != nil - } - - /// If the regex matches `string`, returns a `Match` describing the - /// first matched string and any captures. If there are no matches, returns - /// `nil`. - /// - /// - parameter string: The string to match against. - /// - /// - returns: An optional `Match` describing the first match, or `nil`. - public func firstMatch(in string: String) -> Match? { - let firstMatch = regularExpression - .firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) - .map { Match(result: $0, in: string) } - return firstMatch - } - - /// If the regex matches `string`, returns an array of `Match`, describing - /// every match inside `string`. If there are no matches, returns an empty - /// array. - /// - /// - parameter string: The string to match against. - /// - /// - returns: An array of `Match` describing every match in `string`. - public func matches(in string: String) -> [Match] { - let matches = regularExpression - .matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) - .map { Match(result: $0, in: string) } - return matches - } - - // MARK: Replacing - /// Returns a new string where each substring matched by `regex` is replaced - /// with `template`. - /// - /// The template string may be a literal string, or include template variables: - /// the variable `$0` will be replaced with the entire matched substring, `$1` - /// with the first capture group, etc. - /// - /// For example, to include the literal string "$1" in the replacement string, - /// you must escape the "$": `\$1`. - /// - /// - parameters: - /// - regex: A regular expression to match against `self`. - /// - template: A template string used to replace matches. - /// - count: The maximum count of matches to replace, beginning with the first match. - /// - /// - returns: A string with all matches of `regex` replaced by `template`. - public func replacingMatches(in input: String, with template: String, count: Int? = nil) -> String { - var output = input - let matches = self.matches(in: input) - let rangedMatches = Array(matches[0 ..< min(matches.count, count ?? .max)]) - for match in rangedMatches.reversed() { - let replacement = match.string(applyingTemplate: template) - output.replaceSubrange(match.range, with: replacement) - } - - return output - } + /// The recommended default options passed to any Regex if not otherwise specified. + public static let defaultOptions: Options = [.anchorsMatchLines] + + // MARK: - Properties + private let regularExpression: NSRegularExpression + + /// The regex patterns string. + public let pattern: String + + /// The regex options. + public let options: Options + + // MARK: - Initializers + /// Create a `Regex` based on a pattern string. + /// + /// If `pattern` is not a valid regular expression, an error is thrown + /// describing the failure. + /// + /// - parameters: + /// - pattern: A pattern string describing the regex. + /// - options: Configure regular expression matching options. + /// For details, see `Regex.Options`. + /// + /// - throws: A value of `ErrorType` describing the invalid regular expression. + public init(_ pattern: String, options: Options = defaultOptions) throws { + self.pattern = pattern + self.options = options + regularExpression = try NSRegularExpression( + pattern: pattern, + options: options.toNSRegularExpressionOptions + ) + } + + // MARK: - Methods: Matching + /// Returns `true` if the regex matches `string`, otherwise returns `false`. + /// + /// - parameter string: The string to test. + /// + /// - returns: `true` if the regular expression matches, otherwise `false`. + public func matches(_ string: String) -> Bool { + firstMatch(in: string) != nil + } + + /// If the regex matches `string`, returns a `Match` describing the + /// first matched string and any captures. If there are no matches, returns + /// `nil`. + /// + /// - parameter string: The string to match against. + /// + /// - returns: An optional `Match` describing the first match, or `nil`. + public func firstMatch(in string: String) -> Match? { + let firstMatch = regularExpression + .firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) + .map { Match(result: $0, in: string) } + return firstMatch + } + + /// If the regex matches `string`, returns an array of `Match`, describing + /// every match inside `string`. If there are no matches, returns an empty + /// array. + /// + /// - parameter string: The string to match against. + /// + /// - returns: An array of `Match` describing every match in `string`. + public func matches(in string: String) -> [Match] { + let matches = regularExpression + .matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) + .map { Match(result: $0, in: string) } + return matches + } + + // MARK: Replacing + /// Returns a new string where each substring matched by `regex` is replaced + /// with `template`. + /// + /// The template string may be a literal string, or include template variables: + /// the variable `$0` will be replaced with the entire matched substring, `$1` + /// with the first capture group, etc. + /// + /// For example, to include the literal string "$1" in the replacement string, + /// you must escape the "$": `\$1`. + /// + /// - parameters: + /// - regex: A regular expression to match against `self`. + /// - template: A template string used to replace matches. + /// - count: The maximum count of matches to replace, beginning with the first match. + /// + /// - returns: A string with all matches of `regex` replaced by `template`. + public func replacingMatches(in input: String, with template: String, count: Int? = nil) -> String { + var output = input + let matches = self.matches(in: input) + let rangedMatches = Array(matches[0 ..< min(matches.count, count ?? .max)]) + for match in rangedMatches.reversed() { + let replacement = match.string(applyingTemplate: template) + output.replaceSubrange(match.range, with: replacement) + } + + return output + } } // MARK: - CustomStringConvertible extension Regex: CustomStringConvertible { - /// Returns a string describing the regex using its pattern string. - public var description: String { - "/\(regularExpression.pattern)/\(options)" - } + /// Returns a string describing the regex using its pattern string. + public var description: String { + "/\(regularExpression.pattern)/\(options)" + } } // MARK: - Equatable extension Regex: Equatable { - /// Determines the equality of to `Regex`` instances. - /// Two `Regex` are considered equal, if both the pattern string and the options - /// passed on initialization are equal. - public static func == (lhs: Regex, rhs: Regex) -> Bool { - lhs.regularExpression.pattern == rhs.regularExpression.pattern && - lhs.regularExpression.options == rhs.regularExpression.options - } + /// Determines the equality of to `Regex`` instances. + /// Two `Regex` are considered equal, if both the pattern string and the options + /// passed on initialization are equal. + public static func == (lhs: Regex, rhs: Regex) -> Bool { + lhs.regularExpression.pattern == rhs.regularExpression.pattern && + lhs.regularExpression.options == rhs.regularExpression.options + } } // MARK: - Hashable extension Regex: Hashable { - /// Manages hashing of the `Regex` instance. - public func hash(into hasher: inout Hasher) { - hasher.combine(pattern) - hasher.combine(options) - } + /// Manages hashing of the `Regex` instance. + public func hash(into hasher: inout Hasher) { + hasher.combine(pattern) + hasher.combine(options) + } } // MARK: - Options extension Regex { - /// `Options` defines alternate behaviours of regular expressions when matching. - public struct Options: OptionSet { - // MARK: - Properties - /// Ignores the case of letters when matching. - public static let ignoreCase = Options(rawValue: 1) - - /// Ignore any metacharacters in the pattern, treating every character as - /// a literal. - public static let ignoreMetacharacters = Options(rawValue: 1 << 1) - - /// By default, "^" matches the beginning of the string and "$" matches the - /// end of the string, ignoring any newlines. With this option, "^" will - /// the beginning of each line, and "$" will match the end of each line. - public static let anchorsMatchLines = Options(rawValue: 1 << 2) - - /// Usually, "." matches all characters except newlines (\n). Using this, - /// options will allow "." to match newLines - public static let dotMatchesLineSeparators = Options(rawValue: 1 << 3) - - /// The raw value of the `OptionSet` - public let rawValue: Int - - /// Transform an instance of `Regex.Options` into the equivalent `NSRegularExpression.Options`. - /// - /// - returns: The equivalent `NSRegularExpression.Options`. - var toNSRegularExpressionOptions: NSRegularExpression.Options { - var options = NSRegularExpression.Options() - if contains(.ignoreCase) { options.insert(.caseInsensitive) } - if contains(.ignoreMetacharacters) { options.insert(.ignoreMetacharacters) } - if contains(.anchorsMatchLines) { options.insert(.anchorsMatchLines) } - if contains(.dotMatchesLineSeparators) { options.insert(.dotMatchesLineSeparators) } - return options - } - - // MARK: - Initializers - /// The raw value init for the `OptionSet` - public init(rawValue: Int) { - self.rawValue = rawValue - } - } + /// `Options` defines alternate behaviours of regular expressions when matching. + public struct Options: OptionSet { + // MARK: - Properties + /// Ignores the case of letters when matching. + public static let ignoreCase = Options(rawValue: 1) + + /// Ignore any metacharacters in the pattern, treating every character as + /// a literal. + public static let ignoreMetacharacters = Options(rawValue: 1 << 1) + + /// By default, "^" matches the beginning of the string and "$" matches the + /// end of the string, ignoring any newlines. With this option, "^" will + /// the beginning of each line, and "$" will match the end of each line. + public static let anchorsMatchLines = Options(rawValue: 1 << 2) + + /// Usually, "." matches all characters except newlines (\n). Using this, + /// options will allow "." to match newLines + public static let dotMatchesLineSeparators = Options(rawValue: 1 << 3) + + /// The raw value of the `OptionSet` + public let rawValue: Int + + /// Transform an instance of `Regex.Options` into the equivalent `NSRegularExpression.Options`. + /// + /// - returns: The equivalent `NSRegularExpression.Options`. + var toNSRegularExpressionOptions: NSRegularExpression.Options { + var options = NSRegularExpression.Options() + if contains(.ignoreCase) { options.insert(.caseInsensitive) } + if contains(.ignoreMetacharacters) { options.insert(.ignoreMetacharacters) } + if contains(.anchorsMatchLines) { options.insert(.anchorsMatchLines) } + if contains(.dotMatchesLineSeparators) { options.insert(.dotMatchesLineSeparators) } + return options + } + + // MARK: - Initializers + /// The raw value init for the `OptionSet` + public init(rawValue: Int) { + self.rawValue = rawValue + } + } } extension Regex.Options: CustomStringConvertible { - public var description: String { - var description = "" - if contains(.ignoreCase) { description += "i" } - if contains(.ignoreMetacharacters) { description += "x" } - if !contains(.anchorsMatchLines) { description += "a" } - if contains(.dotMatchesLineSeparators) { description += "m" } - return description - } + public var description: String { + var description = "" + if contains(.ignoreCase) { description += "i" } + if contains(.ignoreMetacharacters) { description += "x" } + if !contains(.anchorsMatchLines) { description += "a" } + if contains(.dotMatchesLineSeparators) { description += "m" } + return description + } } extension Regex.Options: Equatable, Hashable { - public static func == (lhs: Regex.Options, rhs: Regex.Options) -> Bool { - lhs.rawValue == rhs.rawValue - } + public static func == (lhs: Regex.Options, rhs: Regex.Options) -> Bool { + lhs.rawValue == rhs.rawValue + } - public func hash(into hasher: inout Hasher) { - hasher.combine(rawValue) - } + public func hash(into hasher: inout Hasher) { + hasher.combine(rawValue) + } } // MARK: - Match extension Regex { - /// A `Match` encapsulates the result of a single match in a string, - /// providing access to the matched string, as well as any capture groups within - /// that string. - public class Match: CustomStringConvertible { - // MARK: Properties - /// The entire matched string. - public lazy var string: String = { - String(describing: self.baseString[self.range]) - }() - - /// The range of the matched string. - public lazy var range: Range = { - Range(self.result.range, in: self.baseString)! - }() - - /// The matching string for each capture group in the regular expression - /// (if any). - /// - /// **Note:** Usually if the match was successful, the captures will by - /// definition be non-nil. However if a given capture group is optional, the - /// captured string may also be nil, depending on the particular string that - /// is being matched against. - /// - /// Example: - /// - /// let regex = Regex("(a)?(b)") - /// - /// regex.matches(in: "ab")first?.captures // [Optional("a"), Optional("b")] - /// regex.matches(in: "b").first?.captures // [nil, Optional("b")] - public lazy var captures: [String?] = { - let captureRanges = stride(from: 0, to: result.numberOfRanges, by: 1) - .map(result.range) - .dropFirst() - .map { [unowned self] in - Range($0, in: self.baseString) - } - - return captureRanges.map { [unowned self] captureRange in - guard let captureRange = captureRange else { return nil } - return String(describing: self.baseString[captureRange]) + /// A `Match` encapsulates the result of a single match in a string, + /// providing access to the matched string, as well as any capture groups within + /// that string. + public class Match: CustomStringConvertible { + // MARK: Properties + /// The entire matched string. + public lazy var string: String = { + String(describing: self.baseString[self.range]) + }() + + /// The range of the matched string. + public lazy var range: Range = { + Range(self.result.range, in: self.baseString)! + }() + + /// The matching string for each capture group in the regular expression + /// (if any). + /// + /// **Note:** Usually if the match was successful, the captures will by + /// definition be non-nil. However if a given capture group is optional, the + /// captured string may also be nil, depending on the particular string that + /// is being matched against. + /// + /// Example: + /// + /// let regex = Regex("(a)?(b)") + /// + /// regex.matches(in: "ab")first?.captures // [Optional("a"), Optional("b")] + /// regex.matches(in: "b").first?.captures // [nil, Optional("b")] + public lazy var captures: [String?] = { + let captureRanges = stride(from: 0, to: result.numberOfRanges, by: 1) + .map(result.range) + .dropFirst() + .map { [unowned self] in + Range($0, in: self.baseString) } - }() - - let result: NSTextCheckingResult - - let baseString: String - - // MARK: - Initializers - internal init(result: NSTextCheckingResult, in string: String) { - precondition( - result.regularExpression != nil, - "NSTextCheckingResult must originate from regular expression parsing." - ) - - self.result = result - self.baseString = string - } - - // MARK: - Methods - /// Returns a new string where the matched string is replaced according to the `template`. - /// - /// The template string may be a literal string, or include template variables: - /// the variable `$0` will be replaced with the entire matched substring, `$1` - /// with the first capture group, etc. - /// - /// For example, to include the literal string "$1" in the replacement string, - /// you must escape the "$": `\$1`. - /// - /// - parameters: - /// - template: The template string used to replace matches. - /// - /// - returns: A string with `template` applied to the matched string. - public func string(applyingTemplate template: String) -> String { - let replacement = result.regularExpression!.replacementString( - for: result, - in: baseString, - offset: 0, - template: template - ) - - return replacement - } - - // MARK: - CustomStringConvertible - /// Returns a string describing the match. - public var description: String { - "Match<\"\(string)\">" - } - } + + return captureRanges.map { [unowned self] captureRange in + guard let captureRange = captureRange else { return nil } + return String(describing: self.baseString[captureRange]) + } + }() + + let result: NSTextCheckingResult + + let baseString: String + + // MARK: - Initializers + internal init(result: NSTextCheckingResult, in string: String) { + precondition( + result.regularExpression != nil, + "NSTextCheckingResult must originate from regular expression parsing." + ) + + self.result = result + self.baseString = string + } + + // MARK: - Methods + /// Returns a new string where the matched string is replaced according to the `template`. + /// + /// The template string may be a literal string, or include template variables: + /// the variable `$0` will be replaced with the entire matched substring, `$1` + /// with the first capture group, etc. + /// + /// For example, to include the literal string "$1" in the replacement string, + /// you must escape the "$": `\$1`. + /// + /// - parameters: + /// - template: The template string used to replace matches. + /// + /// - returns: A string with `template` applied to the matched string. + public func string(applyingTemplate template: String) -> String { + let replacement = result.regularExpression!.replacementString( + for: result, + in: baseString, + offset: 0, + template: template + ) + + return replacement + } + + // MARK: - CustomStringConvertible + /// Returns a string describing the match. + public var description: String { + "Match<\"\(string)\">" + } + } } diff --git a/Sources/Utility/TestHelper.swift b/Sources/Utility/TestHelper.swift index 1b72080..5bbf0ac 100644 --- a/Sources/Utility/TestHelper.swift +++ b/Sources/Utility/TestHelper.swift @@ -2,21 +2,21 @@ import Foundation /// A helper class for Unit Testing only. public final class TestHelper { - /// The console output data. - public typealias ConsoleOutput = (message: String, level: Logger.PrintLevel) - - /// The shared `TestHelper` object. - public static let shared = TestHelper() - - /// Use only in Unit Tests. - public var consoleOutputs: [ConsoleOutput] = [] - - /// Use only in Unit Tests. - public var exitStatus: Logger.ExitStatus? - - /// Deletes all data collected until now. - public func reset() { - consoleOutputs = [] - exitStatus = nil - } + /// The console output data. + public typealias ConsoleOutput = (message: String, level: Logger.PrintLevel) + + /// The shared `TestHelper` object. + public static let shared = TestHelper() + + /// Use only in Unit Tests. + public var consoleOutputs: [ConsoleOutput] = [] + + /// Use only in Unit Tests. + public var exitStatus: Logger.ExitStatus? + + /// Deletes all data collected until now. + public func reset() { + consoleOutputs = [] + exitStatus = nil + } } diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift index 5be114c..953b8f8 100644 --- a/Tests/AnyLintCLITests/AnyLintCLITests.swift +++ b/Tests/AnyLintCLITests/AnyLintCLITests.swift @@ -1,7 +1,7 @@ import XCTest final class AnyLintCLITests: XCTestCase { - func testExample() { - // TODO: [cg_2020-03-07] not yet implemented - } + func testExample() { + // TODO: [cg_2020-03-07] not yet implemented + } } diff --git a/Tests/AnyLintTests/AutoCorrectionTests.swift b/Tests/AnyLintTests/AutoCorrectionTests.swift index f554ec5..f0bff5e 100644 --- a/Tests/AnyLintTests/AutoCorrectionTests.swift +++ b/Tests/AnyLintTests/AutoCorrectionTests.swift @@ -2,36 +2,36 @@ import XCTest final class AutoCorrectionTests: XCTestCase { - func testInitWithDictionaryLiteral() { - let autoCorrection: AutoCorrection = ["before": "Lisence", "after": "License"] - XCTAssertEqual(autoCorrection.before, "Lisence") - XCTAssertEqual(autoCorrection.after, "License") - } - - func testAppliedMessageLines() { - let singleLineAutoCorrection: AutoCorrection = ["before": "Lisence", "after": "License"] - XCTAssertEqual( - singleLineAutoCorrection.appliedMessageLines, - [ - "Autocorrection applied, the diff is: (+ added, - removed)", - "- Lisence", - "+ License", - ] - ) - - let multiLineAutoCorrection: AutoCorrection = [ - "before": "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", - "after": "A\nB\nD\nE\nF1\nF2\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", - ] - XCTAssertEqual( - multiLineAutoCorrection.appliedMessageLines, - [ - "Autocorrection applied, the diff is: (+ added, - removed)", - "- [L3] C", - "+ [L5] F1", - "- [L6] F", - "+ [L6] F2", - ] - ) - } + func testInitWithDictionaryLiteral() { + let autoCorrection: AutoCorrection = ["before": "Lisence", "after": "License"] + XCTAssertEqual(autoCorrection.before, "Lisence") + XCTAssertEqual(autoCorrection.after, "License") + } + + func testAppliedMessageLines() { + let singleLineAutoCorrection: AutoCorrection = ["before": "Lisence", "after": "License"] + XCTAssertEqual( + singleLineAutoCorrection.appliedMessageLines, + [ + "Autocorrection applied, the diff is: (+ added, - removed)", + "- Lisence", + "+ License", + ] + ) + + let multiLineAutoCorrection: AutoCorrection = [ + "before": "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", + "after": "A\nB\nD\nE\nF1\nF2\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", + ] + XCTAssertEqual( + multiLineAutoCorrection.appliedMessageLines, + [ + "Autocorrection applied, the diff is: (+ added, - removed)", + "- [L3] C", + "+ [L5] F1", + "- [L6] F", + "+ [L6] F2", + ] + ) + } } diff --git a/Tests/AnyLintTests/CheckInfoTests.swift b/Tests/AnyLintTests/CheckInfoTests.swift index 91e25ee..422372c 100644 --- a/Tests/AnyLintTests/CheckInfoTests.swift +++ b/Tests/AnyLintTests/CheckInfoTests.swift @@ -3,32 +3,32 @@ import XCTest final class CheckInfoTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testInitWithStringLiteral() { - XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) - - let checkInfo1: CheckInfo = "test1@error: hint1" - XCTAssertEqual(checkInfo1.id, "test1") - XCTAssertEqual(checkInfo1.hint, "hint1") - XCTAssertEqual(checkInfo1.severity, .error) - - let checkInfo2: CheckInfo = "test2@warning: hint2" - XCTAssertEqual(checkInfo2.id, "test2") - XCTAssertEqual(checkInfo2.hint, "hint2") - XCTAssertEqual(checkInfo2.severity, .warning) - - let checkInfo3: CheckInfo = "test3@info: hint3" - XCTAssertEqual(checkInfo3.id, "test3") - XCTAssertEqual(checkInfo3.hint, "hint3") - XCTAssertEqual(checkInfo3.severity, .info) - - let checkInfo4: CheckInfo = "test4: hint4" - XCTAssertEqual(checkInfo4.id, "test4") - XCTAssertEqual(checkInfo4.hint, "hint4") - XCTAssertEqual(checkInfo4.severity, .warning) - } + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testInitWithStringLiteral() { + XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) + + let checkInfo1: CheckInfo = "test1@error: hint1" + XCTAssertEqual(checkInfo1.id, "test1") + XCTAssertEqual(checkInfo1.hint, "hint1") + XCTAssertEqual(checkInfo1.severity, .error) + + let checkInfo2: CheckInfo = "test2@warning: hint2" + XCTAssertEqual(checkInfo2.id, "test2") + XCTAssertEqual(checkInfo2.hint, "hint2") + XCTAssertEqual(checkInfo2.severity, .warning) + + let checkInfo3: CheckInfo = "test3@info: hint3" + XCTAssertEqual(checkInfo3.id, "test3") + XCTAssertEqual(checkInfo3.hint, "hint3") + XCTAssertEqual(checkInfo3.severity, .info) + + let checkInfo4: CheckInfo = "test4: hint4" + XCTAssertEqual(checkInfo4.id, "test4") + XCTAssertEqual(checkInfo4.hint, "hint4") + XCTAssertEqual(checkInfo4.severity, .warning) + } } diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index 5f51209..15a2405 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -5,218 +5,218 @@ import XCTest // swiftlint:disable function_body_length final class FileContentsCheckerTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testPerformCheck() { - let temporaryFiles: [TemporaryFile] = [ - (subpath: "Sources/Hello.swift", contents: "let x = 5\nvar y = 10"), - (subpath: "Sources/World.swift", contents: "let x=5\nvar y=10"), - ] - - withTemporaryFiles(temporaryFiles) { filePathsToCheck in - let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: #"(let|var) \w+=\w+"#, - violationLocation: .init(range: .fullMatch, bound: .lower), - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: nil, - repeatIfAutoCorrected: false - ).performCheck() - - XCTAssertEqual(violations.count, 2) - - XCTAssertEqual(violations[0].checkInfo, checkInfo) - XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[0].locationInfo!.line, 1) - XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[1].checkInfo, checkInfo) - XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].locationInfo!.line, 2) - XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) - } - } - - func testSkipInFile() { - let temporaryFiles: [TemporaryFile] = [ - (subpath: "Sources/Hello.swift", contents: "// AnyLint.skipInFile: OtherRule, Whitespacing\n\n\nlet x=5\nvar y=10"), - (subpath: "Sources/World.swift", contents: "// AnyLint.skipInFile: All\n\n\nlet x=5\nvar y=10"), - (subpath: "Sources/Foo.swift", contents: "// AnyLint.skipInFile: OtherRule\n\n\nlet x=5\nvar y=10"), - ] - - withTemporaryFiles(temporaryFiles) { filePathsToCheck in - let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: #"(let|var) \w+=\w+"#, - violationLocation: .init(range: .fullMatch, bound: .lower), - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: nil, - repeatIfAutoCorrected: false - ).performCheck() - - XCTAssertEqual(violations.count, 2) - - XCTAssertEqual(violations[0].checkInfo, checkInfo) - XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[0].locationInfo!.line, 4) - XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[1].checkInfo, checkInfo) - XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[1].locationInfo!.line, 5) - XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) - } - } - - func testSkipHere() { - let temporaryFiles: [TemporaryFile] = [ - (subpath: "Sources/Hello.swift", contents: "// AnyLint.skipHere: OtherRule, Whitespacing\n\n\nlet x=5\nvar y=10"), - (subpath: "Sources/World.swift", contents: "\n\n// AnyLint.skipHere: OtherRule, Whitespacing\nlet x=5\nvar y=10"), - (subpath: "Sources/Foo.swift", contents: "\n\n\nlet x=5\nvar y=10 // AnyLint.skipHere: OtherRule, Whitespacing\n"), - (subpath: "Sources/Bar.swift", contents: "\n\n\nlet x=5\nvar y=10\n// AnyLint.skipHere: OtherRule, Whitespacing"), - ] - - withTemporaryFiles(temporaryFiles) { filePathsToCheck in - let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: #"(let|var) \w+=\w+"#, - violationLocation: .init(range: .fullMatch, bound: .lower), - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: nil, - repeatIfAutoCorrected: false - ).performCheck() - - XCTAssertEqual(violations.count, 6) - - XCTAssertEqual(violations[0].checkInfo, checkInfo) - XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[0].locationInfo!.line, 4) - XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[1].checkInfo, checkInfo) - XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[1].locationInfo!.line, 5) - XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[2].checkInfo, checkInfo) - XCTAssertEqual(violations[2].filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[2].locationInfo!.line, 5) - XCTAssertEqual(violations[2].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[3].checkInfo, checkInfo) - XCTAssertEqual(violations[3].filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[3].locationInfo!.line, 4) - XCTAssertEqual(violations[3].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[4].checkInfo, checkInfo) - XCTAssertEqual(violations[4].filePath, "\(tempDir)/Sources/Bar.swift") - XCTAssertEqual(violations[4].locationInfo!.line, 4) - XCTAssertEqual(violations[4].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[5].checkInfo, checkInfo) - XCTAssertEqual(violations[5].filePath, "\(tempDir)/Sources/Bar.swift") - XCTAssertEqual(violations[5].locationInfo!.line, 5) - XCTAssertEqual(violations[5].locationInfo!.charInLine, 1) - } - } - - func testSkipIfEqualsToAutocorrectReplacement() { - let temporaryFiles: [TemporaryFile] = [ - (subpath: "Sources/Hello.swift", contents: "let x = 5\nvar y = 10"), - (subpath: "Sources/World.swift", contents: "let x =5\nvar y= 10"), - ] - - withTemporaryFiles(temporaryFiles) { filePathsToCheck in - let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: #"(let|var) (\w+)\s*=\s*(\w+)"#, - violationLocation: .init(range: .fullMatch, bound: .lower), - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: "$1 $2 = $3", - repeatIfAutoCorrected: false - ).performCheck() - - XCTAssertEqual(violations.count, 2) - - XCTAssertEqual(violations[0].checkInfo, checkInfo) - XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[0].locationInfo!.line, 1) - XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - - XCTAssertEqual(violations[1].checkInfo, checkInfo) - XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].locationInfo!.line, 2) - XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) - } - } - - func testRepeatIfAutoCorrected() { - let temporaryFiles: [TemporaryFile] = [ - (subpath: "Sources/Hello.swift", contents: "let x = 500\nvar y = 10000"), - (subpath: "Sources/World.swift", contents: "let x = 50000000\nvar y = 100000000000000"), - ] - - withTemporaryFiles(temporaryFiles) { filePathsToCheck in - let checkInfo = CheckInfo(id: "LongNumbers", hint: "Format long numbers with `_` after each triple of digits from the right.", severity: .warning) - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: #"(? FilePathsChecker { - FilePathsChecker( - checkInfo: sayHelloCheck(), - regex: #".*Hello\.swift"#, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: nil, - violateIfNoMatchesFound: true - ) - } - - private func sayHelloCheck() -> CheckInfo { - CheckInfo(id: "say_hello", hint: "Should always say hello.", severity: .info) - } - - private func noWorldChecker(filePathsToCheck: [String]) -> FilePathsChecker { - FilePathsChecker( - checkInfo: noWorldCheck(), - regex: #".*World\.swift"#, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: nil, - violateIfNoMatchesFound: false - ) - } - - private func noWorldCheck() -> CheckInfo { - CheckInfo(id: "no_world", hint: "Do not include the global world, be more specific instead.", severity: .error) - } + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testPerformCheck() { + withTemporaryFiles( + [ + (subpath: "Sources/Hello.swift", contents: ""), + (subpath: "Sources/World.swift", contents: ""), + ] + ) { filePathsToCheck in + let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + XCTAssertEqual(violations.count, 0) + } + + withTemporaryFiles([(subpath: "Sources/World.swift", contents: "")]) { filePathsToCheck in + let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + + XCTAssertEqual(violations.count, 1) + + XCTAssertEqual(violations[0].checkInfo, sayHelloCheck()) + XCTAssertNil(violations[0].filePath) + XCTAssertNil(violations[0].locationInfo) + XCTAssertNil(violations[0].locationInfo) + } + + withTemporaryFiles( + [ + (subpath: "Sources/Hello.swift", contents: ""), + (subpath: "Sources/World.swift", contents: ""), + ] + ) { filePathsToCheck in + let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() + + XCTAssertEqual(violations.count, 1) + + XCTAssertEqual(violations[0].checkInfo, noWorldCheck()) + XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") + XCTAssertNil(violations[0].locationInfo) + XCTAssertNil(violations[0].locationInfo) + } + } + + private func sayHelloChecker(filePathsToCheck: [String]) -> FilePathsChecker { + FilePathsChecker( + checkInfo: sayHelloCheck(), + regex: #".*Hello\.swift"#, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, + violateIfNoMatchesFound: true + ) + } + + private func sayHelloCheck() -> CheckInfo { + CheckInfo(id: "say_hello", hint: "Should always say hello.", severity: .info) + } + + private func noWorldChecker(filePathsToCheck: [String]) -> FilePathsChecker { + FilePathsChecker( + checkInfo: noWorldCheck(), + regex: #".*World\.swift"#, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, + violateIfNoMatchesFound: false + ) + } + + private func noWorldCheck() -> CheckInfo { + CheckInfo(id: "no_world", hint: "Do not include the global world, be more specific instead.", severity: .error) + } } diff --git a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift b/Tests/AnyLintTests/Extensions/ArrayExtTests.swift index 6162f5b..a674a54 100644 --- a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift +++ b/Tests/AnyLintTests/Extensions/ArrayExtTests.swift @@ -3,17 +3,17 @@ import XCTest final class ArrayExtTests: XCTestCase { - func testContainsLineAtIndexesMatchingRegex() { - let regex: Regex = #"foo:bar"# - let lines: [String] = ["hello\n foo", "hello\n foo bar", "hello bar", "\nfoo:\nbar", "foo:bar", ":foo:bar"] - - XCTAssertFalse(lines.containsLine(at: [1, 2, 3], matchingRegex: regex)) - XCTAssertFalse(lines.containsLine(at: [-2, -1, 0], matchingRegex: regex)) - XCTAssertFalse(lines.containsLine(at: [-1, 2, 10], matchingRegex: regex)) - XCTAssertFalse(lines.containsLine(at: [3, 2], matchingRegex: regex)) - - XCTAssertTrue(lines.containsLine(at: [-2, 3, 4], matchingRegex: regex)) - XCTAssertTrue(lines.containsLine(at: [5, 6, 7], matchingRegex: regex)) - XCTAssertTrue(lines.containsLine(at: [-2, 4, 10], matchingRegex: regex)) - } + func testContainsLineAtIndexesMatchingRegex() { + let regex: Regex = #"foo:bar"# + let lines: [String] = ["hello\n foo", "hello\n foo bar", "hello bar", "\nfoo:\nbar", "foo:bar", ":foo:bar"] + + XCTAssertFalse(lines.containsLine(at: [1, 2, 3], matchingRegex: regex)) + XCTAssertFalse(lines.containsLine(at: [-2, -1, 0], matchingRegex: regex)) + XCTAssertFalse(lines.containsLine(at: [-1, 2, 10], matchingRegex: regex)) + XCTAssertFalse(lines.containsLine(at: [3, 2], matchingRegex: regex)) + + XCTAssertTrue(lines.containsLine(at: [-2, 3, 4], matchingRegex: regex)) + XCTAssertTrue(lines.containsLine(at: [5, 6, 7], matchingRegex: regex)) + XCTAssertTrue(lines.containsLine(at: [-2, 4, 10], matchingRegex: regex)) + } } diff --git a/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift index a59d9d8..b31a729 100644 --- a/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift +++ b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift @@ -3,23 +3,23 @@ import Foundation import XCTest extension XCTestCase { - typealias TemporaryFile = (subpath: String, contents: String) - - var tempDir: String { "AnyLintTempTests" } - - func withTemporaryFiles(_ temporaryFiles: [TemporaryFile], testCode: ([String]) throws -> Void) { - var filePathsToCheck: [String] = [] - - for tempFile in temporaryFiles { - let tempFileUrl = FileManager.default.currentDirectoryUrl.appendingPathComponent(tempDir).appendingPathComponent(tempFile.subpath) - let tempFileParentDirUrl = tempFileUrl.deletingLastPathComponent() - try? FileManager.default.createDirectory(atPath: tempFileParentDirUrl.path, withIntermediateDirectories: true, attributes: nil) - FileManager.default.createFile(atPath: tempFileUrl.path, contents: tempFile.contents.data(using: .utf8), attributes: nil) - filePathsToCheck.append(tempFileUrl.relativePathFromCurrent) - } - - try? testCode(filePathsToCheck) - - try? FileManager.default.removeItem(atPath: tempDir) - } + typealias TemporaryFile = (subpath: String, contents: String) + + var tempDir: String { "AnyLintTempTests" } + + func withTemporaryFiles(_ temporaryFiles: [TemporaryFile], testCode: ([String]) throws -> Void) { + var filePathsToCheck: [String] = [] + + for tempFile in temporaryFiles { + let tempFileUrl = FileManager.default.currentDirectoryUrl.appendingPathComponent(tempDir).appendingPathComponent(tempFile.subpath) + let tempFileParentDirUrl = tempFileUrl.deletingLastPathComponent() + try? FileManager.default.createDirectory(atPath: tempFileParentDirUrl.path, withIntermediateDirectories: true, attributes: nil) + FileManager.default.createFile(atPath: tempFileUrl.path, contents: tempFile.contents.data(using: .utf8), attributes: nil) + filePathsToCheck.append(tempFileUrl.relativePathFromCurrent) + } + + try? testCode(filePathsToCheck) + + try? FileManager.default.removeItem(atPath: tempDir) + } } diff --git a/Tests/AnyLintTests/FilesSearchTests.swift b/Tests/AnyLintTests/FilesSearchTests.swift index 5b78388..3650794 100644 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -5,58 +5,58 @@ import XCTest // swiftlint:disable force_try final class FilesSearchTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testAllFilesWithinPath() { - withTemporaryFiles( - [ - (subpath: "Sources/Hello.swift", contents: ""), - (subpath: "Sources/World.swift", contents: ""), - (subpath: "Sources/.hidden_file", contents: ""), - (subpath: "Sources/.hidden_dir/unhidden_file", contents: ""), - ] - ) { _ in - let includeFilterFilePaths = FilesSearch.shared.allFiles( - within: FileManager.default.currentDirectoryPath, - includeFilters: [try Regex("\(tempDir)/.*")], - excludeFilters: [] - ).sorted() - XCTAssertEqual(includeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift", "\(tempDir)/Sources/World.swift"]) - - let excludeFilterFilePaths = FilesSearch.shared.allFiles( - within: FileManager.default.currentDirectoryPath, - includeFilters: [try Regex("\(tempDir)/.*")], - excludeFilters: ["World"] + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testAllFilesWithinPath() { + withTemporaryFiles( + [ + (subpath: "Sources/Hello.swift", contents: ""), + (subpath: "Sources/World.swift", contents: ""), + (subpath: "Sources/.hidden_file", contents: ""), + (subpath: "Sources/.hidden_dir/unhidden_file", contents: ""), + ] + ) { _ in + let includeFilterFilePaths = FilesSearch.shared.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [try Regex("\(tempDir)/.*")], + excludeFilters: [] + ).sorted() + XCTAssertEqual(includeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift", "\(tempDir)/Sources/World.swift"]) + + let excludeFilterFilePaths = FilesSearch.shared.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [try Regex("\(tempDir)/.*")], + excludeFilters: ["World"] + ) + XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift"]) + } + } + + func testPerformanceOfSameSearchOptions() { + let swiftSourcesFilePaths = (0 ... 800).map { (subpath: "Sources/Foo\($0).swift", contents: "Lorem ipsum\ndolor sit amet\n") } + let testsFilePaths = (0 ... 400).map { (subpath: "Tests/Foo\($0).swift", contents: "Lorem ipsum\ndolor sit amet\n") } + let storyboardSourcesFilePaths = (0 ... 300).map { (subpath: "Sources/Foo\($0).storyboard", contents: "Lorem ipsum\ndolor sit amet\n") } + + withTemporaryFiles(swiftSourcesFilePaths + testsFilePaths + storyboardSourcesFilePaths) { _ in + let fileSearchCode: () -> [String] = { + FilesSearch.shared.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [try! Regex(#"\#(self.tempDir)/Sources/Foo.*"#)], + excludeFilters: [try! Regex(#"\#(self.tempDir)/.*\.storyboard"#)] ) - XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift"]) - } - } - - func testPerformanceOfSameSearchOptions() { - let swiftSourcesFilePaths = (0 ... 800).map { (subpath: "Sources/Foo\($0).swift", contents: "Lorem ipsum\ndolor sit amet\n") } - let testsFilePaths = (0 ... 400).map { (subpath: "Tests/Foo\($0).swift", contents: "Lorem ipsum\ndolor sit amet\n") } - let storyboardSourcesFilePaths = (0 ... 300).map { (subpath: "Sources/Foo\($0).storyboard", contents: "Lorem ipsum\ndolor sit amet\n") } - - withTemporaryFiles(swiftSourcesFilePaths + testsFilePaths + storyboardSourcesFilePaths) { _ in - let fileSearchCode: () -> [String] = { - FilesSearch.shared.allFiles( - within: FileManager.default.currentDirectoryPath, - includeFilters: [try! Regex(#"\#(self.tempDir)/Sources/Foo.*"#)], - excludeFilters: [try! Regex(#"\#(self.tempDir)/.*\.storyboard"#)] - ) - } - - // first run + } + + // first run + XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) + + measure { + // subsequent runs (should be much faster) XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) - - measure { - // subsequent runs (should be much faster) - XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) - XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) - } - } - } + XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) + } + } + } } diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index 99402c6..e42e631 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -3,116 +3,116 @@ import XCTest final class LintTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testValidateRegexMatchesForEach() { - XCTAssertNil(TestHelper.shared.exitStatus) - - let regex: Regex = #"foo[0-9]?bar"# - let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) - - Lint.validate( - regex: regex, - matchesForEach: ["foo1bar", "foobar", "myfoo4barbeque"], - checkInfo: checkInfo - ) - XCTAssertNil(TestHelper.shared.exitStatus) - - Lint.validate( - regex: regex, - matchesForEach: ["foo1bar", "FooBar", "myfoo4barbeque"], - checkInfo: checkInfo - ) - XCTAssertEqual(TestHelper.shared.exitStatus, .failure) - } - - func testValidateRegexDoesNotMatchAny() { - XCTAssertNil(TestHelper.shared.exitStatus) - - let regex: Regex = #"foo[0-9]?bar"# - let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) - - Lint.validate( - regex: regex, - doesNotMatchAny: ["fooLbar", "FooBar", "myfoo40barbeque"], - checkInfo: checkInfo - ) - XCTAssertNil(TestHelper.shared.exitStatus) - - Lint.validate( - regex: regex, - doesNotMatchAny: ["fooLbar", "foobar", "myfoo40barbeque"], - checkInfo: checkInfo - ) - XCTAssertEqual(TestHelper.shared.exitStatus, .failure) - } - - func testValidateAutocorrectsAllExamplesWithAnonymousGroups() { - XCTAssertNil(TestHelper.shared.exitStatus) - - let anonymousCaptureRegex = try? Regex(#"([^\.]+)(\.)([^\.]+)(\.)([^\.]+)"#) - - Lint.validateAutocorrectsAll( - checkInfo: CheckInfo(id: "id", hint: "hint"), - examples: [ - AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), - AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), - ], - regex: anonymousCaptureRegex!, - autocorrectReplacement: "$5$2$3$4$1" - ) - - XCTAssertNil(TestHelper.shared.exitStatus) - - Lint.validateAutocorrectsAll( - checkInfo: CheckInfo(id: "id", hint: "hint"), - examples: [ - AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), - AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), - ], - regex: anonymousCaptureRegex!, - autocorrectReplacement: "$4$1$2$3$0" - ) - - XCTAssertEqual(TestHelper.shared.exitStatus, .failure) - } - - func testValidateAutocorrectsAllExamplesWithNamedGroups() { - XCTAssertNil(TestHelper.shared.exitStatus) - - let namedCaptureRegex: Regex = [ - "prefix": #"[^\.]+"#, - "separator1": #"\."#, - "content": #"[^\.]+"#, - "separator2": #"\."#, - "suffix": #"[^\.]+"#, - ] - - Lint.validateAutocorrectsAll( - checkInfo: CheckInfo(id: "id", hint: "hint"), - examples: [ - AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), - AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), - ], - regex: namedCaptureRegex, - autocorrectReplacement: "$suffix$separator1$content$separator2$prefix" - ) - - XCTAssertNil(TestHelper.shared.exitStatus) - - Lint.validateAutocorrectsAll( - checkInfo: CheckInfo(id: "id", hint: "hint"), - examples: [ - AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), - AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), - ], - regex: namedCaptureRegex, - autocorrectReplacement: "$sfx$sep1$cnt$sep2$pref" - ) - - XCTAssertEqual(TestHelper.shared.exitStatus, .failure) - } + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testValidateRegexMatchesForEach() { + XCTAssertNil(TestHelper.shared.exitStatus) + + let regex: Regex = #"foo[0-9]?bar"# + let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) + + Lint.validate( + regex: regex, + matchesForEach: ["foo1bar", "foobar", "myfoo4barbeque"], + checkInfo: checkInfo + ) + XCTAssertNil(TestHelper.shared.exitStatus) + + Lint.validate( + regex: regex, + matchesForEach: ["foo1bar", "FooBar", "myfoo4barbeque"], + checkInfo: checkInfo + ) + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) + } + + func testValidateRegexDoesNotMatchAny() { + XCTAssertNil(TestHelper.shared.exitStatus) + + let regex: Regex = #"foo[0-9]?bar"# + let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) + + Lint.validate( + regex: regex, + doesNotMatchAny: ["fooLbar", "FooBar", "myfoo40barbeque"], + checkInfo: checkInfo + ) + XCTAssertNil(TestHelper.shared.exitStatus) + + Lint.validate( + regex: regex, + doesNotMatchAny: ["fooLbar", "foobar", "myfoo40barbeque"], + checkInfo: checkInfo + ) + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) + } + + func testValidateAutocorrectsAllExamplesWithAnonymousGroups() { + XCTAssertNil(TestHelper.shared.exitStatus) + + let anonymousCaptureRegex = try? Regex(#"([^\.]+)(\.)([^\.]+)(\.)([^\.]+)"#) + + Lint.validateAutocorrectsAll( + checkInfo: CheckInfo(id: "id", hint: "hint"), + examples: [ + AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), + AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), + ], + regex: anonymousCaptureRegex!, + autocorrectReplacement: "$5$2$3$4$1" + ) + + XCTAssertNil(TestHelper.shared.exitStatus) + + Lint.validateAutocorrectsAll( + checkInfo: CheckInfo(id: "id", hint: "hint"), + examples: [ + AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), + AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), + ], + regex: anonymousCaptureRegex!, + autocorrectReplacement: "$4$1$2$3$0" + ) + + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) + } + + func testValidateAutocorrectsAllExamplesWithNamedGroups() { + XCTAssertNil(TestHelper.shared.exitStatus) + + let namedCaptureRegex: Regex = [ + "prefix": #"[^\.]+"#, + "separator1": #"\."#, + "content": #"[^\.]+"#, + "separator2": #"\."#, + "suffix": #"[^\.]+"#, + ] + + Lint.validateAutocorrectsAll( + checkInfo: CheckInfo(id: "id", hint: "hint"), + examples: [ + AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), + AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), + ], + regex: namedCaptureRegex, + autocorrectReplacement: "$suffix$separator1$content$separator2$prefix" + ) + + XCTAssertNil(TestHelper.shared.exitStatus) + + Lint.validateAutocorrectsAll( + checkInfo: CheckInfo(id: "id", hint: "hint"), + examples: [ + AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), + AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), + ], + regex: namedCaptureRegex, + autocorrectReplacement: "$sfx$sep1$cnt$sep2$pref" + ) + + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) + } } diff --git a/Tests/AnyLintTests/RegexExtTests.swift b/Tests/AnyLintTests/RegexExtTests.swift index b729aa7..f869c96 100644 --- a/Tests/AnyLintTests/RegexExtTests.swift +++ b/Tests/AnyLintTests/RegexExtTests.swift @@ -3,16 +3,16 @@ import XCTest final class RegexExtTests: XCTestCase { - func testInitWithStringLiteral() { - let regex: Regex = #"(?capture[_\-\.]group)\s+\n(.*)"# - XCTAssertEqual(regex.pattern, #"(?capture[_\-\.]group)\s+\n(.*)"#) - } - - func testInitWithDictionaryLiteral() { - let regex: Regex = [ - "name": #"capture[_\-\.]group"#, - "suffix": #"\s+\n.*"#, - ] - XCTAssertEqual(regex.pattern, #"(?capture[_\-\.]group)(?\s+\n.*)"#) - } + func testInitWithStringLiteral() { + let regex: Regex = #"(?capture[_\-\.]group)\s+\n(.*)"# + XCTAssertEqual(regex.pattern, #"(?capture[_\-\.]group)\s+\n(.*)"#) + } + + func testInitWithDictionaryLiteral() { + let regex: Regex = [ + "name": #"capture[_\-\.]group"#, + "suffix": #"\s+\n.*"#, + ] + XCTAssertEqual(regex.pattern, #"(?capture[_\-\.]group)(?\s+\n.*)"#) + } } diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 6358172..1e9f8ca 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -4,120 +4,120 @@ import Rainbow import XCTest final class StatisticsTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - Statistics.shared.reset() - } - - func testFoundViolationsInCheck() { - XCTAssert(Statistics.shared.executedChecks.isEmpty) - XCTAssert(Statistics.shared.violationsBySeverity[.info]!.isEmpty) - XCTAssert(Statistics.shared.violationsBySeverity[.warning]!.isEmpty) - XCTAssert(Statistics.shared.violationsBySeverity[.error]!.isEmpty) - XCTAssert(Statistics.shared.violationsPerCheck.isEmpty) - - let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) - Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo1)], - in: checkInfo1 - ) - - XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1]) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 0) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 0) - XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 1) - - let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) - Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo2), Violation(checkInfo: checkInfo2)], - in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) - ) - - XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1, checkInfo2]) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 2) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 0) - XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 2) - - let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) - Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3)], - in: CheckInfo(id: "id3", hint: "hint3", severity: .error) - ) - - XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1, checkInfo2, checkInfo3]) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 2) - XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 3) - XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 3) - } - - func testLogSummary() { // swiftlint:disable:this function_body_length - Statistics.shared.logCheckSummary(printExecutionTime: false) - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "No checks found to perform.") - - TestHelper.shared.reset() - - let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) - Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo1)], - in: checkInfo1 - ) - - let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) - Statistics.shared.found( - violations: [ - Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Harry.swift"), - Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Albus.swift"), - ], - in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) - ) - - let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) - Statistics.shared.found( - violations: [ - Violation(checkInfo: checkInfo3, filePath: "Hogwarts/Harry.swift", locationInfo: (line: 10, charInLine: 30)), - Violation(checkInfo: checkInfo3, filePath: "Hogwarts/Harry.swift", locationInfo: (line: 72, charInLine: 17)), - Violation(checkInfo: checkInfo3, filePath: "Hogwarts/Albus.swift", locationInfo: (line: 40, charInLine: 4)), - ], - in: CheckInfo(id: "id3", hint: "hint3", severity: .error) - ) - - Statistics.shared.checkedFiles(at: ["Hogwarts/Harry.swift"]) - Statistics.shared.checkedFiles(at: ["Hogwarts/Harry.swift", "Hogwarts/Albus.swift"]) - Statistics.shared.checkedFiles(at: ["Hogwarts/Albus.swift"]) - - Statistics.shared.logCheckSummary(printExecutionTime: true) - - XCTAssertEqual( - TestHelper.shared.consoleOutputs.map { $0.level }, - [.info, .info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] - ) - - let expectedOutputs = [ - "Executed checks sorted by their execution time:", - "\("[id1]".bold) Found 1 violation(s).", - ">> Hint: hint1".bold.italic, - "\("[id2]".bold) Found 2 violation(s) at:", - "> 1. Hogwarts/Harry.swift", - "> 2. Hogwarts/Albus.swift", - ">> Hint: hint2".bold.italic, - "\("[id3]".bold) Found 3 violation(s) at:", - "> 1. Hogwarts/Harry.swift:10:30:", - "> 2. Hogwarts/Harry.swift:72:17:", - "> 3. Hogwarts/Albus.swift:40:4:", - ">> Hint: hint3".bold.italic, - "Performed 3 check(s) in 2 file(s) and found 3 error(s) & 2 warning(s).", - ] - - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, expectedOutputs.count) - - for (index, expectedOutput) in expectedOutputs.enumerated() { - XCTAssertEqual(TestHelper.shared.consoleOutputs[index].message, expectedOutput) - } - } + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + Statistics.shared.reset() + } + + func testFoundViolationsInCheck() { + XCTAssert(Statistics.shared.executedChecks.isEmpty) + XCTAssert(Statistics.shared.violationsBySeverity[.info]!.isEmpty) + XCTAssert(Statistics.shared.violationsBySeverity[.warning]!.isEmpty) + XCTAssert(Statistics.shared.violationsBySeverity[.error]!.isEmpty) + XCTAssert(Statistics.shared.violationsPerCheck.isEmpty) + + let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) + Statistics.shared.found( + violations: [Violation(checkInfo: checkInfo1)], + in: checkInfo1 + ) + + XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1]) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 0) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 0) + XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 1) + + let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) + Statistics.shared.found( + violations: [Violation(checkInfo: checkInfo2), Violation(checkInfo: checkInfo2)], + in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) + ) + + XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1, checkInfo2]) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 2) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 0) + XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 2) + + let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) + Statistics.shared.found( + violations: [Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3)], + in: CheckInfo(id: "id3", hint: "hint3", severity: .error) + ) + + XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1, checkInfo2, checkInfo3]) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 2) + XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 3) + XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 3) + } + + func testLogSummary() { // swiftlint:disable:this function_body_length + Statistics.shared.logCheckSummary(printExecutionTime: false) + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "No checks found to perform.") + + TestHelper.shared.reset() + + let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) + Statistics.shared.found( + violations: [Violation(checkInfo: checkInfo1)], + in: checkInfo1 + ) + + let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) + Statistics.shared.found( + violations: [ + Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Harry.swift"), + Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Albus.swift"), + ], + in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) + ) + + let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) + Statistics.shared.found( + violations: [ + Violation(checkInfo: checkInfo3, filePath: "Hogwarts/Harry.swift", locationInfo: (line: 10, charInLine: 30)), + Violation(checkInfo: checkInfo3, filePath: "Hogwarts/Harry.swift", locationInfo: (line: 72, charInLine: 17)), + Violation(checkInfo: checkInfo3, filePath: "Hogwarts/Albus.swift", locationInfo: (line: 40, charInLine: 4)), + ], + in: CheckInfo(id: "id3", hint: "hint3", severity: .error) + ) + + Statistics.shared.checkedFiles(at: ["Hogwarts/Harry.swift"]) + Statistics.shared.checkedFiles(at: ["Hogwarts/Harry.swift", "Hogwarts/Albus.swift"]) + Statistics.shared.checkedFiles(at: ["Hogwarts/Albus.swift"]) + + Statistics.shared.logCheckSummary(printExecutionTime: true) + + XCTAssertEqual( + TestHelper.shared.consoleOutputs.map { $0.level }, + [.info, .info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] + ) + + let expectedOutputs = [ + "Executed checks sorted by their execution time:", + "\("[id1]".bold) Found 1 violation(s).", + ">> Hint: hint1".bold.italic, + "\("[id2]".bold) Found 2 violation(s) at:", + "> 1. Hogwarts/Harry.swift", + "> 2. Hogwarts/Albus.swift", + ">> Hint: hint2".bold.italic, + "\("[id3]".bold) Found 3 violation(s) at:", + "> 1. Hogwarts/Harry.swift:10:30:", + "> 2. Hogwarts/Harry.swift:72:17:", + "> 3. Hogwarts/Albus.swift:40:4:", + ">> Hint: hint3".bold.italic, + "Performed 3 check(s) in 2 file(s) and found 3 error(s) & 2 warning(s).", + ] + + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, expectedOutputs.count) + + for (index, expectedOutput) in expectedOutputs.enumerated() { + XCTAssertEqual(TestHelper.shared.consoleOutputs[index].message, expectedOutput) + } + } } diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift index c9d0ebd..dd91efa 100644 --- a/Tests/AnyLintTests/ViolationTests.swift +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -4,25 +4,25 @@ import Rainbow import XCTest final class ViolationTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - Statistics.shared.reset() - } - - func testLocationMessage() { - let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) - XCTAssertNil(Violation(checkInfo: checkInfo).locationMessage(pathType: .relative)) - - let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") - XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") - - let locationInfoViolation = Violation( - checkInfo: checkInfo, - filePath: "Temp/Souces/World.swift", - locationInfo: String.LocationInfo(line: 5, charInLine: 15) - ) - - XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") - } + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + Statistics.shared.reset() + } + + func testLocationMessage() { + let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) + XCTAssertNil(Violation(checkInfo: checkInfo).locationMessage(pathType: .relative)) + + let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") + XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") + + let locationInfoViolation = Violation( + checkInfo: checkInfo, + filePath: "Temp/Souces/World.swift", + locationInfo: String.LocationInfo(line: 5, charInLine: 15) + ) + + XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") + } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index ae7fdcc..eb118b5 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -8,85 +8,85 @@ import XCTest // swiftlint:disable line_length file_length extension ArrayExtTests { - static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ - ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) - ] + static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ + ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) + ] } extension AutoCorrectionTests { - static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), - ("testAppliedMessageLines", testAppliedMessageLines) - ] + static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ + ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), + ("testAppliedMessageLines", testAppliedMessageLines) + ] } extension CheckInfoTests { - static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral) - ] + static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ + ("testInitWithStringLiteral", testInitWithStringLiteral) + ] } extension FileContentsCheckerTests { - static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck), - ("testSkipInFile", testSkipInFile), - ("testSkipHere", testSkipHere), - ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), - ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected) - ] + static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ + ("testPerformCheck", testPerformCheck), + ("testSkipInFile", testSkipInFile), + ("testSkipHere", testSkipHere), + ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), + ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected) + ] } extension FilePathsCheckerTests { - static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck) - ] + static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ + ("testPerformCheck", testPerformCheck) + ] } extension FilesSearchTests { - static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ - ("testAllFilesWithinPath", testAllFilesWithinPath), - ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions) - ] + static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ + ("testAllFilesWithinPath", testAllFilesWithinPath), + ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions) + ] } extension LintTests { - static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ - ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), - ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), - ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), - ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups) - ] + static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ + ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), + ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), + ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), + ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups) + ] } extension RegexExtTests { - static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral), - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral) - ] + static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ + ("testInitWithStringLiteral", testInitWithStringLiteral), + ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral) + ] } extension StatisticsTests { - static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ - ("testFoundViolationsInCheck", testFoundViolationsInCheck), - ("testLogSummary", testLogSummary) - ] + static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ + ("testFoundViolationsInCheck", testFoundViolationsInCheck), + ("testLogSummary", testLogSummary) + ] } extension ViolationTests { - static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ - ("testLocationMessage", testLocationMessage) - ] + static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ + ("testLocationMessage", testLocationMessage) + ] } XCTMain([ - testCase(ArrayExtTests.allTests), - testCase(AutoCorrectionTests.allTests), - testCase(CheckInfoTests.allTests), - testCase(FileContentsCheckerTests.allTests), - testCase(FilePathsCheckerTests.allTests), - testCase(FilesSearchTests.allTests), - testCase(LintTests.allTests), - testCase(RegexExtTests.allTests), - testCase(StatisticsTests.allTests), - testCase(ViolationTests.allTests) + testCase(ArrayExtTests.allTests), + testCase(AutoCorrectionTests.allTests), + testCase(CheckInfoTests.allTests), + testCase(FileContentsCheckerTests.allTests), + testCase(FilePathsCheckerTests.allTests), + testCase(FilesSearchTests.allTests), + testCase(LintTests.allTests), + testCase(RegexExtTests.allTests), + testCase(StatisticsTests.allTests), + testCase(ViolationTests.allTests) ]) diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/UtilityTests/Extensions/RegexExtTests.swift index 5add852..78742ba 100644 --- a/Tests/UtilityTests/Extensions/RegexExtTests.swift +++ b/Tests/UtilityTests/Extensions/RegexExtTests.swift @@ -2,51 +2,51 @@ import XCTest final class RegexExtTests: XCTestCase { - func testStringLiteralInit() { - let regex: Regex = #".*"# - XCTAssertEqual(regex.description, #"/.*/"#) - } - - func testStringLiteralInitWithOptions() { - let regexI: Regex = #".*\i"# - XCTAssertEqual(regexI.description, #"/.*/i"#) - - let regexM: Regex = #".*\m"# - XCTAssertEqual(regexM.description, #"/.*/m"#) - - let regexIM: Regex = #".*\im"# - XCTAssertEqual(regexIM.description, #"/.*/im"#) - - let regexMI: Regex = #".*\mi"# - XCTAssertEqual(regexMI.description, #"/.*/im"#) - } - - func testDictionaryLiteralInit() { - let regex: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#] - XCTAssertEqual(regex.description, #"/(?[a-z]+)(?\d+\.?\d*)/"#) - } - - func testDictionaryLiteralInitWithOptions() { - let regexI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "i"] - XCTAssertEqual(regexI.description, #"/(?[a-z]+)(?\d+\.?\d*)/i"#) - - let regexM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "m"] - XCTAssertEqual(regexM.description, #"/(?[a-z]+)(?\d+\.?\d*)/m"#) - - let regexMI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "mi"] - XCTAssertEqual(regexMI.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) - - let regexIM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "im"] - XCTAssertEqual(regexIM.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) - } - - func testReplacingMatchesInInputWithTemplate() { - let regexTrailing: Regex = #"(?<=\n)([-–] .*[^ ])( {0,1}| {3,})\n"# - let text: String = "\n- Sample Text.\n" - - XCTAssertEqual( - regexTrailing.replacingMatches(in: text, with: "$1 \n"), - "\n- Sample Text. \n" - ) - } + func testStringLiteralInit() { + let regex: Regex = #".*"# + XCTAssertEqual(regex.description, #"/.*/"#) + } + + func testStringLiteralInitWithOptions() { + let regexI: Regex = #".*\i"# + XCTAssertEqual(regexI.description, #"/.*/i"#) + + let regexM: Regex = #".*\m"# + XCTAssertEqual(regexM.description, #"/.*/m"#) + + let regexIM: Regex = #".*\im"# + XCTAssertEqual(regexIM.description, #"/.*/im"#) + + let regexMI: Regex = #".*\mi"# + XCTAssertEqual(regexMI.description, #"/.*/im"#) + } + + func testDictionaryLiteralInit() { + let regex: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#] + XCTAssertEqual(regex.description, #"/(?[a-z]+)(?\d+\.?\d*)/"#) + } + + func testDictionaryLiteralInitWithOptions() { + let regexI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "i"] + XCTAssertEqual(regexI.description, #"/(?[a-z]+)(?\d+\.?\d*)/i"#) + + let regexM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "m"] + XCTAssertEqual(regexM.description, #"/(?[a-z]+)(?\d+\.?\d*)/m"#) + + let regexMI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "mi"] + XCTAssertEqual(regexMI.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) + + let regexIM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "im"] + XCTAssertEqual(regexIM.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) + } + + func testReplacingMatchesInInputWithTemplate() { + let regexTrailing: Regex = #"(?<=\n)([-–] .*[^ ])( {0,1}| {3,})\n"# + let text: String = "\n- Sample Text.\n" + + XCTAssertEqual( + regexTrailing.replacingMatches(in: text, with: "$1 \n"), + "\n- Sample Text. \n" + ) + } } diff --git a/Tests/UtilityTests/LoggerTests.swift b/Tests/UtilityTests/LoggerTests.swift index 3496482..18954bd 100644 --- a/Tests/UtilityTests/LoggerTests.swift +++ b/Tests/UtilityTests/LoggerTests.swift @@ -2,30 +2,30 @@ import XCTest final class LoggerTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } - func testMessage() { - XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) + func testMessage() { + XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) - log.message("Test", level: .info) + log.message("Test", level: .info) - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .info) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "Test") + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .info) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "Test") - log.message("Test 2", level: .warning) + log.message("Test 2", level: .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Test 2") + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .warning) + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Test 2") - log.message("Test 3", level: .error) + log.message("Test 3", level: .error) - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 3) - XCTAssertEqual(TestHelper.shared.consoleOutputs[2].level, .error) - XCTAssertEqual(TestHelper.shared.consoleOutputs[2].message, "Test 3") - } + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 3) + XCTAssertEqual(TestHelper.shared.consoleOutputs[2].level, .error) + XCTAssertEqual(TestHelper.shared.consoleOutputs[2].message, "Test 3") + } } diff --git a/lint.swift b/lint.swift index df3ebcd..599c1b5 100755 --- a/lint.swift +++ b/lint.swift @@ -4,114 +4,114 @@ import Utility import ShellOut // @JohnSundell try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { - // MARK: - Variables - let swiftSourceFiles: Regex = #"Sources/.*\.swift"# - let swiftTestFiles: Regex = #"Tests/.*\.swift"# - let readmeFile: Regex = #"README\.md"# - let changelogFile: Regex = #"^CHANGELOG\.md$"# - let projectName: String = "AnyLint" - - // MARK: - Checks - // MARK: Changelog - try Lint.checkFilePaths( - checkInfo: "Changelog: Each project should have a CHANGELOG.md file, tracking the changes within a project over time.", - regex: changelogFile, - matchingExamples: ["CHANGELOG.md"], - nonMatchingExamples: ["CHANGELOG.markdown", "Changelog.md", "ChangeLog.md"], - violateIfNoMatchesFound: true - ) - - // MARK: ChangelogEntryTrailingWhitespaces - try Lint.checkFileContents( - checkInfo: "ChangelogEntryTrailingWhitespaces: The summary line of a Changelog entry should end with two whitespaces.", - regex: #"\n([-–] (?!None\.).*[^ ])( {0,1}| {3,})\n"#, - matchingExamples: ["\n- Fixed a bug.\n Issue:", "\n- Added a new option. (see [Link](#)) \nPR:"], - nonMatchingExamples: ["\n- Fixed a bug. \n Issue:", "\n- Added a new option. (see [Link](#)) \nPR:"], - includeFilters: [changelogFile], - autoCorrectReplacement: "\n$1 \n", - autoCorrectExamples: [ - ["before": "\n- Fixed a bug.\n Issue:", "after": "\n- Fixed a bug. \n Issue:"], - ["before": "\n- Fixed a bug. \n Issue:", "after": "\n- Fixed a bug. \n Issue:"], - ["before": "\n- Fixed a bug. \n Issue:", "after": "\n- Fixed a bug. \n Issue:"], - ["before": "\n- Fixed a bug !\n Issue:", "after": "\n- Fixed a bug ! \n Issue:"], - ["before": "\n- Fixed a bug ! \n Issue:", "after": "\n- Fixed a bug ! \n Issue:"], - ["before": "\n- Fixed a bug ! \n Issue:", "after": "\n- Fixed a bug ! \n Issue:"], - ] - ) - - // MARK: ChangelogEntryLeadingWhitespaces - try Lint.checkFileContents( - checkInfo: "ChangelogEntryLeadingWhitespaces: The links line of a Changelog entry should start with two whitespaces.", - regex: #"\n( {0,1}| {3,})(Tasks?:|Issues?:|PRs?:|Authors?:)"#, - matchingExamples: ["\n- Fixed a bug.\nIssue: [Link](#)", "\n- Fixed a bug. \nIssue: [Link](#)", "\n- Fixed a bug. \nIssue: [Link](#)"], - nonMatchingExamples: ["- Fixed a bug.\n Issue: [Link](#)"], - includeFilters: [changelogFile], - autoCorrectReplacement: "\n $2", - autoCorrectExamples: [ - ["before": "\n- Fixed a bug.\nIssue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"], - ["before": "\n- Fixed a bug.\n Issue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"], - ["before": "\n- Fixed a bug.\n Issue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"], - ] - ) - - // MARK: EmptyMethodBody - try Lint.checkFileContents( - checkInfo: "EmptyMethodBody: Don't use whitespace or newlines for the body of empty methods.", - regex: ["declaration": #"(init|func [^\(\s]+)\([^{}]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], - matchingExamples: [ - "init() { }", - "init() {\n\n}", - "init(\n x: Int,\n y: Int\n) { }", - "func foo2bar() { }", - "func foo2bar(x: Int, y: Int) { }", - "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", - ], - nonMatchingExamples: ["init() { /* comment */ }", "init() {}", "func foo2bar() {}", "func foo2bar(x: Int, y: Int) {}"], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: "$declaration {}", - autoCorrectExamples: [ - ["before": "init() { }", "after": "init() {}"], - ["before": "init(x: Int, y: Int) { }", "after": "init(x: Int, y: Int) {}"], - ["before": "init()\n{\n \n}", "after": "init() {}"], - ["before": "init(\n x: Int,\n y: Int\n) {\n \n}", "after": "init(\n x: Int,\n y: Int\n) {}"], - ["before": "func foo2bar() { }", "after": "func foo2bar() {}"], - ["before": "func foo2bar(x: Int, y: Int) { }", "after": "func foo2bar(x: Int, y: Int) {}"], - ["before": "func foo2bar()\n{\n \n}", "after": "func foo2bar() {}"], - ["before": "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", "after": "func foo2bar(\n x: Int,\n y: Int\n) {}"], - ] - ) - - // MARK: EmptyTodo - try Lint.checkFileContents( - checkInfo: "EmptyTodo: `// TODO:` comments should not be empty.", - regex: #"// TODO: ?(\[[\d\-_a-z]+\])? *\n"#, - matchingExamples: ["// TODO:\n", "// TODO: [2020-03-19]\n", "// TODO: [cg_2020-03-19] \n"], - nonMatchingExamples: ["// TODO: refactor", "// TODO: not yet implemented", "// TODO: [cg_2020-03-19] not yet implemented"], - includeFilters: [swiftSourceFiles, swiftTestFiles] - ) - - // MARK: EmptyType - try Lint.checkFileContents( - checkInfo: "EmptyType: Don't keep empty types in code without commenting inside why they are needed.", - regex: #"(class|protocol|struct|enum) [^\{]+\{\s*\}"#, - matchingExamples: ["class Foo {}", "enum Constants {\n \n}", "struct MyViewModel(x: Int, y: Int, closure: () -> Void) {}"], - nonMatchingExamples: ["class Foo { /* TODO: not yet implemented */ }", "func foo() {}", "init() {}", "enum Bar { case x, y }"], - includeFilters: [swiftSourceFiles, swiftTestFiles] - ) - - // MARK: GuardMultiline2 - try Lint.checkFileContents( - checkInfo: "GuardMultiline2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", - regex: [ - "newline": #"\n"#, - "guardIndent": #" *"#, - "guard": #"guard *"#, - "line1": #"[^\n]+,"#, - "line1Indent": #"\n *"#, - "line2": #"[^\n]*\S"#, - "else": #"\s*else\s*\{\s*"# - ], - matchingExamples: [ + // MARK: - Variables + let swiftSourceFiles: Regex = #"Sources/.*\.swift"# + let swiftTestFiles: Regex = #"Tests/.*\.swift"# + let readmeFile: Regex = #"README\.md"# + let changelogFile: Regex = #"^CHANGELOG\.md$"# + let projectName: String = "AnyLint" + + // MARK: - Checks + // MARK: Changelog + try Lint.checkFilePaths( + checkInfo: "Changelog: Each project should have a CHANGELOG.md file, tracking the changes within a project over time.", + regex: changelogFile, + matchingExamples: ["CHANGELOG.md"], + nonMatchingExamples: ["CHANGELOG.markdown", "Changelog.md", "ChangeLog.md"], + violateIfNoMatchesFound: true + ) + + // MARK: ChangelogEntryTrailingWhitespaces + try Lint.checkFileContents( + checkInfo: "ChangelogEntryTrailingWhitespaces: The summary line of a Changelog entry should end with two whitespaces.", + regex: #"\n([-–] (?!None\.).*[^ ])( {0,1}| {3,})\n"#, + matchingExamples: ["\n- Fixed a bug.\n Issue:", "\n- Added a new option. (see [Link](#)) \nPR:"], + nonMatchingExamples: ["\n- Fixed a bug. \n Issue:", "\n- Added a new option. (see [Link](#)) \nPR:"], + includeFilters: [changelogFile], + autoCorrectReplacement: "\n$1 \n", + autoCorrectExamples: [ + ["before": "\n- Fixed a bug.\n Issue:", "after": "\n- Fixed a bug. \n Issue:"], + ["before": "\n- Fixed a bug. \n Issue:", "after": "\n- Fixed a bug. \n Issue:"], + ["before": "\n- Fixed a bug. \n Issue:", "after": "\n- Fixed a bug. \n Issue:"], + ["before": "\n- Fixed a bug !\n Issue:", "after": "\n- Fixed a bug ! \n Issue:"], + ["before": "\n- Fixed a bug ! \n Issue:", "after": "\n- Fixed a bug ! \n Issue:"], + ["before": "\n- Fixed a bug ! \n Issue:", "after": "\n- Fixed a bug ! \n Issue:"], + ] + ) + + // MARK: ChangelogEntryLeadingWhitespaces + try Lint.checkFileContents( + checkInfo: "ChangelogEntryLeadingWhitespaces: The links line of a Changelog entry should start with two whitespaces.", + regex: #"\n( {0,1}| {3,})(Tasks?:|Issues?:|PRs?:|Authors?:)"#, + matchingExamples: ["\n- Fixed a bug.\nIssue: [Link](#)", "\n- Fixed a bug. \nIssue: [Link](#)", "\n- Fixed a bug. \nIssue: [Link](#)"], + nonMatchingExamples: ["- Fixed a bug.\n Issue: [Link](#)"], + includeFilters: [changelogFile], + autoCorrectReplacement: "\n $2", + autoCorrectExamples: [ + ["before": "\n- Fixed a bug.\nIssue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"], + ["before": "\n- Fixed a bug.\n Issue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"], + ["before": "\n- Fixed a bug.\n Issue: [Link](#)", "after": "\n- Fixed a bug.\n Issue: [Link](#)"], + ] + ) + + // MARK: EmptyMethodBody + try Lint.checkFileContents( + checkInfo: "EmptyMethodBody: Don't use whitespace or newlines for the body of empty methods.", + regex: ["declaration": #"(init|func [^\(\s]+)\([^{}]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], + matchingExamples: [ + "init() { }", + "init() {\n\n}", + "init(\n x: Int,\n y: Int\n) { }", + "func foo2bar() { }", + "func foo2bar(x: Int, y: Int) { }", + "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", + ], + nonMatchingExamples: ["init() { /* comment */ }", "init() {}", "func foo2bar() {}", "func foo2bar(x: Int, y: Int) {}"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$declaration {}", + autoCorrectExamples: [ + ["before": "init() { }", "after": "init() {}"], + ["before": "init(x: Int, y: Int) { }", "after": "init(x: Int, y: Int) {}"], + ["before": "init()\n{\n \n}", "after": "init() {}"], + ["before": "init(\n x: Int,\n y: Int\n) {\n \n}", "after": "init(\n x: Int,\n y: Int\n) {}"], + ["before": "func foo2bar() { }", "after": "func foo2bar() {}"], + ["before": "func foo2bar(x: Int, y: Int) { }", "after": "func foo2bar(x: Int, y: Int) {}"], + ["before": "func foo2bar()\n{\n \n}", "after": "func foo2bar() {}"], + ["before": "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", "after": "func foo2bar(\n x: Int,\n y: Int\n) {}"], + ] + ) + + // MARK: EmptyTodo + try Lint.checkFileContents( + checkInfo: "EmptyTodo: `// TODO:` comments should not be empty.", + regex: #"// TODO: ?(\[[\d\-_a-z]+\])? *\n"#, + matchingExamples: ["// TODO:\n", "// TODO: [2020-03-19]\n", "// TODO: [cg_2020-03-19] \n"], + nonMatchingExamples: ["// TODO: refactor", "// TODO: not yet implemented", "// TODO: [cg_2020-03-19] not yet implemented"], + includeFilters: [swiftSourceFiles, swiftTestFiles] + ) + + // MARK: EmptyType + try Lint.checkFileContents( + checkInfo: "EmptyType: Don't keep empty types in code without commenting inside why they are needed.", + regex: #"(class|protocol|struct|enum) [^\{]+\{\s*\}"#, + matchingExamples: ["class Foo {}", "enum Constants {\n \n}", "struct MyViewModel(x: Int, y: Int, closure: () -> Void) {}"], + nonMatchingExamples: ["class Foo { /* TODO: not yet implemented */ }", "func foo() {}", "init() {}", "enum Bar { case x, y }"], + includeFilters: [swiftSourceFiles, swiftTestFiles] + ) + + // MARK: GuardMultiline2 + try Lint.checkFileContents( + checkInfo: "GuardMultiline2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: [ + "newline": #"\n"#, + "guardIndent": #" *"#, + "guard": #"guard *"#, + "line1": #"[^\n]+,"#, + "line1Indent": #"\n *"#, + "line2": #"[^\n]*\S"#, + "else": #"\s*else\s*\{\s*"# + ], + matchingExamples: [ """ guard let x1 = y1?.imagePath, @@ -119,8 +119,8 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - nonMatchingExamples: [ + ], + nonMatchingExamples: [ """ guard @@ -136,9 +136,9 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """, - ], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: """ + ], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: """ $guardIndentguard $guardIndent $line1 @@ -146,16 +146,16 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { $guardIndentelse { $guardIndent\u{0020}\u{0020}\u{0020}\u{0020} """, - autoCorrectExamples: [ - [ - "before": """ + autoCorrectExamples: [ + [ + "before": """ let x = 15 guard let x1 = y1?.imagePath, let z = EnumType(rawValue: 15) else { return 2 } """, - "after": """ + "after": """ let x = 15 guard let x1 = y1?.imagePath, @@ -164,25 +164,25 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - ] - ) - - // MARK: GuardMultiline3 - try Lint.checkFileContents( - checkInfo: "GuardMultiline3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", - regex: [ - "newline": #"\n"#, - "guardIndent": #" *"#, - "guard": #"guard *"#, - "line1": #"[^\n]+,"#, - "line1Indent": #"\n *"#, - "line2": #"[^\n]+,"#, - "line2Indent": #"\n *"#, - "line3": #"[^\n]*\S"#, - "else": #"\s*else\s*\{\s*"# - ], - matchingExamples: [ + ], + ] + ) + + // MARK: GuardMultiline3 + try Lint.checkFileContents( + checkInfo: "GuardMultiline3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: [ + "newline": #"\n"#, + "guardIndent": #" *"#, + "guard": #"guard *"#, + "line1": #"[^\n]+,"#, + "line1Indent": #"\n *"#, + "line2": #"[^\n]+,"#, + "line2Indent": #"\n *"#, + "line3": #"[^\n]*\S"#, + "else": #"\s*else\s*\{\s*"# + ], + matchingExamples: [ """ guard let x1 = y1?.imagePath, @@ -191,8 +191,8 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - nonMatchingExamples: [ + ], + nonMatchingExamples: [ """ guard @@ -209,9 +209,9 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """, - ], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: """ + ], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: """ $guardIndentguard $guardIndent $line1 @@ -220,9 +220,9 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { $guardIndentelse { $guardIndent\u{0020}\u{0020}\u{0020}\u{0020} """, - autoCorrectExamples: [ - [ - "before": """ + autoCorrectExamples: [ + [ + "before": """ let x = 15 guard let x1 = y1?.imagePath, let x2 = y2?.imagePath, @@ -230,7 +230,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """, - "after": """ + "after": """ let x = 15 guard let x1 = y1?.imagePath, @@ -240,27 +240,27 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - ] - ) - - // MARK: GuardMultiline4 - try Lint.checkFileContents( - checkInfo: "GuardMultiline4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", - regex: [ - "newline": #"\n"#, - "guardIndent": #" *"#, - "guard": #"guard *"#, - "line1": #"[^\n]+,"#, - "line1Indent": #"\n *"#, - "line2": #"[^\n]+,"#, - "line2Indent": #"\n *"#, - "line3": #"[^\n]+,"#, - "line3Indent": #"\n *"#, - "line4": #"[^\n]*\S"#, - "else": #"\s*else\s*\{\s*"# - ], - matchingExamples: [ + ], + ] + ) + + // MARK: GuardMultiline4 + try Lint.checkFileContents( + checkInfo: "GuardMultiline4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: [ + "newline": #"\n"#, + "guardIndent": #" *"#, + "guard": #"guard *"#, + "line1": #"[^\n]+,"#, + "line1Indent": #"\n *"#, + "line2": #"[^\n]+,"#, + "line2Indent": #"\n *"#, + "line3": #"[^\n]+,"#, + "line3Indent": #"\n *"#, + "line4": #"[^\n]*\S"#, + "else": #"\s*else\s*\{\s*"# + ], + matchingExamples: [ """ guard let x1 = y1?.imagePath, @@ -270,8 +270,8 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - nonMatchingExamples: [ + ], + nonMatchingExamples: [ """ guard @@ -289,9 +289,9 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """, - ], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: """ + ], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: """ $guardIndentguard $guardIndent $line1 @@ -301,9 +301,9 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { $guardIndentelse { $guardIndent\u{0020}\u{0020}\u{0020}\u{0020} """, - autoCorrectExamples: [ - [ - "before": """ + autoCorrectExamples: [ + [ + "before": """ let x = 15 guard let x1 = y1?.imagePath, let x2 = y2?.imagePath, @@ -312,7 +312,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """, - "after": """ + "after": """ let x = 15 guard let x1 = y1?.imagePath, @@ -323,15 +323,15 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - ] - ) - - // MARK: GuardMultilineN - try Lint.checkFileContents( - checkInfo: "GuardMultilineN: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", - regex: #"\n *guard *([^\n]+,\n){4,}[^\n]*\S\s*else\s*\{\s*"#, - matchingExamples: [ + ], + ] + ) + + // MARK: GuardMultilineN + try Lint.checkFileContents( + checkInfo: "GuardMultilineN: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: #"\n *guard *([^\n]+,\n){4,}[^\n]*\S\s*else\s*\{\s*"#, + matchingExamples: [ """ guard let x1 = y1?.imagePath, @@ -343,8 +343,8 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """ - ], - nonMatchingExamples: [ + ], + nonMatchingExamples: [ """ guard @@ -364,155 +364,155 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { return 2 } """, - ], - includeFilters: [swiftSourceFiles, swiftTestFiles] - ) - - // MARK: IfAsGuard - try Lint.checkFileContents( - checkInfo: "IfAsGuard: Don't use an if statement to just return – use guard for such cases instead.", - regex: #" +if [^\{]+\{\s*return\s*[^\}]*\}(?! *else)"#, - matchingExamples: [" if x == 5 { return }", " if x == 5 {\n return nil\n}", " if x == 5 { return 500 }", " if x == 5 { return do(x: 500, y: 200) }"], - nonMatchingExamples: [" if x == 5 {\n let y = 200\n return y\n}", " if x == 5 { someMethod(x: 500, y: 200) }", " if x == 500 { return } else {"], - includeFilters: [swiftSourceFiles, swiftTestFiles] - ) - - // MARK: LateForceUnwrapping3 - try Lint.checkFileContents( - checkInfo: "LateForceUnwrapping3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", - regex: [ - "openingBrace": #"\("#, - "callPart1": #"[^\s\?\.]+"#, - "separator1": #"\?\."#, - "callPart2": #"[^\s\?\.]+"#, - "separator2": #"\?\."#, - "callPart3": #"[^\s\?\.]+"#, - "separator3": #"\?\."#, - "callPart4": #"[^\s\?\.]+"#, - "closingBraceUnwrap": #"\)!"#, - ], - matchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n"], - nonMatchingExamples: ["call(x: (viewModel?.username)!)", "let x = viewModel!.user!.profile!.imagePath\n"], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3!.$callPart4", - autoCorrectExamples: [ - ["before": "let x = (viewModel?.user?.profile?.imagePath)!\n", "after": "let x = viewModel!.user!.profile!.imagePath\n"], - ] - ) - - // MARK: LateForceUnwrapping2 - try Lint.checkFileContents( - checkInfo: "LateForceUnwrapping2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", - regex: [ - "openingBrace": #"\("#, - "callPart1": #"[^\s\?\.]+"#, - "separator1": #"\?\."#, - "callPart2": #"[^\s\?\.]+"#, - "separator2": #"\?\."#, - "callPart3": #"[^\s\?\.]+"#, - "closingBraceUnwrap": #"\)!"#, - ], - matchingExamples: ["call(x: (viewModel?.profile?.username)!)"], - nonMatchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n", "let x = viewModel!.profile!.imagePath\n"], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3", - autoCorrectExamples: [ - ["before": "let x = (viewModel?.profile?.imagePath)!\n", "after": "let x = viewModel!.profile!.imagePath\n"], - ] - ) - - // MARK: LateForceUnwrapping1 - try Lint.checkFileContents( - checkInfo: "LateForceUnwrapping1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", - regex: [ - "openingBrace": #"\("#, - "callPart1": #"[^\s\?\.]+"#, - "separator1": #"\?\."#, - "callPart2": #"[^\s\?\.]+"#, - "closingBraceUnwrap": #"\)!"#, - ], - matchingExamples: ["call(x: (viewModel?.username)!)"], - nonMatchingExamples: ["call(x: (viewModel?.profile?.username)!)", "call(x: viewModel!.username)"], - includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: "$callPart1!.$callPart2", - autoCorrectExamples: [ - ["before": "call(x: (viewModel?.username)!)", "after": "call(x: viewModel!.username)"], - ] - ) - - // MARK: LinuxMainUpToDate - try Lint.customCheck(checkInfo: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { checkInfo in - var violations: [Violation] = [] - - let linuxMainFilePath = "Tests/LinuxMain.swift" - let linuxMainContentsBeforeRegeneration = try! String(contentsOfFile: linuxMainFilePath) - - let sourceryDirPath = ".sourcery" - let testsDirPath = "Tests/\(projectName)Tests" - let stencilFilePath = "\(sourceryDirPath)/LinuxMain.stencil" - let generatedLinuxMainFilePath = "\(sourceryDirPath)/LinuxMain.generated.swift" - - let sourceryInstallPath = try? shellOut(to: "which", arguments: ["sourcery"]) - guard sourceryInstallPath != nil else { - log.message( - "Skipped custom check \(checkInfo) – requires Sourcery to be installed, download from: https://github.com/krzysztofzablocki/Sourcery", - level: .warning + ], + includeFilters: [swiftSourceFiles, swiftTestFiles] + ) + + // MARK: IfAsGuard + try Lint.checkFileContents( + checkInfo: "IfAsGuard: Don't use an if statement to just return – use guard for such cases instead.", + regex: #" +if [^\{]+\{\s*return\s*[^\}]*\}(?! *else)"#, + matchingExamples: [" if x == 5 { return }", " if x == 5 {\n return nil\n}", " if x == 5 { return 500 }", " if x == 5 { return do(x: 500, y: 200) }"], + nonMatchingExamples: [" if x == 5 {\n let y = 200\n return y\n}", " if x == 5 { someMethod(x: 500, y: 200) }", " if x == 500 { return } else {"], + includeFilters: [swiftSourceFiles, swiftTestFiles] + ) + + // MARK: LateForceUnwrapping3 + try Lint.checkFileContents( + checkInfo: "LateForceUnwrapping3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + regex: [ + "openingBrace": #"\("#, + "callPart1": #"[^\s\?\.]+"#, + "separator1": #"\?\."#, + "callPart2": #"[^\s\?\.]+"#, + "separator2": #"\?\."#, + "callPart3": #"[^\s\?\.]+"#, + "separator3": #"\?\."#, + "callPart4": #"[^\s\?\.]+"#, + "closingBraceUnwrap": #"\)!"#, + ], + matchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n"], + nonMatchingExamples: ["call(x: (viewModel?.username)!)", "let x = viewModel!.user!.profile!.imagePath\n"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3!.$callPart4", + autoCorrectExamples: [ + ["before": "let x = (viewModel?.user?.profile?.imagePath)!\n", "after": "let x = viewModel!.user!.profile!.imagePath\n"], + ] + ) + + // MARK: LateForceUnwrapping2 + try Lint.checkFileContents( + checkInfo: "LateForceUnwrapping2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + regex: [ + "openingBrace": #"\("#, + "callPart1": #"[^\s\?\.]+"#, + "separator1": #"\?\."#, + "callPart2": #"[^\s\?\.]+"#, + "separator2": #"\?\."#, + "callPart3": #"[^\s\?\.]+"#, + "closingBraceUnwrap": #"\)!"#, + ], + matchingExamples: ["call(x: (viewModel?.profile?.username)!)"], + nonMatchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n", "let x = viewModel!.profile!.imagePath\n"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3", + autoCorrectExamples: [ + ["before": "let x = (viewModel?.profile?.imagePath)!\n", "after": "let x = viewModel!.profile!.imagePath\n"], + ] + ) + + // MARK: LateForceUnwrapping1 + try Lint.checkFileContents( + checkInfo: "LateForceUnwrapping1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + regex: [ + "openingBrace": #"\("#, + "callPart1": #"[^\s\?\.]+"#, + "separator1": #"\?\."#, + "callPart2": #"[^\s\?\.]+"#, + "closingBraceUnwrap": #"\)!"#, + ], + matchingExamples: ["call(x: (viewModel?.username)!)"], + nonMatchingExamples: ["call(x: (viewModel?.profile?.username)!)", "call(x: viewModel!.username)"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$callPart1!.$callPart2", + autoCorrectExamples: [ + ["before": "call(x: (viewModel?.username)!)", "after": "call(x: viewModel!.username)"], + ] + ) + + // MARK: LinuxMainUpToDate + try Lint.customCheck(checkInfo: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { checkInfo in + var violations: [Violation] = [] + + let linuxMainFilePath = "Tests/LinuxMain.swift" + let linuxMainContentsBeforeRegeneration = try! String(contentsOfFile: linuxMainFilePath) + + let sourceryDirPath = ".sourcery" + let testsDirPath = "Tests/\(projectName)Tests" + let stencilFilePath = "\(sourceryDirPath)/LinuxMain.stencil" + let generatedLinuxMainFilePath = "\(sourceryDirPath)/LinuxMain.generated.swift" + + let sourceryInstallPath = try? shellOut(to: "which", arguments: ["sourcery"]) + guard sourceryInstallPath != nil else { + log.message( + "Skipped custom check \(checkInfo) – requires Sourcery to be installed, download from: https://github.com/krzysztofzablocki/Sourcery", + level: .warning + ) + return [] + } + + try! shellOut(to: "sourcery", arguments: ["--sources", testsDirPath, "--templates", stencilFilePath, "--output", sourceryDirPath]) + let linuxMainContentsAfterRegeneration = try! String(contentsOfFile: generatedLinuxMainFilePath) + + // move generated file to LinuxMain path to update its contents + try! shellOut(to: "mv", arguments: [generatedLinuxMainFilePath, linuxMainFilePath]) + + if linuxMainContentsBeforeRegeneration != linuxMainContentsAfterRegeneration { + violations.append( + Violation( + checkInfo: checkInfo, + filePath: linuxMainFilePath, + appliedAutoCorrection: AutoCorrection( + before: linuxMainContentsBeforeRegeneration, + after: linuxMainContentsAfterRegeneration + ) ) - return [] - } - - try! shellOut(to: "sourcery", arguments: ["--sources", testsDirPath, "--templates", stencilFilePath, "--output", sourceryDirPath]) - let linuxMainContentsAfterRegeneration = try! String(contentsOfFile: generatedLinuxMainFilePath) - - // move generated file to LinuxMain path to update its contents - try! shellOut(to: "mv", arguments: [generatedLinuxMainFilePath, linuxMainFilePath]) - - if linuxMainContentsBeforeRegeneration != linuxMainContentsAfterRegeneration { - violations.append( - Violation( - checkInfo: checkInfo, - filePath: linuxMainFilePath, - appliedAutoCorrection: AutoCorrection( - before: linuxMainContentsBeforeRegeneration, - after: linuxMainContentsAfterRegeneration - ) - ) - ) - } - - return violations - } - - // MARK: Logger - try Lint.checkFileContents( - checkInfo: "Logger: Don't use `print` – use `log.message` instead.", - regex: #"print\([^\n]+\)"#, - matchingExamples: [#"print("Hellow World!")"#, #"print(5)"#, #"print(\n "hi"\n)"#], - nonMatchingExamples: [#"log.message("Hello world!")"#], - includeFilters: [swiftSourceFiles, swiftTestFiles], - excludeFilters: [#"Sources/.*/Logger\.swift"#] - ) - - // MARK: Readme - try Lint.checkFilePaths( - checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", - regex: #"^README\.md$"#, - matchingExamples: ["README.md"], - nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], - violateIfNoMatchesFound: true - ) - - // MARK: ReadmePath - try Lint.checkFilePaths( - checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", - regex: #"^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$"#, - matchingExamples: ["README.markdown", "readme.md", "ReadMe.md"], - nonMatchingExamples: ["README.md", "CHANGELOG.md", "CONTRIBUTING.md", "api/help.md"], - autoCorrectReplacement: "$1README.md", - autoCorrectExamples: [ - ["before": "api/readme.md", "after": "api/README.md"], - ["before": "ReadMe.md", "after": "README.md"], - ["before": "README.markdown", "after": "README.md"], - ] - ) + ) + } + + return violations + } + + // MARK: Logger + try Lint.checkFileContents( + checkInfo: "Logger: Don't use `print` – use `log.message` instead.", + regex: #"print\([^\n]+\)"#, + matchingExamples: [#"print("Hellow World!")"#, #"print(5)"#, #"print(\n "hi"\n)"#], + nonMatchingExamples: [#"log.message("Hello world!")"#], + includeFilters: [swiftSourceFiles, swiftTestFiles], + excludeFilters: [#"Sources/.*/Logger\.swift"#] + ) + + // MARK: Readme + try Lint.checkFilePaths( + checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", + regex: #"^README\.md$"#, + matchingExamples: ["README.md"], + nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], + violateIfNoMatchesFound: true + ) + + // MARK: ReadmePath + try Lint.checkFilePaths( + checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", + regex: #"^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$"#, + matchingExamples: ["README.markdown", "readme.md", "ReadMe.md"], + nonMatchingExamples: ["README.md", "CHANGELOG.md", "CONTRIBUTING.md", "api/help.md"], + autoCorrectReplacement: "$1README.md", + autoCorrectExamples: [ + ["before": "api/readme.md", "after": "api/README.md"], + ["before": "ReadMe.md", "after": "README.md"], + ["before": "README.markdown", "after": "README.md"], + ] + ) } From 63473a1fe8e1863a5fdc53513d9c0e69f5647172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 01:17:22 +0200 Subject: [PATCH 07/17] Get all tests passing again by adjusting test expectation --- Tests/AnyLintTests/StatisticsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 1e9f8ca..7a894d5 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -99,7 +99,7 @@ final class StatisticsTests: XCTestCase { ) let expectedOutputs = [ - "Executed checks sorted by their execution time:", + "⏱ Executed checks sorted by their execution time:", "\("[id1]".bold) Found 1 violation(s).", ">> Hint: hint1".bold.italic, "\("[id2]".bold) Found 2 violation(s) at:", From d0325df987d7a9ee35cede6a8186e0a7f2ff3a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 01:20:09 +0200 Subject: [PATCH 08/17] Adjust order & style in Package manifest file --- Package.swift | 72 ++++++++++++------------ Tests/AnyLintTests/StatisticsTests.swift | 1 + 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Package.swift b/Package.swift index 70b284a..39e2155 100644 --- a/Package.swift +++ b/Package.swift @@ -2,40 +2,40 @@ import PackageDescription let package = Package( - name: "AnyLint", - platforms: [.macOS(.v10_12)], - products: [ - .library(name: "AnyLint", targets: ["AnyLint", "Utility"]), - .executable(name: "anylint", targets: ["AnyLintCLI"]), - ], - dependencies: [ - .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), - .package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.1"), - ], - targets: [ - .executableTarget( - name: "AnyLintCLI", - dependencies: ["Rainbow", "SwiftCLI", "Utility"] - ), - .target( - name: "AnyLint", - dependencies: ["Utility"] - ), - .target( - name: "Utility", - dependencies: ["Rainbow"] - ), - .testTarget( - name: "AnyLintTests", - dependencies: ["AnyLint"] - ), - .testTarget( - name: "AnyLintCLITests", - dependencies: ["AnyLintCLI"] - ), - .testTarget( - name: "UtilityTests", - dependencies: ["Utility"] - ) - ] + name: "AnyLint", + platforms: [.macOS(.v10_12)], + products: [ + .library(name: "AnyLint", targets: ["AnyLint"]), + .executable(name: "anylint", targets: ["AnyLintCLI"]), + ], + dependencies: [ + .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), + .package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.1"), + ], + targets: [ + .target( + name: "AnyLint", + dependencies: ["Utility"] + ), + .testTarget( + name: "AnyLintTests", + dependencies: ["AnyLint"] + ), + .executableTarget( + name: "AnyLintCLI", + dependencies: ["Rainbow", "SwiftCLI", "Utility"] + ), + .testTarget( + name: "AnyLintCLITests", + dependencies: ["AnyLintCLI"] + ), + .testTarget( + name: "UtilityTests", + dependencies: ["Utility"] + ), + .target( + name: "Utility", + dependencies: ["Rainbow"] + ), + ] ) diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 7a894d5..a0872c8 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -103,6 +103,7 @@ final class StatisticsTests: XCTestCase { "\("[id1]".bold) Found 1 violation(s).", ">> Hint: hint1".bold.italic, "\("[id2]".bold) Found 2 violation(s) at:", + "> 1. Hogwarts/Harry.swift", "> 2. Hogwarts/Albus.swift", ">> Hint: hint2".bold.italic, From 059ae0070fcb1d2e7a3c2117f08c339cafa46790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 05:32:24 +0200 Subject: [PATCH 09/17] Adjust CI build specs to run on custom machine --- .github/workflows/main.yml | 104 ++--------------- CHANGELOG.md | 4 +- .../AnyLintCLI/Commands/SingleCommand.swift | 4 +- Tests/LinuxMain.swift | 105 +++++++++--------- 4 files changed, 68 insertions(+), 149 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29c1d4e..e51c569 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,109 +2,29 @@ name: CI on: push: - branches: [main, versions] + branches: [main] + pull_request: branches: [main] -jobs: - cancel-previous-runs: - runs-on: ubuntu-latest +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true - steps: - - name: Cancel previous runs of this workflow on same branch - uses: rokroskar/workflow-run-cleanup-action@v0.2.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - anylint: - runs-on: ubuntu-latest +jobs: + ci: + runs-on: [self-hosted] steps: - - uses: actions/checkout@v2 - - - name: Export latest tool versions - run: | - latest_version() { - curl --silent "https://api.github.com/repos/$1/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' - } - echo "ANYLINT_LATEST_VERSION=$( latest_version Flinesoft/AnyLint )" >> $GITHUB_ENV - echo "SWIFT_SH_LATEST_VERSION=$( latest_version mxcl/swift-sh )" >> $GITHUB_ENV - - - name: AnyLint Cache - uses: actions/cache@v1 - id: anylint-cache - with: - path: anylint-cache - key: ${{ runner.os }}-v1-anylint-${{ env.ANYLINT_LATEST_VERSION }}-swift-sh-${{ env.SWIFT_SH_LATEST_VERSION }} - - - name: Copy from cache - if: steps.anylint-cache.outputs.cache-hit - run: | - sudo cp -f anylint-cache/anylint /usr/local/bin/anylint - sudo cp -f anylint-cache/swift-sh /usr/local/bin/swift-sh - - - name: Install AnyLint - if: steps.anylint-cache.outputs.cache-hit != 'true' - run: | - git clone https://github.com/Flinesoft/AnyLint.git - cd AnyLint - swift build -c release - sudo cp -f .build/release/anylint /usr/local/bin/anylint - - - name: Install swift-sh - if: steps.anylint-cache.outputs.cache-hit != 'true' - run: | - git clone https://github.com/mxcl/swift-sh.git - cd swift-sh - swift build -c release - sudo cp -f .build/release/swift-sh /usr/local/bin/swift-sh - - - name: Copy to cache - if: steps.anylint-cache.outputs.cache-hit != 'true' - run: | - mkdir -p anylint-cache - cp -f /usr/local/bin/anylint anylint-cache/anylint - cp -f /usr/local/bin/swift-sh anylint-cache/swift-sh - - - name: Cleanup checkouts - run: rm -rf AnyLint && rm -rf swift-sh - + - name: Checkout Source + uses: actions/checkout@v3 + - name: Run AnyLint run: anylint - swiftlint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Run SwiftLint - uses: norio-nomura/action-swiftlint@3.1.0 - with: - args: --strict - - test-linux: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 + run: swiftlint --strict - name: Run tests run: swift test -v - - test-macos: - runs-on: macos-11 - - steps: - - uses: actions/checkout@v2 - - - name: Run tests - run: swift test -v --enable-code-coverage - - - name: Report Code Coverage - run: | - xcrun llvm-cov export -format="lcov" .build/debug/${PACKAGE_NAME}PackageTests.xctest/Contents/MacOS/${PACKAGE_NAME}PackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov - bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage.lcov - env: - PACKAGE_NAME: AnyLint - CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b21eb6..d3c0de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,11 +60,11 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ## [0.9.0] - 2022-04-24 ### Added - Added new option `violationLocation` parameter for `checkFileContents` for specifying position of violation marker using `.init(range:bound:)`, where `range` can be one of `.fullMatch` or `.captureGroup(index:)` and bound one of `.lower` or `.upper`. - + ## [0.8.5] - 2022-04-24 ### Fixed - Fixed an issue where first violation can't be shown in Xcode due to 'swift-driver version: 1.45.2' printed on same line. - + ## [0.8.4] - 2022-04-01 ### Fixed - Fixed an issue with pointing to the wrong Swift-SH path on Apple Silicon Macs. Should also fix the path on Linux. diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index 204ff49..f17ebbe 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -48,8 +48,8 @@ class SingleCommand: Command { } let configurationPaths = customPaths.isEmpty - ? [fileManager.currentDirectoryPath.appendingPathComponent(CLIConstants.defaultConfigFileName)] - : customPaths + ? [fileManager.currentDirectoryPath.appendingPathComponent(CLIConstants.defaultConfigFileName)] + : customPaths // init subcommand if let initTemplateName = initTemplateName { diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index eb118b5..58ac5b1 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,6 +1,5 @@ -// Generated using Sourcery 0.18.0 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.0.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT - @testable import AnyLintTests @testable import Utility import XCTest @@ -8,85 +7,85 @@ import XCTest // swiftlint:disable line_length file_length extension ArrayExtTests { - static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ - ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) - ] + static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ + ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) + ] } extension AutoCorrectionTests { - static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), - ("testAppliedMessageLines", testAppliedMessageLines) - ] + static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ + ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), + ("testAppliedMessageLines", testAppliedMessageLines) + ] } extension CheckInfoTests { - static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral) - ] + static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ + ("testInitWithStringLiteral", testInitWithStringLiteral) + ] } extension FileContentsCheckerTests { - static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck), - ("testSkipInFile", testSkipInFile), - ("testSkipHere", testSkipHere), - ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), - ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected) - ] + static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ + ("testPerformCheck", testPerformCheck), + ("testSkipInFile", testSkipInFile), + ("testSkipHere", testSkipHere), + ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), + ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected) + ] } extension FilePathsCheckerTests { - static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck) - ] + static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ + ("testPerformCheck", testPerformCheck) + ] } extension FilesSearchTests { - static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ - ("testAllFilesWithinPath", testAllFilesWithinPath), - ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions) - ] + static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ + ("testAllFilesWithinPath", testAllFilesWithinPath), + ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions) + ] } extension LintTests { - static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ - ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), - ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), - ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), - ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups) - ] + static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ + ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), + ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), + ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), + ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups) + ] } extension RegexExtTests { - static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral), - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral) - ] + static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ + ("testInitWithStringLiteral", testInitWithStringLiteral), + ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral) + ] } extension StatisticsTests { - static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ - ("testFoundViolationsInCheck", testFoundViolationsInCheck), - ("testLogSummary", testLogSummary) - ] + static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ + ("testFoundViolationsInCheck", testFoundViolationsInCheck), + ("testLogSummary", testLogSummary) + ] } extension ViolationTests { - static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ - ("testLocationMessage", testLocationMessage) - ] + static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ + ("testLocationMessage", testLocationMessage) + ] } XCTMain([ - testCase(ArrayExtTests.allTests), - testCase(AutoCorrectionTests.allTests), - testCase(CheckInfoTests.allTests), - testCase(FileContentsCheckerTests.allTests), - testCase(FilePathsCheckerTests.allTests), - testCase(FilesSearchTests.allTests), - testCase(LintTests.allTests), - testCase(RegexExtTests.allTests), - testCase(StatisticsTests.allTests), - testCase(ViolationTests.allTests) + testCase(ArrayExtTests.allTests), + testCase(AutoCorrectionTests.allTests), + testCase(CheckInfoTests.allTests), + testCase(FileContentsCheckerTests.allTests), + testCase(FilePathsCheckerTests.allTests), + testCase(FilesSearchTests.allTests), + testCase(LintTests.allTests), + testCase(RegexExtTests.allTests), + testCase(StatisticsTests.allTests), + testCase(ViolationTests.allTests) ]) From 6bce238c5b360d79236714777e7c9b27640ab3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 06:44:11 +0200 Subject: [PATCH 10/17] Make non-verbose output for running tests --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e51c569..53c81af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,4 +27,4 @@ jobs: run: swiftlint --strict - name: Run tests - run: swift test -v + run: swift test From 253ef90629bb3ceb00f4ca80127f4f8613fc8292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 07:00:14 +0200 Subject: [PATCH 11/17] Turn off linters + switch back to GitHub runners --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53c81af..640b1ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,17 +14,17 @@ concurrency: jobs: ci: - runs-on: [self-hosted] + runs-on: macos-latest steps: - name: Checkout Source uses: actions/checkout@v3 - - name: Run AnyLint - run: anylint + # - name: Run AnyLint + # run: anylint - - name: Run SwiftLint - run: swiftlint --strict + # - name: Run SwiftLint + # run: swiftlint --strict - name: Run tests run: swift test From 804586d2a64105808f0d4a11a65e95c0e221ba60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 07:04:43 +0200 Subject: [PATCH 12/17] Re-enable SwiftLint via marketplace action --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 640b1ca..ed7577f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,8 +23,10 @@ jobs: # - name: Run AnyLint # run: anylint - # - name: Run SwiftLint - # run: swiftlint --strict + - name: Run SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + with: + args: --strict - name: Run tests run: swift test From d9eb67460dc5b6ea58207e5de7410d298afb6d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 07:08:30 +0200 Subject: [PATCH 13/17] Use Ubuntu to run SwiftLint check --- .github/workflows/main.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed7577f..d25cd80 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,20 +13,25 @@ concurrency: jobs: - ci: - runs-on: macos-latest + swiftlint: + runs-on: ubuntu-latest steps: - name: Checkout Source uses: actions/checkout@v3 - - # - name: Run AnyLint - # run: anylint - name: Run SwiftLint uses: norio-nomura/action-swiftlint@3.2.1 with: args: --strict + ci: + runs-on: macos-latest + needs: swiftlint + + steps: + - name: Checkout Source + uses: actions/checkout@v3 + - name: Run tests run: swift test From 4c21d889e8068c62f67e00509cbadaedafb950f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 07:18:10 +0200 Subject: [PATCH 14/17] Fix all SwiftLint warnings --- .swiftlint.yml | 7 +- Package.swift | 4 - Sources/AnyLint/AutoCorrection.swift | 26 ++-- .../Checkers/FileContentsChecker.swift | 48 +++--- .../AnyLint/Checkers/FilePathsChecker.swift | 16 +- .../AnyLint/Extensions/FileManagerExt.swift | 10 +- Sources/AnyLint/Extensions/StringExt.swift | 10 +- Sources/AnyLint/FilesSearch.swift | 62 ++++---- Sources/AnyLint/Lint.swift | 100 ++++++------- Sources/AnyLint/Severity.swift | 18 +-- Sources/AnyLint/Statistics.swift | 52 +++---- Sources/AnyLint/Violation.swift | 12 +- Sources/AnyLint/ViolationLocationConfig.swift | 10 +- .../AnyLintCLI/Commands/SingleCommand.swift | 30 ++-- .../BlankTemplate.swift | 138 +++++++++--------- .../ConfigurationTemplate.swift | 26 ++-- Sources/AnyLintCLI/Globals/CLIConstants.swift | 48 +++--- .../AnyLintCLI/Globals/ValidateOrFail.swift | 2 +- Sources/AnyLintCLI/Tasks/InitTask.swift | 14 +- Sources/AnyLintCLI/Tasks/LintTask.swift | 22 +-- Sources/Utility/Constants.swift | 18 +-- .../Utility/Extensions/FileManagerExt.swift | 2 +- Sources/Utility/Extensions/RegexExt.swift | 12 +- Sources/Utility/Extensions/StringExt.swift | 16 +- Sources/Utility/Logger.swift | 74 +++++----- Sources/Utility/TestHelper.swift | 8 +- Tests/AnyLintCLITests/AnyLintCLITests.swift | 7 - Tests/AnyLintTests/AutoCorrectionTests.swift | 4 +- Tests/AnyLintTests/CheckInfoTests.swift | 10 +- .../Checkers/FileContentsCheckerTests.swift | 68 ++++----- .../Checkers/FilePathsCheckerTests.swift | 22 +-- .../Extensions/ArrayExtTests.swift | 4 +- .../Extensions/XCTestCaseExt.swift | 10 +- Tests/AnyLintTests/FilesSearchTests.swift | 12 +- Tests/AnyLintTests/LintTests.swift | 40 ++--- Tests/AnyLintTests/RegexExtTests.swift | 2 +- Tests/AnyLintTests/StatisticsTests.swift | 36 ++--- Tests/AnyLintTests/ViolationTests.swift | 8 +- .../Extensions/RegexExtTests.swift | 22 +-- 39 files changed, 510 insertions(+), 520 deletions(-) delete mode 100644 Tests/AnyLintCLITests/AnyLintCLITests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index c1c1009..44483a7 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -85,8 +85,10 @@ excluded: - Tests/LinuxMain.swift disabled_rules: - - todo + - blanket_disable_command - cyclomatic_complexity + - todo + # Rule Configurations conditional_returns_on_newline: @@ -107,6 +109,9 @@ identifier_name: - db - to +indentation_width: + indentation_width: 3 + line_length: warning: 160 ignores_comments: true diff --git a/Package.swift b/Package.swift index 39e2155..7cae923 100644 --- a/Package.swift +++ b/Package.swift @@ -25,10 +25,6 @@ let package = Package( name: "AnyLintCLI", dependencies: ["Rainbow", "SwiftCLI", "Utility"] ), - .testTarget( - name: "AnyLintCLITests", - dependencies: ["AnyLintCLI"] - ), .testTarget( name: "UtilityTests", dependencies: ["Utility"] diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift index b5e40d2..2932598 100644 --- a/Sources/AnyLint/AutoCorrection.swift +++ b/Sources/AnyLint/AutoCorrection.swift @@ -5,27 +5,27 @@ import Utility public struct AutoCorrection { /// The matching text before applying the autocorrection. public let before: String - + /// The matching text after applying the autocorrection. public let after: String - + var appliedMessageLines: [String] { if useDiffOutput, #available(OSX 10.15, *) { var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] - + let beforeLines = before.components(separatedBy: .newlines) let afterLines = after.components(separatedBy: .newlines) - + for difference in afterLines.difference(from: beforeLines).sorted() { switch difference { case let .insert(offset, element, _): lines.append("+ [L\(offset + 1)] \(element)".green) - + case let .remove(offset, element, _): lines.append("- [L\(offset + 1)] \(element)".red) } } - + return lines } else { return [ @@ -35,12 +35,12 @@ public struct AutoCorrection { ] } } - + var useDiffOutput: Bool { before.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing || after.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing } - + /// Initializes an autocorrection. public init(before: String, after: String) { self.before = before @@ -58,7 +58,7 @@ extension AutoCorrection: ExpressibleByDictionaryLiteral { log.exit(status: .failure) exit(EXIT_FAILURE) // only reachable in unit tests } - + self = AutoCorrection(before: before, after: after) } } @@ -70,20 +70,20 @@ extension CollectionDifference.Change: Comparable where ChangeElement == String switch (lhs, rhs) { case let (.remove(leftOffset, _, _), .remove(rightOffset, _, _)), let (.insert(leftOffset, _, _), .insert(rightOffset, _, _)): return leftOffset < rightOffset - + case let (.remove(leftOffset, _, _), .insert(rightOffset, _, _)): return leftOffset < rightOffset || true - + case let (.insert(leftOffset, _, _), .remove(rightOffset, _, _)): return leftOffset < rightOffset || false } } - + public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case let (.remove(leftOffset, _, _), .remove(rightOffset, _, _)), let (.insert(leftOffset, _, _), .insert(rightOffset, _, _)): return leftOffset == rightOffset - + case (.remove, .insert), (.insert, .remove): return false } diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index 822a25c..d832efc 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -14,79 +14,79 @@ extension FileContentsChecker: Checker { func performCheck() throws -> [Violation] { // swiftlint:disable:this function_body_length log.message("Start checking \(checkInfo) ...", level: .debug) var violations: [Violation] = [] - + for filePath in filePathsToCheck.reversed() { log.message("Start reading contents of file at \(filePath) ...", level: .debug) - + if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { var newFileContents: String = fileContents let linesInFile: [String] = fileContents.components(separatedBy: .newlines) - + // skip check in file if contains `AnyLint.skipInFile: ` let skipInFileRegex = try Regex(#"AnyLint\.skipInFile:[^\n]*([, ]All[,\s]|[, ]\#(checkInfo.id)[,\s])"#) guard !skipInFileRegex.matches(fileContents) else { log.message("Skipping \(checkInfo) in file \(filePath) due to 'AnyLint.skipInFile' instruction ...", level: .debug) continue } - + let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(checkInfo.id)"#) - + for match in regex.matches(in: fileContents).reversed() { let locationInfo: String.LocationInfo - + switch self.violationLocation.range { case .fullMatch: switch self.violationLocation.bound { case .lower: locationInfo = fileContents.locationInfo(of: match.range.lowerBound) - + case .upper: locationInfo = fileContents.locationInfo(of: match.range.upperBound) } - + case .captureGroup(let index): let capture = match.captures[index]! let captureRange = NSRange(match.string.range(of: capture)!, in: match.string) - + switch self.violationLocation.bound { case .lower: locationInfo = fileContents.locationInfo( of: fileContents.index(match.range.lowerBound, offsetBy: captureRange.location) ) - + case .upper: locationInfo = fileContents.locationInfo( of: fileContents.index(match.range.lowerBound, offsetBy: captureRange.location + captureRange.length) ) } } - + log.message("Found violating match at \(locationInfo) ...", level: .debug) - + // skip found match if contains `AnyLint.skipHere: ` in same line or one line before guard !linesInFile.containsLine(at: [locationInfo.line - 2, locationInfo.line - 1], matchingRegex: skipHereRegex) else { log.message("Skip reporting last match due to 'AnyLint.skipHere' instruction ...", level: .debug) continue } - + let autoCorrection: AutoCorrection? = { guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } - + let newMatchString = regex.replaceAllCaptures(in: match.string, with: autoCorrectReplacement) return AutoCorrection(before: match.string, after: newMatchString) }() - + if let autoCorrection = autoCorrection { guard match.string != autoCorrection.after else { // can skip auto-correction & violation reporting because auto-correct replacement is equal to matched string continue } - + // apply auto correction newFileContents.replaceSubrange(match.range, with: autoCorrection.after) log.message("Applied autocorrection for last match ...", level: .debug) } - + log.message("Reporting violation for \(checkInfo) in file \(filePath) at \(locationInfo) ...", level: .debug) violations.append( Violation( @@ -98,7 +98,7 @@ extension FileContentsChecker: Checker { ) ) } - + if newFileContents != fileContents { log.message("Rewriting contents of file \(filePath) due to autocorrection changes ...", level: .debug) try newFileContents.write(toFile: filePath, atomically: true, encoding: .utf8) @@ -109,18 +109,18 @@ extension FileContentsChecker: Checker { level: .warning ) } - + Statistics.shared.checkedFiles(at: [filePath]) } - + violations = violations.reversed() - + if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { log.message("Repeating check \(checkInfo) because auto-corrections were applied on last run.", level: .debug) - + // only paths where auto-corrections were applied need to be re-checked let filePathsToReCheck = Array(Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! })).sorted() - + let violationsOnRechecks = try FileContentsChecker( checkInfo: checkInfo, regex: regex, @@ -131,7 +131,7 @@ extension FileContentsChecker: Checker { ).performCheck() violations.append(contentsOf: violationsOnRechecks) } - + return violations } } diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index ac2f6e7..ea0316f 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -12,7 +12,7 @@ struct FilePathsChecker { extension FilePathsChecker: Checker { func performCheck() throws -> [Violation] { var violations: [Violation] = [] - + if violateIfNoMatchesFound { let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count if matchingFilePathsCount <= 0 { @@ -24,29 +24,29 @@ extension FilePathsChecker: Checker { } else { for filePath in filePathsToCheck where regex.matches(filePath) { log.message("Found violating match for \(checkInfo) ...", level: .debug) - + let appliedAutoCorrection: AutoCorrection? = try { guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } - + let newFilePath = regex.replaceAllCaptures(in: filePath, with: autoCorrectReplacement) try fileManager.moveFileSafely(from: filePath, to: newFilePath) - + return AutoCorrection(before: filePath, after: newFilePath) }() - + if appliedAutoCorrection != nil { log.message("Applied autocorrection for last match ...", level: .debug) } - + log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) violations.append( Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil, appliedAutoCorrection: appliedAutoCorrection) ) } - + Statistics.shared.checkedFiles(at: filePathsToCheck) } - + return violations } } diff --git a/Sources/AnyLint/Extensions/FileManagerExt.swift b/Sources/AnyLint/Extensions/FileManagerExt.swift index 7e21940..c6d54c1 100644 --- a/Sources/AnyLint/Extensions/FileManagerExt.swift +++ b/Sources/AnyLint/Extensions/FileManagerExt.swift @@ -9,23 +9,23 @@ extension FileManager { log.exit(status: .failure) return // only reachable in unit tests } - + guard !fileExists(atPath: targetPath) || sourcePath.lowercased() == targetPath.lowercased() else { log.message("File already exists at target path \(targetPath) – can't move from \(sourcePath).", level: .warning) return } - + let targetParentDirectoryPath = targetPath.parentDirectoryPath if !fileExists(atPath: targetParentDirectoryPath) { try createDirectory(atPath: targetParentDirectoryPath, withIntermediateDirectories: true, attributes: nil) } - + guard fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) log.exit(status: .failure) return // only reachable in unit tests } - + if sourcePath.lowercased() == targetPath.lowercased() { // workaround issues on case insensitive file systems let temporaryTargetPath = targetPath + UUID().uuidString @@ -34,7 +34,7 @@ extension FileManager { } else { try moveItem(atPath: sourcePath, toPath: targetPath) } - + FilesSearch.shared.invalidateCache() } } diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 269eeb3..9216c7f 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -7,25 +7,25 @@ public typealias Regex = Utility.Regex extension String { /// Info about the exact location of a character in a given file. public typealias LocationInfo = (line: Int, charInLine: Int) - + /// Returns the location info for a given line index. public func locationInfo(of index: String.Index) -> LocationInfo { let prefix = self[startIndex ..< index] let prefixLines = prefix.components(separatedBy: .newlines) guard let lastPrefixLine = prefixLines.last else { return (line: 1, charInLine: 1) } - + let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 return (line: prefixLines.count, charInLine: charInLine) } - + func showNewlines() -> String { components(separatedBy: .newlines).joined(separator: #"\n"#) } - + func showWhitespaces() -> String { components(separatedBy: .whitespaces).joined(separator: "␣") } - + func showWhitespacesAndNewlines() -> String { showNewlines().showWhitespaces() } diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index 1c11efa..e2138eb 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -8,19 +8,19 @@ public final class FilesSearch { let includeFilters: [Regex] let excludeFilters: [Regex] } - + /// The shared instance. public static let shared = FilesSearch() - + private var cachedFilePaths: [SearchOptions: [String]] = [:] - + private init() {} - + /// Should be called whenever files within the current directory are renamed, moved, added or deleted. func invalidateCache() { cachedFilePaths = [:] } - + /// Returns all file paths within given `path` matching the given `include` and `exclude` filters. public func allFiles( // swiftlint:disable:this function_body_length within path: String, @@ -31,28 +31,28 @@ public final class FilesSearch { "Start searching for matching files in path \(path) with includeFilters \(includeFilters) and excludeFilters \(excludeFilters) ...", level: .debug ) - + let searchOptions = SearchOptions(pathToSearch: path, includeFilters: includeFilters, excludeFilters: excludeFilters) if let cachedFilePaths: [String] = cachedFilePaths[searchOptions] { log.message("A file search with exactly the above search options was already done and was not invalidated, using cached results ...", level: .debug) return cachedFilePaths } - + guard let url = URL(string: path, relativeTo: fileManager.currentDirectoryUrl) else { log.message("Could not convert path '\(path)' to type URL.", level: .error) log.exit(status: .failure) return [] // only reachable in unit tests } - + let propKeys = [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey] guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: propKeys, options: [], errorHandler: nil) else { log.message("Couldn't create enumerator for path '\(path)'.", level: .error) log.exit(status: .failure) return [] // only reachable in unit tests } - + var filePaths: [String] = [] - + for case let fileUrl as URL in enumerator { guard let resourceValues = try? fileUrl.resourceValues(forKeys: [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey]), @@ -63,40 +63,40 @@ public final class FilesSearch { log.exit(status: .failure) return [] // only reachable in unit tests } - + // skip if any exclude filter applies if excludeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { if !isRegularFilePath { enumerator.skipDescendants() } - + continue } - + // skip hidden files and directories -#if os(Linux) - if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { - if !isRegularFilePath { - enumerator.skipDescendants() + #if os(Linux) + if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { + if !isRegularFilePath { + enumerator.skipDescendants() + } + + continue } - - continue - } -#else - if isHiddenFilePath { - if !isRegularFilePath { - enumerator.skipDescendants() + #else + if isHiddenFilePath { + if !isRegularFilePath { + enumerator.skipDescendants() + } + + continue } - - continue - } -#endif - + #endif + guard isRegularFilePath, includeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) else { continue } - + filePaths.append(fileUrl.relativePathFromCurrent) } - + cachedFilePaths[searchOptions] = filePaths return filePaths } diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index d840e3c..1381928 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -31,14 +31,14 @@ public enum Lint { try Statistics.shared.measureTime(check: checkInfo) { validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - + validateParameterCombinations( checkInfo: checkInfo, autoCorrectReplacement: autoCorrectReplacement, autoCorrectExamples: autoCorrectExamples, violateIfNoMatchesFound: nil ) - + if let autoCorrectReplacement = autoCorrectReplacement { validateAutocorrectsAll( checkInfo: checkInfo, @@ -47,18 +47,18 @@ public enum Lint { autocorrectReplacement: autoCorrectReplacement ) } - + guard !Options.validateOnly else { Statistics.shared.executedChecks.append(checkInfo) return } - + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( within: fileManager.currentDirectoryPath, includeFilters: includeFilters, excludeFilters: excludeFilters ) - + let violations = try FileContentsChecker( checkInfo: checkInfo, regex: regex, @@ -67,11 +67,11 @@ public enum Lint { autoCorrectReplacement: autoCorrectReplacement, repeatIfAutoCorrected: repeatIfAutoCorrected ).performCheck() - + Statistics.shared.found(violations: violations, in: checkInfo) } } - + /// Checks the names of files. /// /// - Parameters: @@ -104,7 +104,7 @@ public enum Lint { autoCorrectExamples: autoCorrectExamples, violateIfNoMatchesFound: violateIfNoMatchesFound ) - + if let autoCorrectReplacement = autoCorrectReplacement { validateAutocorrectsAll( checkInfo: checkInfo, @@ -113,18 +113,18 @@ public enum Lint { autocorrectReplacement: autoCorrectReplacement ) } - + guard !Options.validateOnly else { Statistics.shared.executedChecks.append(checkInfo) return } - + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( within: fileManager.currentDirectoryPath, includeFilters: includeFilters, excludeFilters: excludeFilters ) - + let violations = try FilePathsChecker( checkInfo: checkInfo, regex: regex, @@ -132,11 +132,11 @@ public enum Lint { autoCorrectReplacement: autoCorrectReplacement, violateIfNoMatchesFound: violateIfNoMatchesFound ).performCheck() - + Statistics.shared.found(violations: violations, in: checkInfo) } } - + /// Run custom logic as checks. /// /// - Parameters: @@ -148,34 +148,34 @@ public enum Lint { Statistics.shared.executedChecks.append(checkInfo) return } - + Statistics.shared.found(violations: try customClosure(checkInfo), in: checkInfo) } } - + /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. public static func logSummaryAndExit(arguments: [String] = [], afterPerformingChecks checksToPerform: () throws -> Void = {}) throws { let failOnWarnings = arguments.contains(Constants.strictArgument) let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) let measure = arguments.contains(Constants.measureArgument) - + if targetIsXcode { log = Logger(outputType: .xcode) } - + log.logDebugLevel = arguments.contains(Constants.debugArgument) Options.validateOnly = arguments.contains(Constants.validateArgument) - + try checksToPerform() - + guard !Options.validateOnly else { Statistics.shared.logValidationSummary() log.exit(status: .success) return // only reachable in unit tests } - + Statistics.shared.logCheckSummary(printExecutionTime: measure) - + if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { log.exit(status: .failure) } else if failOnWarnings && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled { @@ -184,61 +184,57 @@ public enum Lint { log.exit(status: .success) } } - + static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { if matchingExamples.isFilled { log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) } - - for example in matchingExamples { - if !regex.matches(example) { - log.message( - "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } + + for example in matchingExamples where !regex.matches(example) { + log.message( + "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", + level: .error + ) + log.exit(status: .failure) } } - + static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { if nonMatchingExamples.isFilled { log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) } - - for example in nonMatchingExamples { - if regex.matches(example) { - log.message( - "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } + + for example in nonMatchingExamples where regex.matches(example) { + log.message( + "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", + level: .error + ) + log.exit(status: .failure) } } - + static func validateAutocorrectsAll(checkInfo: CheckInfo, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { if examples.isFilled { log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) } - + for autocorrect in examples { let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) if autocorrected != autocorrect.after { log.message( - """ - Autocorrecting example for \(checkInfo.id) did not result in expected output. - Before: '\(autocorrect.before.showWhitespacesAndNewlines())' - After: '\(autocorrected.showWhitespacesAndNewlines())' - Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' - """, - level: .error + """ + Autocorrecting example for \(checkInfo.id) did not result in expected output. + Before: '\(autocorrect.before.showWhitespacesAndNewlines())' + After: '\(autocorrected.showWhitespacesAndNewlines())' + Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' + """, + level: .error ) log.exit(status: .failure) } } } - + static func validateParameterCombinations( checkInfo: CheckInfo, autoCorrectReplacement: String?, @@ -251,7 +247,7 @@ public enum Lint { level: .warning ) } - + guard autoCorrectReplacement == nil || violateIfNoMatchesFound != true else { log.message( "Incompatible options specified for check \(checkInfo.id): autoCorrectReplacement and violateIfNoMatchesFound can't be used together.", diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift index 21992f8..1161367 100644 --- a/Sources/AnyLint/Severity.swift +++ b/Sources/AnyLint/Severity.swift @@ -5,37 +5,37 @@ import Utility public enum Severity: Int, CaseIterable { /// Use for checks that are mostly informational and not necessarily problematic. case info - + /// Use for checks that might potentially be problematic. case warning - + /// Use for checks that probably are problematic. case error - + var logLevel: Logger.PrintLevel { switch self { case .info: return .info - + case .warning: return .warning - + case .error: return .error } } - + static func from(string: String) -> Severity? { switch string { case "info", "i": return .info - + case "warning", "w": return .warning - + case "error", "e": return .error - + default: return nil } diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index a4eb4b3..0c44d3c 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -3,35 +3,35 @@ import Utility final class Statistics { static let shared = Statistics() - + var executedChecks: [CheckInfo] = [] var violationsPerCheck: [CheckInfo: [Violation]] = [:] var violationsBySeverity: [Severity: [Violation]] = [.info: [], .warning: [], .error: []] var filesChecked: Set = [] var executionTimePerCheck: [CheckInfo: TimeInterval] = [:] - + var maxViolationSeverity: Severity? { violationsBySeverity.keys.filter { !violationsBySeverity[$0]!.isEmpty }.max { $0.rawValue < $1.rawValue } } - + private init() {} - + func checkedFiles(at filePaths: [String]) { filePaths.forEach { filesChecked.insert($0) } } - + func found(violations: [Violation], in check: CheckInfo) { executedChecks.append(check) violationsPerCheck[check] = violations violationsBySeverity[check.severity]!.append(contentsOf: violations) } - + func measureTime(check: CheckInfo, lintTaskClosure: () throws -> Void) rethrows { let startedAt = Date() try lintTaskClosure() self.executionTimePerCheck[check] = Date().timeIntervalSince(startedAt) } - + /// Use for unit testing only. func reset() { executedChecks = [] @@ -39,13 +39,13 @@ final class Statistics { violationsBySeverity = [.info: [], .warning: [], .error: []] filesChecked = [] } - + func logValidationSummary() { guard log.outputType != .xcode else { log.message("Performing validations only while reporting for Xcode is probably misuse of the `-l` / `--validate` option.", level: .warning) return } - + if executedChecks.isEmpty { log.message("No checks found to validate.", level: .warning) } else { @@ -55,22 +55,22 @@ final class Statistics { ) } } - + func logCheckSummary(printExecutionTime: Bool) { // make sure first violation reports in a new line when e.g. 'swift-driver version: 1.45.2' is printed print("\n") // AnyLint.skipHere: Logger - + if executedChecks.isEmpty { log.message("No checks found to perform.", level: .warning) } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { if printExecutionTime { self.logExecutionTimes() } - + switch log.outputType { case .console, .test: logViolationsToConsole() - + case .xcode: showViolationsInXcode() } @@ -78,46 +78,46 @@ final class Statistics { if printExecutionTime { self.logExecutionTimes() } - + log.message( "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", level: .success ) } } - + func logExecutionTimes() { log.message("⏱ Executed checks sorted by their execution time:", level: .info) - + for (check, executionTime) in self.executionTimePerCheck.sorted(by: { $0.value > $1.value }) { - let milliseconds = Int(executionTime * 1000) + let milliseconds = Int(executionTime * 1_000) log.message("\(milliseconds)ms\t\(check.id)", level: .info) } } - + func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { let violations: [Violation] = violationsBySeverity[severity]! guard excludeAutocorrected else { return violations } return violations.filter { $0.appliedAutoCorrection == nil } } - + private func logViolationsToConsole() { for check in executedChecks { if let checkViolations = violationsPerCheck[check], checkViolations.isFilled { let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage(pathType: .relative) != nil } - + if violationsWithLocationMessage.isFilled { log.message( "\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:", level: check.severity.logLevel ) let numerationDigits = String(violationsWithLocationMessage.count).count - + for (index, violation) in violationsWithLocationMessage.enumerated() { let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) let prefix = "> \(violationNumString). " log.message(prefix + violation.locationMessage(pathType: .relative)!, level: check.severity.logLevel) - + let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined() if let appliedAutoCorrection = violation.appliedAutoCorrection { for messageLine in appliedAutoCorrection.appliedMessageLines { @@ -137,20 +137,20 @@ final class Statistics { } else { log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", level: check.severity.logLevel) } - + log.message(">> Hint: \(check.hint)".bold.italic, level: check.severity.logLevel) } } - + let errors = "\(violationsBySeverity[.error]!.count) error(s)" let warnings = "\(violationsBySeverity[.warning]!.count) warning(s)" - + log.message( "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) and found \(errors) & \(warnings).", level: maxViolationSeverity!.logLevel ) } - + private func showViolationsInXcode() { for severity in violationsBySeverity.keys.sorted().reversed() { let severityViolations = violationsBySeverity[severity]! diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index f04de0a..ed4a10a 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -6,19 +6,19 @@ import Utility public struct Violation { /// The info about the chack that caused this violation. public let checkInfo: CheckInfo - + /// The file path the violation is related to. public let filePath: String? - + /// 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 locationInfo: String.LocationInfo? - + /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? - + /// Initializes a violation object. public init( checkInfo: CheckInfo, @@ -33,7 +33,7 @@ public struct Violation { self.locationInfo = locationInfo self.appliedAutoCorrection = appliedAutoCorrection } - + /// Returns a string representation of a violations filled with path and line information if available. public func locationMessage(pathType: String.PathType) -> String? { guard let filePath = filePath else { return nil } diff --git a/Sources/AnyLint/ViolationLocationConfig.swift b/Sources/AnyLint/ViolationLocationConfig.swift index 82fb566..ec75b5d 100644 --- a/Sources/AnyLint/ViolationLocationConfig.swift +++ b/Sources/AnyLint/ViolationLocationConfig.swift @@ -6,23 +6,23 @@ public struct ViolationLocationConfig { public enum Range { /// Uses the full matched range of the Regex. case fullMatch - + /// Uses the capture group range of the provided index. case captureGroup(index: Int) } - + /// The bound to use for pionter reporting. One of `.lower` or `.upper`. public enum Bound { /// Uses the lower end of the provided range. case lower - + /// Uses the upper end of the provided range. case upper } - + let range: Range let bound: Bound - + /// Initializes a new instance with given range and bound. /// - Parameters: /// - range: The range to use for pointer reporting. One of `.fullMatch` or `.captureGroup(index:)`. diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index f17ebbe..b74b129 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -6,51 +6,51 @@ class SingleCommand: Command { // MARK: - Basics var name: String = CLIConstants.commandName var shortDescription: String = "Lint anything by combining the power of Swift & regular expressions." - + // MARK: - Subcommands @Flag("-v", "--version", description: "Prints the current tool version") var version: Bool - + @Flag("-x", "--xcode", description: "Prints warnings & errors in a format to be reported right within Xcodes left sidebar") var xcode: Bool - + @Flag("-d", "--debug", description: "Logs much more detailed information about what AnyLint is doing for debugging purposes") var debug: Bool - + @Flag("-s", "--strict", description: "Fails on warnings as well - by default, the command only fails on errors)") var strict: Bool - + @Flag("-l", "--validate", description: "Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`.") var validate: Bool - + @Flag("-m", "--measure", description: "Prints the time it took to execute each check for performance optimizations") var measure: Bool - + @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(CLIConstants.initTemplateCases)]") var initTemplateName: String? - + // MARK: - Options @VariadicKey("-p", "--path", description: "Provide a custom path to the config file (multiple usage supported)") var customPaths: [String] - + // MARK: - Execution func execute() throws { if xcode { log = Logger(outputType: .xcode) } - + log.logDebugLevel = debug - + // version subcommand if version { try VersionTask().perform() log.exit(status: .success) } - + let configurationPaths = customPaths.isEmpty ? [fileManager.currentDirectoryPath.appendingPathComponent(CLIConstants.defaultConfigFileName)] : customPaths - + // init subcommand if let initTemplateName = initTemplateName { guard let initTemplate = InitTask.Template(rawValue: initTemplateName) else { @@ -58,13 +58,13 @@ class SingleCommand: Command { log.exit(status: .failure) return // only reachable in unit tests } - + for configPath in configurationPaths { try InitTask(configFilePath: configPath, template: initTemplate).perform() } log.exit(status: .success) } - + // lint main command var anyConfigFileFailed = false for configPath in configurationPaths { diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 960a9c0..eed024d 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -1,78 +1,78 @@ import Foundation import Utility -// swiftlint:disable function_body_length +// swiftlint:disable function_body_length indentation_width enum BlankTemplate: ConfigurationTemplate { static func fileContents() -> String { commonPrefix + #""" - // MARK: - Variables - let readmeFile: Regex = #"^README\.md$"# - - // MARK: - Checks - // MARK: Readme - try Lint.checkFilePaths( - checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", - regex: readmeFile, - matchingExamples: ["README.md"], - nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], - violateIfNoMatchesFound: true - ) - - // MARK: ReadmePath - try Lint.checkFilePaths( - checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", - regex: #"^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$"#, - matchingExamples: ["README.markdown", "readme.md", "ReadMe.md"], - nonMatchingExamples: ["README.md", "CHANGELOG.md", "CONTRIBUTING.md", "api/help.md"], - autoCorrectReplacement: "$1README.md", - autoCorrectExamples: [ - ["before": "api/readme.md", "after": "api/README.md"], - ["before": "ReadMe.md", "after": "README.md"], - ["before": "README.markdown", "after": "README.md"], - ] - ) - - // MARK: ReadmeTopLevelTitle - try Lint.checkFileContents( - checkInfo: "ReadmeTopLevelTitle: The README.md file should only contain a single top level title.", - regex: #"(^|\n)#[^#](.*\n)*\n#[^#]"#, - matchingExamples: [ - """ - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - """, - ], - nonMatchingExamples: [ - """ - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle - """, - ], - includeFilters: [readmeFile] - ) - - // MARK: ReadmeTypoLicense - try Lint.checkFileContents( - checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.", - regex: #"([\s#]L|l)isence([\s\.,:;])"#, - matchingExamples: [" lisence:", "## Lisence\n"], - nonMatchingExamples: [" license:", "## License\n"], - includeFilters: [readmeFile], - autoCorrectReplacement: "$1icense$2", - autoCorrectExamples: [ - ["before": " lisence:", "after": " license:"], - ["before": "## Lisence\n", "after": "## License\n"], - ] - ) - """# + commonSuffix + // MARK: - Variables + let readmeFile: Regex = #"^README\.md$"# + + // MARK: - Checks + // MARK: Readme + try Lint.checkFilePaths( + checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", + regex: readmeFile, + matchingExamples: ["README.md"], + nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], + violateIfNoMatchesFound: true + ) + + // MARK: ReadmePath + try Lint.checkFilePaths( + checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", + regex: #"^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$"#, + matchingExamples: ["README.markdown", "readme.md", "ReadMe.md"], + nonMatchingExamples: ["README.md", "CHANGELOG.md", "CONTRIBUTING.md", "api/help.md"], + autoCorrectReplacement: "$1README.md", + autoCorrectExamples: [ + ["before": "api/readme.md", "after": "api/README.md"], + ["before": "ReadMe.md", "after": "README.md"], + ["before": "README.markdown", "after": "README.md"], + ] + ) + + // MARK: ReadmeTopLevelTitle + try Lint.checkFileContents( + checkInfo: "ReadmeTopLevelTitle: The README.md file should only contain a single top level title.", + regex: #"(^|\n)#[^#](.*\n)*\n#[^#]"#, + matchingExamples: [ + """ + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + """, + ], + nonMatchingExamples: [ + """ + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + """, + ], + includeFilters: [readmeFile] + ) + + // MARK: ReadmeTypoLicense + try Lint.checkFileContents( + checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.", + regex: #"([\s#]L|l)isence([\s\.,:;])"#, + matchingExamples: [" lisence:", "## Lisence\n"], + nonMatchingExamples: [" license:", "## License\n"], + includeFilters: [readmeFile], + autoCorrectReplacement: "$1icense$2", + autoCorrectExamples: [ + ["before": " lisence:", "after": " license:"], + ["before": "## Lisence\n", "after": "## License\n"], + ] + ) + """# + commonSuffix } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index 724f32a..d366dd0 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -7,20 +7,20 @@ protocol ConfigurationTemplate { extension ConfigurationTemplate { static var commonPrefix: String { - """ - #!\(CLIConstants.swiftShPath) - import AnyLint // @FlineDev - - try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { - - """ + """ + #!\(CLIConstants.swiftShPath) + import AnyLint // @FlineDev + + try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { + + """ } - + static var commonSuffix: String { - """ - - } - - """ + """ + + } + + """ } } diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift index 2cb1511..6c9dc8d 100644 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -8,10 +8,10 @@ enum CLIConstants { switch self.getPlatform() { case .intel: return "/usr/local/bin/swift-sh" - + case .appleSilicon: return "/opt/homebrew/bin/swift-sh" - + case .linux: return "/home/linuxbrew/.linuxbrew/bin/swift-sh" } @@ -24,29 +24,29 @@ extension CLIConstants { case appleSilicon case linux } - + fileprivate static func getPlatform() -> Platform { -#if os(Linux) - return .linux -#else + #if os(Linux) + return .linux + #else // Source: https://stackoverflow.com/a/69624732 - var systemInfo = utsname() - let exitCode = uname(&systemInfo) - - let fallbackPlatform: Platform = .appleSilicon - guard exitCode == EXIT_SUCCESS else { return fallbackPlatform } - - let cpuArchitecture = String(cString: &systemInfo.machine.0, encoding: .utf8) - switch cpuArchitecture { - case "x86_64": - return .intel - - case "arm64": - return .appleSilicon - - default: - return fallbackPlatform - } -#endif + var systemInfo = utsname() + let exitCode = uname(&systemInfo) + + let fallbackPlatform: Platform = .appleSilicon + guard exitCode == EXIT_SUCCESS else { return fallbackPlatform } + + let cpuArchitecture = String(cString: &systemInfo.machine.0, encoding: .utf8) + switch cpuArchitecture { + case "x86_64": + return .intel + + case "arm64": + return .appleSilicon + + default: + return fallbackPlatform + } + #endif } } diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index 6d25443..40eb7c8 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -14,7 +14,7 @@ enum ValidateOrFail { return // only reachable in unit tests } } - + static func configFileExists(at configFilePath: String) throws { guard fileManager.fileExists(atPath: configFilePath) else { log.message( diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index 5077dbe..7da3e3a 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -5,7 +5,7 @@ import Utility struct InitTask { enum Template: String, CaseIterable { case blank - + var configFileContents: String { switch self { case .blank: @@ -13,7 +13,7 @@ struct InitTask { } } } - + let configFilePath: String let template: Template } @@ -25,22 +25,22 @@ extension InitTask: TaskHandler { log.exit(status: .failure) return // only reachable in unit tests } - + ValidateOrFail.swiftShInstalled() - + log.message("Making sure config file directory exists ...", level: .info) try Task.run(bash: "mkdir -p '\(configFilePath.parentDirectoryPath)'") - + log.message("Creating config file using template '\(template.rawValue)' ...", level: .info) fileManager.createFile( atPath: configFilePath, contents: template.configFileContents.data(using: .utf8), attributes: nil ) - + log.message("Making config file executable ...", level: .info) try Task.run(bash: "chmod +x '\(configFilePath)'") - + log.message("Successfully created config file at \(configFilePath)", level: .success) } } diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index 5b86f6e..8e112de 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -14,45 +14,45 @@ extension LintTask: TaskHandler { enum LintError: Error { case configFileFailed } - + /// - Throws: `LintError.configFileFailed` if running a configuration file fails func perform() throws { try ValidateOrFail.configFileExists(at: configFilePath) - + if !fileManager.isExecutableFile(atPath: configFilePath) { try Task.run(bash: "chmod +x '\(configFilePath)'") } - + ValidateOrFail.swiftShInstalled() - + do { log.message("Start linting using config file at \(configFilePath) ...", level: .info) - + var command = "\(configFilePath.absolutePath) \(log.outputType.rawValue)" - + if logDebugLevel { command += " \(Constants.debugArgument)" } - + if failOnWarnings { command += " \(Constants.strictArgument)" } - + if validateOnly { command += " \(Constants.validateArgument)" } - + if measure { command += " \(Constants.measureArgument)" } - + try Task.run(bash: command) log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success) } catch is RunError { if log.outputType != .xcode { log.message("Linting failed using config file at \(configFilePath).", level: .error) } - + throw LintError.configFileFailed } } diff --git a/Sources/Utility/Constants.swift b/Sources/Utility/Constants.swift index 5b72cb9..d48f24d 100644 --- a/Sources/Utility/Constants.swift +++ b/Sources/Utility/Constants.swift @@ -10,31 +10,31 @@ public var log = Logger(outputType: .console) public enum Constants { /// The current tool version string. Conforms to SemVer 2.0. public static let currentVersion: String = "0.10.1" - + /// The name of this tool. public static let toolName: String = "AnyLint" - + /// The debug mode argument for command line pass-through. public static let debugArgument: String = "debug" - + /// The strict mode argument for command-line pass-through. public static let strictArgument: String = "strict" - + /// The validate-only mode argument for command-line pass-through. public static let validateArgument: String = "validate" - + /// The measure mode to see how long each check took to execute public static let measureArgument: String = "measure" - + /// The separator indicating that next come regex options. public static let regexOptionsSeparator: String = #"\"# - + /// Hint that the case insensitive option should be active on a Regex. public static let caseInsensitiveRegexOption: String = "i" - + /// Hint that the case dot matches newline option should be active on a Regex. public static let dotMatchesNewlinesRegexOption: String = "m" - + /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. public static let newlinesRequiredForDiffing: Int = 3 } diff --git a/Sources/Utility/Extensions/FileManagerExt.swift b/Sources/Utility/Extensions/FileManagerExt.swift index 89bccc7..19a4d4d 100644 --- a/Sources/Utility/Extensions/FileManagerExt.swift +++ b/Sources/Utility/Extensions/FileManagerExt.swift @@ -5,7 +5,7 @@ extension FileManager { public var currentDirectoryUrl: URL { URL(string: currentDirectoryPath)! } - + /// Checks if a file exists and the given paths and is a directory. public func fileExistsAndIsDirectory(atPath path: String) -> Bool { var isDirectory: ObjCBool = false diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/Utility/Extensions/RegexExt.swift index 608ed93..e49e363 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -20,7 +20,7 @@ extension Regex: ExpressibleByStringLiteral { return Regex.defaultOptions } }() - + do { self = try Regex(pattern, options: options) } catch { @@ -35,19 +35,19 @@ extension Regex: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, String)...) { var patternElements = elements var options: Options = Regex.defaultOptions - + if let regexOptionsValue = elements.last(where: { $0.0 == Constants.regexOptionsSeparator })?.1 { patternElements.removeAll { $0.0 == Constants.regexOptionsSeparator } - + if regexOptionsValue.contains(Constants.caseInsensitiveRegexOption) { options.insert(.ignoreCase) } - + if regexOptionsValue.contains(Constants.dotMatchesNewlinesRegexOption) { options.insert(.dotMatchesLineSeparators) } } - + do { let pattern: String = patternElements.reduce(into: "") { result, element in result.append("(?<\(element.0)>\(element.1))") } self = try Regex(pattern, options: options) @@ -64,7 +64,7 @@ extension Regex { public func replaceAllCaptures(in input: String, with template: String) -> String { replacingMatches(in: input, with: numerizedNamedCaptureRefs(in: template)) } - + /// Numerizes references to named capture groups to work around missing named capture group replacement in `NSRegularExpression` APIs. func numerizedNamedCaptureRefs(in replacementString: String) -> String { let captureGroupNameRegex = Regex(#"\(\?\<([a-zA-Z0-9_-]+)\>[^\)]+\)"#) diff --git a/Sources/Utility/Extensions/StringExt.swift b/Sources/Utility/Extensions/StringExt.swift index 6f4d5a0..37ed1dd 100644 --- a/Sources/Utility/Extensions/StringExt.swift +++ b/Sources/Utility/Extensions/StringExt.swift @@ -5,41 +5,41 @@ extension String { public enum PathType { /// The relative path. case relative - + /// The absolute path. case absolute } - + /// Returns the absolute path for a path given relative to the current directory. public var absolutePath: String { guard !self.starts(with: fileManager.currentDirectoryUrl.path) else { return self } return fileManager.currentDirectoryUrl.appendingPathComponent(self).path } - + /// Returns the relative path for a path given relative to the current directory. public var relativePath: String { guard self.starts(with: fileManager.currentDirectoryUrl.path) else { return self } return replacingOccurrences(of: fileManager.currentDirectoryUrl.path, with: "") } - + /// Returns the parent directory path. public var parentDirectoryPath: String { let url = URL(fileURLWithPath: self) guard url.pathComponents.count > 1 else { return fileManager.currentDirectoryPath } return url.deletingLastPathComponent().absoluteString } - + /// Returns the path with the given type related to the current directory. public func path(type: PathType) -> String { switch type { case .absolute: return absolutePath - + case .relative: return relativePath } } - + /// Returns the path with a components appended at it. public func appendingPathComponent(_ pathComponent: String) -> String { guard let pathUrl = URL(string: self) else { @@ -47,7 +47,7 @@ extension String { log.exit(status: .failure) return "" // only reachable in unit tests } - + return pathUrl.appendingPathComponent(pathComponent).absoluteString } } diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index e2b5775..4cf5c60 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -7,81 +7,81 @@ public final class Logger { public enum PrintLevel: String { /// Print success information. case success - + /// Print any kind of information potentially interesting to users. case info - + /// Print information that might potentially be problematic. case warning - + /// Print information that probably is problematic. case error - + /// Print detailed information for debugging purposes. case debug - + var color: Color { switch self { case .success: return Color.lightGreen - + case .info: return Color.lightBlue - + case .warning: return Color.yellow - + case .error: return Color.red - + case .debug: return Color.default } } } - + /// The output type. public enum OutputType: String { /// Output is targeted to a console to be read by developers. case console - + /// Output is targeted to Xcodes left pane to be interpreted by it to mark errors & warnings. case xcode - + /// Output is targeted for unit tests. Collect into globally accessible TestHelper. case test } - + /// The exit status. public enum ExitStatus { /// Successfully finished task. case success - + /// Failed to finish task. case failure - + var statusCode: Int32 { switch self { case .success: return EXIT_SUCCESS - + case .failure: return EXIT_FAILURE } } } - + /// The output type of the logger. public let outputType: OutputType - + /// Defines if the log should include debug logs. public var logDebugLevel: Bool = false - + /// Initializes a new Logger object with a given output type. public init(outputType: OutputType) { self.outputType = outputType } - + /// Communicates a message to the chosen output target with proper formatting based on level & source. /// /// - Parameters: @@ -89,53 +89,53 @@ public final class Logger { /// - level: The level of the print statement. public func message(_ message: String, level: PrintLevel) { guard level != .debug || logDebugLevel else { return } - + switch outputType { case .console: consoleMessage(message, level: level) - + case .xcode: xcodeMessage(message, level: level) - + case .test: TestHelper.shared.consoleOutputs.append((message, level)) } } - + /// Exits the current program with the given status. public func exit(status: ExitStatus) { switch outputType { case .console, .xcode: -#if os(Linux) - Glibc.exit(status.statusCode) -#else - Darwin.exit(status.statusCode) -#endif - + #if os(Linux) + Glibc.exit(status.statusCode) + #else + Darwin.exit(status.statusCode) + #endif + case .test: TestHelper.shared.exitStatus = status } } - + private func consoleMessage(_ message: String, level: PrintLevel) { switch level { case .success: print(formattedCurrentTime(), "✅", message.green) - + case .info: print(formattedCurrentTime(), "ℹ️ ", message.lightCyan) - + case .warning: print(formattedCurrentTime(), "⚠️ ", message.yellow) - + case .error: print(formattedCurrentTime(), "❌", message.red) - + case .debug: print(formattedCurrentTime(), "💬", message) } } - + /// Reports a message in an Xcode compatible format to be shown in the left pane. /// /// - Parameters: @@ -149,7 +149,7 @@ public final class Logger { print("\(level.rawValue): \(Constants.toolName): \(message)") } } - + private func formattedCurrentTime() -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm:ss.SSS" diff --git a/Sources/Utility/TestHelper.swift b/Sources/Utility/TestHelper.swift index 5bbf0ac..978ac27 100644 --- a/Sources/Utility/TestHelper.swift +++ b/Sources/Utility/TestHelper.swift @@ -4,16 +4,16 @@ import Foundation public final class TestHelper { /// The console output data. public typealias ConsoleOutput = (message: String, level: Logger.PrintLevel) - + /// The shared `TestHelper` object. public static let shared = TestHelper() - + /// Use only in Unit Tests. public var consoleOutputs: [ConsoleOutput] = [] - + /// Use only in Unit Tests. public var exitStatus: Logger.ExitStatus? - + /// Deletes all data collected until now. public func reset() { consoleOutputs = [] diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift deleted file mode 100644 index 953b8f8..0000000 --- a/Tests/AnyLintCLITests/AnyLintCLITests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -final class AnyLintCLITests: XCTestCase { - func testExample() { - // TODO: [cg_2020-03-07] not yet implemented - } -} diff --git a/Tests/AnyLintTests/AutoCorrectionTests.swift b/Tests/AnyLintTests/AutoCorrectionTests.swift index f0bff5e..2a94689 100644 --- a/Tests/AnyLintTests/AutoCorrectionTests.swift +++ b/Tests/AnyLintTests/AutoCorrectionTests.swift @@ -7,7 +7,7 @@ final class AutoCorrectionTests: XCTestCase { XCTAssertEqual(autoCorrection.before, "Lisence") XCTAssertEqual(autoCorrection.after, "License") } - + func testAppliedMessageLines() { let singleLineAutoCorrection: AutoCorrection = ["before": "Lisence", "after": "License"] XCTAssertEqual( @@ -18,7 +18,7 @@ final class AutoCorrectionTests: XCTestCase { "+ License", ] ) - + let multiLineAutoCorrection: AutoCorrection = [ "before": "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", "after": "A\nB\nD\nE\nF1\nF2\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", diff --git a/Tests/AnyLintTests/CheckInfoTests.swift b/Tests/AnyLintTests/CheckInfoTests.swift index 422372c..2181f79 100644 --- a/Tests/AnyLintTests/CheckInfoTests.swift +++ b/Tests/AnyLintTests/CheckInfoTests.swift @@ -7,25 +7,25 @@ final class CheckInfoTests: XCTestCase { log = Logger(outputType: .test) TestHelper.shared.reset() } - + func testInitWithStringLiteral() { XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) - + let checkInfo1: CheckInfo = "test1@error: hint1" XCTAssertEqual(checkInfo1.id, "test1") XCTAssertEqual(checkInfo1.hint, "hint1") XCTAssertEqual(checkInfo1.severity, .error) - + let checkInfo2: CheckInfo = "test2@warning: hint2" XCTAssertEqual(checkInfo2.id, "test2") XCTAssertEqual(checkInfo2.hint, "hint2") XCTAssertEqual(checkInfo2.severity, .warning) - + let checkInfo3: CheckInfo = "test3@info: hint3" XCTAssertEqual(checkInfo3.id, "test3") XCTAssertEqual(checkInfo3.hint, "hint3") XCTAssertEqual(checkInfo3.severity, .info) - + let checkInfo4: CheckInfo = "test4: hint4" XCTAssertEqual(checkInfo4.id, "test4") XCTAssertEqual(checkInfo4.hint, "hint4") diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index 15a2405..327c7a5 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -9,13 +9,13 @@ final class FileContentsCheckerTests: XCTestCase { log = Logger(outputType: .test) TestHelper.shared.reset() } - + func testPerformCheck() { let temporaryFiles: [TemporaryFile] = [ (subpath: "Sources/Hello.swift", contents: "let x = 5\nvar y = 10"), (subpath: "Sources/World.swift", contents: "let x=5\nvar y=10"), ] - + withTemporaryFiles(temporaryFiles) { filePathsToCheck in let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) let violations = try FileContentsChecker( @@ -26,28 +26,28 @@ final class FileContentsCheckerTests: XCTestCase { autoCorrectReplacement: nil, repeatIfAutoCorrected: false ).performCheck() - + XCTAssertEqual(violations.count, 2) - + XCTAssertEqual(violations[0].checkInfo, checkInfo) XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[0].locationInfo!.line, 1) XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[1].checkInfo, checkInfo) XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[1].locationInfo!.line, 2) XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) } } - + func testSkipInFile() { let temporaryFiles: [TemporaryFile] = [ (subpath: "Sources/Hello.swift", contents: "// AnyLint.skipInFile: OtherRule, Whitespacing\n\n\nlet x=5\nvar y=10"), (subpath: "Sources/World.swift", contents: "// AnyLint.skipInFile: All\n\n\nlet x=5\nvar y=10"), (subpath: "Sources/Foo.swift", contents: "// AnyLint.skipInFile: OtherRule\n\n\nlet x=5\nvar y=10"), ] - + withTemporaryFiles(temporaryFiles) { filePathsToCheck in let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) let violations = try FileContentsChecker( @@ -58,21 +58,21 @@ final class FileContentsCheckerTests: XCTestCase { autoCorrectReplacement: nil, repeatIfAutoCorrected: false ).performCheck() - + XCTAssertEqual(violations.count, 2) - + XCTAssertEqual(violations[0].checkInfo, checkInfo) XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/Foo.swift") XCTAssertEqual(violations[0].locationInfo!.line, 4) XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[1].checkInfo, checkInfo) XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/Foo.swift") XCTAssertEqual(violations[1].locationInfo!.line, 5) XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) } } - + func testSkipHere() { let temporaryFiles: [TemporaryFile] = [ (subpath: "Sources/Hello.swift", contents: "// AnyLint.skipHere: OtherRule, Whitespacing\n\n\nlet x=5\nvar y=10"), @@ -80,7 +80,7 @@ final class FileContentsCheckerTests: XCTestCase { (subpath: "Sources/Foo.swift", contents: "\n\n\nlet x=5\nvar y=10 // AnyLint.skipHere: OtherRule, Whitespacing\n"), (subpath: "Sources/Bar.swift", contents: "\n\n\nlet x=5\nvar y=10\n// AnyLint.skipHere: OtherRule, Whitespacing"), ] - + withTemporaryFiles(temporaryFiles) { filePathsToCheck in let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) let violations = try FileContentsChecker( @@ -91,47 +91,47 @@ final class FileContentsCheckerTests: XCTestCase { autoCorrectReplacement: nil, repeatIfAutoCorrected: false ).performCheck() - + XCTAssertEqual(violations.count, 6) - + XCTAssertEqual(violations[0].checkInfo, checkInfo) XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/Hello.swift") XCTAssertEqual(violations[0].locationInfo!.line, 4) XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[1].checkInfo, checkInfo) XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/Hello.swift") XCTAssertEqual(violations[1].locationInfo!.line, 5) XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[2].checkInfo, checkInfo) XCTAssertEqual(violations[2].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[2].locationInfo!.line, 5) XCTAssertEqual(violations[2].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[3].checkInfo, checkInfo) XCTAssertEqual(violations[3].filePath, "\(tempDir)/Sources/Foo.swift") XCTAssertEqual(violations[3].locationInfo!.line, 4) XCTAssertEqual(violations[3].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[4].checkInfo, checkInfo) XCTAssertEqual(violations[4].filePath, "\(tempDir)/Sources/Bar.swift") XCTAssertEqual(violations[4].locationInfo!.line, 4) XCTAssertEqual(violations[4].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[5].checkInfo, checkInfo) XCTAssertEqual(violations[5].filePath, "\(tempDir)/Sources/Bar.swift") XCTAssertEqual(violations[5].locationInfo!.line, 5) XCTAssertEqual(violations[5].locationInfo!.charInLine, 1) } } - + func testSkipIfEqualsToAutocorrectReplacement() { let temporaryFiles: [TemporaryFile] = [ (subpath: "Sources/Hello.swift", contents: "let x = 5\nvar y = 10"), (subpath: "Sources/World.swift", contents: "let x =5\nvar y= 10"), ] - + withTemporaryFiles(temporaryFiles) { filePathsToCheck in let checkInfo = CheckInfo(id: "Whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) let violations = try FileContentsChecker( @@ -142,27 +142,27 @@ final class FileContentsCheckerTests: XCTestCase { autoCorrectReplacement: "$1 $2 = $3", repeatIfAutoCorrected: false ).performCheck() - + XCTAssertEqual(violations.count, 2) - + XCTAssertEqual(violations[0].checkInfo, checkInfo) XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[0].locationInfo!.line, 1) XCTAssertEqual(violations[0].locationInfo!.charInLine, 1) - + XCTAssertEqual(violations[1].checkInfo, checkInfo) XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[1].locationInfo!.line, 2) XCTAssertEqual(violations[1].locationInfo!.charInLine, 1) } } - + func testRepeatIfAutoCorrected() { let temporaryFiles: [TemporaryFile] = [ (subpath: "Sources/Hello.swift", contents: "let x = 500\nvar y = 10000"), (subpath: "Sources/World.swift", contents: "let x = 50000000\nvar y = 100000000000000"), ] - + withTemporaryFiles(temporaryFiles) { filePathsToCheck in let checkInfo = CheckInfo(id: "LongNumbers", hint: "Format long numbers with `_` after each triple of digits from the right.", severity: .warning) let violations = try FileContentsChecker( @@ -173,45 +173,45 @@ final class FileContentsCheckerTests: XCTestCase { autoCorrectReplacement: "$1_$2", repeatIfAutoCorrected: true ).performCheck() - + XCTAssertEqual(violations.count, 7) - + XCTAssertEqual(violations[0].checkInfo, checkInfo) XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/Hello.swift") XCTAssertEqual(violations[0].locationInfo!.line, 2) XCTAssertEqual(violations[0].locationInfo!.charInLine, 9) XCTAssertEqual(violations[0].appliedAutoCorrection!.after, "10_000") - + XCTAssertEqual(violations[1].checkInfo, checkInfo) XCTAssertEqual(violations[1].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[1].locationInfo!.line, 1) XCTAssertEqual(violations[1].locationInfo!.charInLine, 9) XCTAssertEqual(violations[1].appliedAutoCorrection!.after, "50000_000") - + XCTAssertEqual(violations[2].checkInfo, checkInfo) XCTAssertEqual(violations[2].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[2].locationInfo!.line, 2) XCTAssertEqual(violations[2].locationInfo!.charInLine, 9) XCTAssertEqual(violations[2].appliedAutoCorrection!.after, "100000000000_000") - + XCTAssertEqual(violations[3].checkInfo, checkInfo) XCTAssertEqual(violations[3].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[3].locationInfo!.line, 1) XCTAssertEqual(violations[3].locationInfo!.charInLine, 9) XCTAssertEqual(violations[3].appliedAutoCorrection!.after, "50_000") - + XCTAssertEqual(violations[4].checkInfo, checkInfo) XCTAssertEqual(violations[4].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[4].locationInfo!.line, 2) XCTAssertEqual(violations[4].locationInfo!.charInLine, 9) XCTAssertEqual(violations[4].appliedAutoCorrection!.after, "100000000_000") - + XCTAssertEqual(violations[5].checkInfo, checkInfo) XCTAssertEqual(violations[5].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[5].locationInfo!.line, 2) XCTAssertEqual(violations[5].locationInfo!.charInLine, 9) XCTAssertEqual(violations[5].appliedAutoCorrection!.after, "100000_000") - + XCTAssertEqual(violations[6].checkInfo, checkInfo) XCTAssertEqual(violations[6].filePath, "\(tempDir)/Sources/World.swift") XCTAssertEqual(violations[6].locationInfo!.line, 2) diff --git a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift index 7d8b2e7..62724c5 100644 --- a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift @@ -7,7 +7,7 @@ final class FilePathsCheckerTests: XCTestCase { log = Logger(outputType: .test) TestHelper.shared.reset() } - + func testPerformCheck() { withTemporaryFiles( [ @@ -18,18 +18,18 @@ final class FilePathsCheckerTests: XCTestCase { let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 0) } - + withTemporaryFiles([(subpath: "Sources/World.swift", contents: "")]) { filePathsToCheck in let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() - + XCTAssertEqual(violations.count, 1) - + XCTAssertEqual(violations[0].checkInfo, sayHelloCheck()) XCTAssertNil(violations[0].filePath) XCTAssertNil(violations[0].locationInfo) XCTAssertNil(violations[0].locationInfo) } - + withTemporaryFiles( [ (subpath: "Sources/Hello.swift", contents: ""), @@ -37,16 +37,16 @@ final class FilePathsCheckerTests: XCTestCase { ] ) { filePathsToCheck in let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() - + XCTAssertEqual(violations.count, 1) - + XCTAssertEqual(violations[0].checkInfo, noWorldCheck()) XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") XCTAssertNil(violations[0].locationInfo) XCTAssertNil(violations[0].locationInfo) } } - + private func sayHelloChecker(filePathsToCheck: [String]) -> FilePathsChecker { FilePathsChecker( checkInfo: sayHelloCheck(), @@ -56,11 +56,11 @@ final class FilePathsCheckerTests: XCTestCase { violateIfNoMatchesFound: true ) } - + private func sayHelloCheck() -> CheckInfo { CheckInfo(id: "say_hello", hint: "Should always say hello.", severity: .info) } - + private func noWorldChecker(filePathsToCheck: [String]) -> FilePathsChecker { FilePathsChecker( checkInfo: noWorldCheck(), @@ -70,7 +70,7 @@ final class FilePathsCheckerTests: XCTestCase { violateIfNoMatchesFound: false ) } - + private func noWorldCheck() -> CheckInfo { CheckInfo(id: "no_world", hint: "Do not include the global world, be more specific instead.", severity: .error) } diff --git a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift b/Tests/AnyLintTests/Extensions/ArrayExtTests.swift index a674a54..f2b5020 100644 --- a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift +++ b/Tests/AnyLintTests/Extensions/ArrayExtTests.swift @@ -6,12 +6,12 @@ final class ArrayExtTests: XCTestCase { func testContainsLineAtIndexesMatchingRegex() { let regex: Regex = #"foo:bar"# let lines: [String] = ["hello\n foo", "hello\n foo bar", "hello bar", "\nfoo:\nbar", "foo:bar", ":foo:bar"] - + XCTAssertFalse(lines.containsLine(at: [1, 2, 3], matchingRegex: regex)) XCTAssertFalse(lines.containsLine(at: [-2, -1, 0], matchingRegex: regex)) XCTAssertFalse(lines.containsLine(at: [-1, 2, 10], matchingRegex: regex)) XCTAssertFalse(lines.containsLine(at: [3, 2], matchingRegex: regex)) - + XCTAssertTrue(lines.containsLine(at: [-2, 3, 4], matchingRegex: regex)) XCTAssertTrue(lines.containsLine(at: [5, 6, 7], matchingRegex: regex)) XCTAssertTrue(lines.containsLine(at: [-2, 4, 10], matchingRegex: regex)) diff --git a/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift index b31a729..d94562a 100644 --- a/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift +++ b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift @@ -4,12 +4,12 @@ import XCTest extension XCTestCase { typealias TemporaryFile = (subpath: String, contents: String) - + var tempDir: String { "AnyLintTempTests" } - + func withTemporaryFiles(_ temporaryFiles: [TemporaryFile], testCode: ([String]) throws -> Void) { var filePathsToCheck: [String] = [] - + for tempFile in temporaryFiles { let tempFileUrl = FileManager.default.currentDirectoryUrl.appendingPathComponent(tempDir).appendingPathComponent(tempFile.subpath) let tempFileParentDirUrl = tempFileUrl.deletingLastPathComponent() @@ -17,9 +17,9 @@ extension XCTestCase { FileManager.default.createFile(atPath: tempFileUrl.path, contents: tempFile.contents.data(using: .utf8), attributes: nil) filePathsToCheck.append(tempFileUrl.relativePathFromCurrent) } - + try? testCode(filePathsToCheck) - + try? FileManager.default.removeItem(atPath: tempDir) } } diff --git a/Tests/AnyLintTests/FilesSearchTests.swift b/Tests/AnyLintTests/FilesSearchTests.swift index 3650794..a4c5d0a 100644 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -9,7 +9,7 @@ final class FilesSearchTests: XCTestCase { log = Logger(outputType: .test) TestHelper.shared.reset() } - + func testAllFilesWithinPath() { withTemporaryFiles( [ @@ -25,7 +25,7 @@ final class FilesSearchTests: XCTestCase { excludeFilters: [] ).sorted() XCTAssertEqual(includeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift", "\(tempDir)/Sources/World.swift"]) - + let excludeFilterFilePaths = FilesSearch.shared.allFiles( within: FileManager.default.currentDirectoryPath, includeFilters: [try Regex("\(tempDir)/.*")], @@ -34,12 +34,12 @@ final class FilesSearchTests: XCTestCase { XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift"]) } } - + func testPerformanceOfSameSearchOptions() { let swiftSourcesFilePaths = (0 ... 800).map { (subpath: "Sources/Foo\($0).swift", contents: "Lorem ipsum\ndolor sit amet\n") } let testsFilePaths = (0 ... 400).map { (subpath: "Tests/Foo\($0).swift", contents: "Lorem ipsum\ndolor sit amet\n") } let storyboardSourcesFilePaths = (0 ... 300).map { (subpath: "Sources/Foo\($0).storyboard", contents: "Lorem ipsum\ndolor sit amet\n") } - + withTemporaryFiles(swiftSourcesFilePaths + testsFilePaths + storyboardSourcesFilePaths) { _ in let fileSearchCode: () -> [String] = { FilesSearch.shared.allFiles( @@ -48,10 +48,10 @@ final class FilesSearchTests: XCTestCase { excludeFilters: [try! Regex(#"\#(self.tempDir)/.*\.storyboard"#)] ) } - + // first run XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) - + measure { // subsequent runs (should be much faster) XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index e42e631..847c877 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -7,20 +7,20 @@ final class LintTests: XCTestCase { log = Logger(outputType: .test) TestHelper.shared.reset() } - + func testValidateRegexMatchesForEach() { XCTAssertNil(TestHelper.shared.exitStatus) - + let regex: Regex = #"foo[0-9]?bar"# let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) - + Lint.validate( regex: regex, matchesForEach: ["foo1bar", "foobar", "myfoo4barbeque"], checkInfo: checkInfo ) XCTAssertNil(TestHelper.shared.exitStatus) - + Lint.validate( regex: regex, matchesForEach: ["foo1bar", "FooBar", "myfoo4barbeque"], @@ -28,20 +28,20 @@ final class LintTests: XCTestCase { ) XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } - + func testValidateRegexDoesNotMatchAny() { XCTAssertNil(TestHelper.shared.exitStatus) - + let regex: Regex = #"foo[0-9]?bar"# let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) - + Lint.validate( regex: regex, doesNotMatchAny: ["fooLbar", "FooBar", "myfoo40barbeque"], checkInfo: checkInfo ) XCTAssertNil(TestHelper.shared.exitStatus) - + Lint.validate( regex: regex, doesNotMatchAny: ["fooLbar", "foobar", "myfoo40barbeque"], @@ -49,12 +49,12 @@ final class LintTests: XCTestCase { ) XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } - + func testValidateAutocorrectsAllExamplesWithAnonymousGroups() { XCTAssertNil(TestHelper.shared.exitStatus) - + let anonymousCaptureRegex = try? Regex(#"([^\.]+)(\.)([^\.]+)(\.)([^\.]+)"#) - + Lint.validateAutocorrectsAll( checkInfo: CheckInfo(id: "id", hint: "hint"), examples: [ @@ -64,9 +64,9 @@ final class LintTests: XCTestCase { regex: anonymousCaptureRegex!, autocorrectReplacement: "$5$2$3$4$1" ) - + XCTAssertNil(TestHelper.shared.exitStatus) - + Lint.validateAutocorrectsAll( checkInfo: CheckInfo(id: "id", hint: "hint"), examples: [ @@ -76,13 +76,13 @@ final class LintTests: XCTestCase { regex: anonymousCaptureRegex!, autocorrectReplacement: "$4$1$2$3$0" ) - + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } - + func testValidateAutocorrectsAllExamplesWithNamedGroups() { XCTAssertNil(TestHelper.shared.exitStatus) - + let namedCaptureRegex: Regex = [ "prefix": #"[^\.]+"#, "separator1": #"\."#, @@ -90,7 +90,7 @@ final class LintTests: XCTestCase { "separator2": #"\."#, "suffix": #"[^\.]+"#, ] - + Lint.validateAutocorrectsAll( checkInfo: CheckInfo(id: "id", hint: "hint"), examples: [ @@ -100,9 +100,9 @@ final class LintTests: XCTestCase { regex: namedCaptureRegex, autocorrectReplacement: "$suffix$separator1$content$separator2$prefix" ) - + XCTAssertNil(TestHelper.shared.exitStatus) - + Lint.validateAutocorrectsAll( checkInfo: CheckInfo(id: "id", hint: "hint"), examples: [ @@ -112,7 +112,7 @@ final class LintTests: XCTestCase { regex: namedCaptureRegex, autocorrectReplacement: "$sfx$sep1$cnt$sep2$pref" ) - + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } } diff --git a/Tests/AnyLintTests/RegexExtTests.swift b/Tests/AnyLintTests/RegexExtTests.swift index f869c96..6c40075 100644 --- a/Tests/AnyLintTests/RegexExtTests.swift +++ b/Tests/AnyLintTests/RegexExtTests.swift @@ -7,7 +7,7 @@ final class RegexExtTests: XCTestCase { let regex: Regex = #"(?capture[_\-\.]group)\s+\n(.*)"# XCTAssertEqual(regex.pattern, #"(?capture[_\-\.]group)\s+\n(.*)"#) } - + func testInitWithDictionaryLiteral() { let regex: Regex = [ "name": #"capture[_\-\.]group"#, diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index a0872c8..c3f7202 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -9,65 +9,65 @@ final class StatisticsTests: XCTestCase { TestHelper.shared.reset() Statistics.shared.reset() } - + func testFoundViolationsInCheck() { XCTAssert(Statistics.shared.executedChecks.isEmpty) XCTAssert(Statistics.shared.violationsBySeverity[.info]!.isEmpty) XCTAssert(Statistics.shared.violationsBySeverity[.warning]!.isEmpty) XCTAssert(Statistics.shared.violationsBySeverity[.error]!.isEmpty) XCTAssert(Statistics.shared.violationsPerCheck.isEmpty) - + let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) Statistics.shared.found( violations: [Violation(checkInfo: checkInfo1)], in: checkInfo1 ) - + XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1]) XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 0) XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 0) XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 1) - + let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) Statistics.shared.found( violations: [Violation(checkInfo: checkInfo2), Violation(checkInfo: checkInfo2)], in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) ) - + XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1, checkInfo2]) XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 2) XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 0) XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 2) - + let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) Statistics.shared.found( violations: [Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3)], in: CheckInfo(id: "id3", hint: "hint3", severity: .error) ) - + XCTAssertEqual(Statistics.shared.executedChecks, [checkInfo1, checkInfo2, checkInfo3]) XCTAssertEqual(Statistics.shared.violationsBySeverity[.info]!.count, 1) XCTAssertEqual(Statistics.shared.violationsBySeverity[.warning]!.count, 2) XCTAssertEqual(Statistics.shared.violationsBySeverity[.error]!.count, 3) XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 3) } - + func testLogSummary() { // swiftlint:disable:this function_body_length Statistics.shared.logCheckSummary(printExecutionTime: false) XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "No checks found to perform.") - + TestHelper.shared.reset() - + let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) Statistics.shared.found( violations: [Violation(checkInfo: checkInfo1)], in: checkInfo1 ) - + let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) Statistics.shared.found( violations: [ @@ -76,7 +76,7 @@ final class StatisticsTests: XCTestCase { ], in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) ) - + let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) Statistics.shared.found( violations: [ @@ -86,18 +86,18 @@ final class StatisticsTests: XCTestCase { ], in: CheckInfo(id: "id3", hint: "hint3", severity: .error) ) - + Statistics.shared.checkedFiles(at: ["Hogwarts/Harry.swift"]) Statistics.shared.checkedFiles(at: ["Hogwarts/Harry.swift", "Hogwarts/Albus.swift"]) Statistics.shared.checkedFiles(at: ["Hogwarts/Albus.swift"]) - + Statistics.shared.logCheckSummary(printExecutionTime: true) - + XCTAssertEqual( TestHelper.shared.consoleOutputs.map { $0.level }, [.info, .info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] ) - + let expectedOutputs = [ "⏱ Executed checks sorted by their execution time:", "\("[id1]".bold) Found 1 violation(s).", @@ -114,9 +114,9 @@ final class StatisticsTests: XCTestCase { ">> Hint: hint3".bold.italic, "Performed 3 check(s) in 2 file(s) and found 3 error(s) & 2 warning(s).", ] - + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, expectedOutputs.count) - + for (index, expectedOutput) in expectedOutputs.enumerated() { XCTAssertEqual(TestHelper.shared.consoleOutputs[index].message, expectedOutput) } diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift index dd91efa..932ba35 100644 --- a/Tests/AnyLintTests/ViolationTests.swift +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -9,20 +9,20 @@ final class ViolationTests: XCTestCase { TestHelper.shared.reset() Statistics.shared.reset() } - + func testLocationMessage() { let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) XCTAssertNil(Violation(checkInfo: checkInfo).locationMessage(pathType: .relative)) - + let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") - + let locationInfoViolation = Violation( checkInfo: checkInfo, filePath: "Temp/Souces/World.swift", locationInfo: String.LocationInfo(line: 5, charInLine: 15) ) - + XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") } } diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/UtilityTests/Extensions/RegexExtTests.swift index 78742ba..6843535 100644 --- a/Tests/UtilityTests/Extensions/RegexExtTests.swift +++ b/Tests/UtilityTests/Extensions/RegexExtTests.swift @@ -6,44 +6,44 @@ final class RegexExtTests: XCTestCase { let regex: Regex = #".*"# XCTAssertEqual(regex.description, #"/.*/"#) } - + func testStringLiteralInitWithOptions() { let regexI: Regex = #".*\i"# XCTAssertEqual(regexI.description, #"/.*/i"#) - + let regexM: Regex = #".*\m"# XCTAssertEqual(regexM.description, #"/.*/m"#) - + let regexIM: Regex = #".*\im"# XCTAssertEqual(regexIM.description, #"/.*/im"#) - + let regexMI: Regex = #".*\mi"# XCTAssertEqual(regexMI.description, #"/.*/im"#) } - + func testDictionaryLiteralInit() { let regex: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#] XCTAssertEqual(regex.description, #"/(?[a-z]+)(?\d+\.?\d*)/"#) } - + func testDictionaryLiteralInitWithOptions() { let regexI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "i"] XCTAssertEqual(regexI.description, #"/(?[a-z]+)(?\d+\.?\d*)/i"#) - + let regexM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "m"] XCTAssertEqual(regexM.description, #"/(?[a-z]+)(?\d+\.?\d*)/m"#) - + let regexMI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "mi"] XCTAssertEqual(regexMI.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) - + let regexIM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "im"] XCTAssertEqual(regexIM.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) } - + func testReplacingMatchesInInputWithTemplate() { let regexTrailing: Regex = #"(?<=\n)([-–] .*[^ ])( {0,1}| {3,})\n"# let text: String = "\n- Sample Text.\n" - + XCTAssertEqual( regexTrailing.replacingMatches(in: text, with: "$1 \n"), "\n- Sample Text. \n" From 4a3f7aa09f230488f9c9ca879e8a1462c867d157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 17:25:38 +0200 Subject: [PATCH 15/17] Fix code warnings + upgrade Package manifest to Swift 5.7 --- Package.swift | 12 ++++++------ Sources/AnyLintCLI/Globals/CLIConstants.swift | 9 +++++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index 7cae923..3227e34 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.7 import PackageDescription let package = Package( name: "AnyLint", - platforms: [.macOS(.v10_12)], + platforms: [.macOS(.v10_13)], products: [ .library(name: "AnyLint", targets: ["AnyLint"]), .executable(name: "anylint", targets: ["AnyLintCLI"]), @@ -25,13 +25,13 @@ let package = Package( name: "AnyLintCLI", dependencies: ["Rainbow", "SwiftCLI", "Utility"] ), - .testTarget( - name: "UtilityTests", - dependencies: ["Utility"] - ), .target( name: "Utility", dependencies: ["Rainbow"] ), + .testTarget( + name: "UtilityTests", + dependencies: ["Utility"] + ), ] ) diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift index 6c9dc8d..7a58490 100644 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -29,14 +29,19 @@ extension CLIConstants { #if os(Linux) return .linux #else - // Source: https://stackoverflow.com/a/69624732 + // Source: https://stackoverflow.com/a/69624732 var systemInfo = utsname() let exitCode = uname(&systemInfo) let fallbackPlatform: Platform = .appleSilicon guard exitCode == EXIT_SUCCESS else { return fallbackPlatform } - let cpuArchitecture = String(cString: &systemInfo.machine.0, encoding: .utf8) + let cpuArchitecture = withUnsafePointer(to: &systemInfo.machine) { unsafePointer in + unsafePointer.withMemoryRebound(to: CChar.self, capacity: Int(_SYS_NAMELEN)) { pointer in + String(cString: pointer) + } + } + switch cpuArchitecture { case "x86_64": return .intel From a18ae2b37b19556cc5a5f9734cfd5a8057415d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 19:26:06 +0200 Subject: [PATCH 16/17] Add `--unvalidated` option + fix performance measuring caching issue --- CHANGELOG.md | 7 +-- README.md | 1 + Sources/AnyLint/Lint.swift | 44 ++++++++++--------- Sources/AnyLint/Options.swift | 1 + .../AnyLintCLI/Commands/SingleCommand.swift | 4 ++ Sources/AnyLintCLI/Tasks/LintTask.swift | 5 +++ Sources/Utility/Constants.swift | 3 ++ 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c0de0..cd6600d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,15 +19,16 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ## [Unreleased] ### Added -- None. +- Added a new `--unvalidated` (`-u`) option for running all checks without running the validations provided, such as testing for `matchingExamples` and `nonMatchingExamples`. Use with cuation. ### Changed -- None. +- Some internal code clean-up. +- Upgrade to Swift 5.7 manifest syntax. ### Deprecated - None. ### Removed - None. ### Fixed -- None. +- The `--measure` option also measured validations & files search which distorted the measure time for the first check with the same files search. Now, it only measures the actual matching time of the Regex for better evaluation. ### Security - None. diff --git a/README.md b/README.md index c90c6e1..843965a 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ There are also several flags you can pass to `anylint`: 1. `-s` / `--strict`: Fails on warnings as well. (By default, the command only fails on errors.) 1. `-x` / `--xcode`: Prints warnings & errors in a format to be reported right within Xcodes left sidebar. 1. `-l` / `--validate`: Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`. +1. `-u` / `--unvalidated`: Runs the checks without validating their correctness. Only use for faster subsequent runs after a validated run succeeded. 1. `-m` / `--measure`: Prints the time it took to execute each check for performance optimizations. 1. `-v` / `--version`: Prints the current tool version. (Does not run any lint checks.) 1. `-d` / `--debug`: Logs much more detailed information about what AnyLint is doing for debugging purposes. diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 1381928..1444e4f 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -28,7 +28,7 @@ public enum Lint { autoCorrectExamples: [AutoCorrection] = [], repeatIfAutoCorrected: Bool = false ) throws { - try Statistics.shared.measureTime(check: checkInfo) { + if !Options.unvalidated { validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) @@ -47,18 +47,20 @@ public enum Lint { autocorrectReplacement: autoCorrectReplacement ) } + } - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters - ) + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + try Statistics.shared.measureTime(check: checkInfo) { let violations = try FileContentsChecker( checkInfo: checkInfo, regex: regex, @@ -95,7 +97,7 @@ public enum Lint { autoCorrectExamples: [AutoCorrection] = [], violateIfNoMatchesFound: Bool = false ) throws { - try Statistics.shared.measureTime(check: checkInfo) { + if !Options.unvalidated { validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) validateParameterCombinations( @@ -113,18 +115,20 @@ public enum Lint { autocorrectReplacement: autoCorrectReplacement ) } + } - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters - ) + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + try Statistics.shared.measureTime(check: checkInfo) { let violations = try FilePathsChecker( checkInfo: checkInfo, regex: regex, diff --git a/Sources/AnyLint/Options.swift b/Sources/AnyLint/Options.swift index d8d10d2..9a06ad8 100644 --- a/Sources/AnyLint/Options.swift +++ b/Sources/AnyLint/Options.swift @@ -2,4 +2,5 @@ import Foundation enum Options { static var validateOnly: Bool = false + static var unvalidated: Bool = false } diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index b74b129..8e89388 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -23,6 +23,9 @@ class SingleCommand: Command { @Flag("-l", "--validate", description: "Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`.") var validate: Bool + @Flag("-u", "--unvalidated", description: "Runs the checks without validating their correctness. Only use for faster subsequent runs after a validated run succeeded.") + var unvalidated: Bool + @Flag("-m", "--measure", description: "Prints the time it took to execute each check for performance optimizations") var measure: Bool @@ -74,6 +77,7 @@ class SingleCommand: Command { logDebugLevel: self.debug, failOnWarnings: self.strict, validateOnly: self.validate, + unvalidated: self.unvalidated, measure: self.measure ).perform() } catch LintTask.LintError.configFileFailed { diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index 8e112de..ed948c7 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -7,6 +7,7 @@ struct LintTask { let logDebugLevel: Bool let failOnWarnings: Bool let validateOnly: Bool + let unvalidated: Bool let measure: Bool } @@ -42,6 +43,10 @@ extension LintTask: TaskHandler { command += " \(Constants.validateArgument)" } + if unvalidated { + command += " \(Constants.unvalidatedArgument)" + } + if measure { command += " \(Constants.measureArgument)" } diff --git a/Sources/Utility/Constants.swift b/Sources/Utility/Constants.swift index d48f24d..7143faf 100644 --- a/Sources/Utility/Constants.swift +++ b/Sources/Utility/Constants.swift @@ -23,6 +23,9 @@ public enum Constants { /// The validate-only mode argument for command-line pass-through. public static let validateArgument: String = "validate" + /// The unvalidated mode argument for command-line pass-through. + public static let unvalidatedArgument: String = "unvalidated" + /// The measure mode to see how long each check took to execute public static let measureArgument: String = "measure" From b69f38e0e739399fceb5366bc97b5d900ecd054a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 9 Apr 2023 19:27:42 +0200 Subject: [PATCH 17/17] Bump version num & finalize new changelog section --- CHANGELOG.md | 14 ++++++++++++++ Formula/anylint.rb | 2 +- README.md | 4 ++-- Sources/Utility/Constants.swift | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6600d..28c9be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,20 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ## [Unreleased] ### Added +- None. +### Changed +- None. +### Deprecated +- None. +### Removed +- None. +### Fixed +- None. +### Security +- None. + +## [0.11.0] - 2023-04-09 +### Added - Added a new `--unvalidated` (`-u`) option for running all checks without running the validations provided, such as testing for `matchingExamples` and `nonMatchingExamples`. Use with cuation. ### Changed - Some internal code clean-up. diff --git a/Formula/anylint.rb b/Formula/anylint.rb index 1e7b376..f955c58 100644 --- a/Formula/anylint.rb +++ b/Formula/anylint.rb @@ -4,7 +4,7 @@ class Anylint < Formula url "https://github.com/FlineDev/AnyLint.git", :tag => "0.10.1", :revision => "84ee29f12ae7297e917c9a3339dfb25e5dca6dd5" head "https://github.com/FlineDev/AnyLint.git" - depends_on :xcode => ["12.5", :build] + depends_on :xcode => ["14.0", :build] depends_on "swift-sh" def install diff --git a/README.md b/README.md index 843965a..4b24ee4 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ alt="Coverage"/> - Version: 0.10.1 + Version: 0.11.0