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"