diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f545b6..ba4596f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,7 @@ jobs: path: anylint-cache key: ${{ runner.os }}-v1-anylint-${{ env.ANYLINT_LATEST_VERSION }}-swift-sh-${{ env.SWIFT_SH_LATEST_VERSION }} - - name: Copy from cache + - name: Copy from Cache if: steps.anylint-cache.outputs.cache-hit run: | sudo cp -f anylint-cache/anylint /usr/local/bin/anylint @@ -59,7 +59,7 @@ jobs: swift build -c release sudo cp -f .build/release/swift-sh /usr/local/bin/swift-sh - - name: Copy to cache + - name: Copy to Cache if: steps.anylint-cache.outputs.cache-hit != 'true' run: | mkdir -p anylint-cache @@ -89,16 +89,62 @@ jobs: 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 "::set-env name=ANYLINT_LATEST_VERSION::$( latest_version Flinesoft/AnyLint )" + echo "::set-env name=SWIFT_SH_LATEST_VERSION::$( latest_version mxcl/swift-sh )" + + - 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: Run tests run: swift test -v - test-macos: runs-on: macos-latest steps: - uses: actions/checkout@v2 + - name: Install Swift-SH + run: brew install swift-sh + - name: Run tests run: swift test -v --enable-code-coverage diff --git a/.gitignore b/.gitignore index 81b332f..1e257b5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ xcuserdata/ .codacy-coverage *.lcov codacy-coverage.json +.anylint diff --git a/Package.resolved b/Package.resolved index aa38a47..d1d638b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", "state": { "branch": null, - "revision": "c72c4564f8c0a24700a59824880536aca45a4cae", - "version": "6.0.1" + "revision": "2816678bcc37f4833d32abeddbdf5e757fa891d8", + "version": "6.0.2" } } ] diff --git a/Package.swift b/Package.swift index b266fdf..b6deffe 100644 --- a/Package.swift +++ b/Package.swift @@ -14,23 +14,19 @@ let package = Package( targets: [ .target( name: "AnyLint", - dependencies: ["Utility"] + dependencies: ["SwiftCLI", "Utility"] ), .testTarget( name: "AnyLintTests", - dependencies: ["AnyLint"] + dependencies: ["AnyLint", "Rainbow", "SwiftCLI"] ), .target( name: "AnyLintCLI", dependencies: ["Rainbow", "SwiftCLI", "Utility"] ), - .testTarget( - name: "AnyLintCLITests", - dependencies: ["AnyLintCLI"] - ), .target( name: "Utility", - dependencies: ["Rainbow"] + dependencies: ["Rainbow", "SwiftCLI"] ), .testTarget( name: "UtilityTests", diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift index eff4213..24960a2 100644 --- a/Sources/AnyLint/AutoCorrection.swift +++ b/Sources/AnyLint/AutoCorrection.swift @@ -63,7 +63,8 @@ extension AutoCorrection: ExpressibleByDictionaryLiteral { } } -// TODO: make the autocorrection diff sorted by line number +extension AutoCorrection: Codable {} + @available(OSX 10.15, *) extension CollectionDifference.Change: Comparable where ChangeElement == String { public static func < (lhs: Self, rhs: Self) -> Bool { diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index 9cd3080..8250173 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -77,3 +77,5 @@ extension CheckInfo: ExpressibleByStringLiteral { } } } + +extension CheckInfo: Codable {} diff --git a/Sources/AnyLint/Checkers/Checker.swift b/Sources/AnyLint/Checkers/Checker.swift index d0c0f56..144af7a 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 -> [CheckInfo: [Violation]] } diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index 5b03e2e..8d35a7b 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -10,7 +10,7 @@ struct FileContentsChecker { } extension FileContentsChecker: Checker { - func performCheck() throws -> [Violation] { // swiftlint:disable:this function_body_length + func performCheck() throws -> [CheckInfo: [Violation]] { // swiftlint:disable:this function_body_length log.message("Start checking \(checkInfo) ...", level: .debug) var violations: [Violation] = [] @@ -82,7 +82,7 @@ extension FileContentsChecker: Checker { ) } - Statistics.shared.checkedFiles(at: [filePath]) + Statistics.default.checkedFiles(at: [filePath]) } violations = violations.reversed() @@ -91,7 +91,9 @@ extension FileContentsChecker: Checker { 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 filePathsToReCheck = Array( + Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! }) + ).sorted() let violationsOnRechecks = try FileContentsChecker( checkInfo: checkInfo, @@ -100,9 +102,9 @@ extension FileContentsChecker: Checker { autoCorrectReplacement: autoCorrectReplacement, repeatIfAutoCorrected: repeatIfAutoCorrected ).performCheck() - violations.append(contentsOf: violationsOnRechecks) + violations.append(contentsOf: violationsOnRechecks[checkInfo]!) } - return violations + return [checkInfo: violations] } } diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index c88c77f..ca1cb59 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -10,7 +10,7 @@ struct FilePathsChecker { } extension FilePathsChecker: Checker { - func performCheck() throws -> [Violation] { + func performCheck() throws -> [CheckInfo: [Violation]] { var violations: [Violation] = [] if violateIfNoMatchesFound { @@ -44,9 +44,9 @@ extension FilePathsChecker: Checker { ) } - Statistics.shared.checkedFiles(at: filePathsToCheck) + Statistics.default.checkedFiles(at: Set(filePathsToCheck)) } - return violations + return [checkInfo: violations] } } diff --git a/Sources/AnyLint/Checkers/TemplateChecker.swift b/Sources/AnyLint/Checkers/TemplateChecker.swift new file mode 100644 index 0000000..76c5bac --- /dev/null +++ b/Sources/AnyLint/Checkers/TemplateChecker.swift @@ -0,0 +1,102 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import SwiftCLI +import Utility + +/// The source of the subchecks to run. +public enum CheckSource { + /// The device-local source, requiring a path String. + case local(String) + + /// A remote public URL source, requiring the full config file URL string. + case remote(String) + + /// A GitHub repo source config file specified via repo (e.g. 'Flinesoft/AnyLint-Swift'), version (tag or branch) and variant (a subpath to the config file). + case github(repo: String, version: String, variant: String) +} + +struct TemplateChecker { + let source: CheckSource + let runOnly: [String]? + let exclude: [String]? + let logDebugLevel: Bool +} + +extension TemplateChecker: Checker { + func performCheck() throws -> [CheckInfo: [Violation]] { + var correctedSource: CheckSource = source + + if let remoteSource = convertGitHubToRemoteSource(source: correctedSource) { + correctedSource = remoteSource + } + + if let localSource = try downloadRemoteSourceToLocal(source: correctedSource) { + correctedSource = localSource + } + + guard case let .local(templateFilePath) = correctedSource else { + log.message("Found unexpected state while validating checks source.", level: .error) + log.exit(status: .failure) + return [:] // only reachable in unit tests + } + + if !fileManager.isExecutableFile(atPath: templateFilePath) { + try Task.run(bash: "chmod +x '\(templateFilePath)'") + } + + log.message("Running local config file at '\(templateFilePath)'", level: .info) + + var command = "anylint --path \(templateFilePath.absolutePath)" + if logDebugLevel { + command += " \(Constants.debugArgument)" + } + try Task.run(bash: command) + + let dumpFileUrl = URL(fileURLWithPath: Constants.statisticsDumpFilePath) + + guard + let dumpFileData = try? Data(contentsOf: dumpFileUrl), + let dumpedStatistics = try? JSONDecoder().decode(Statistics.self, from: dumpFileData) + else { + log.message("Could not decode Statistics JSON at \(dumpFileUrl.path)", level: .error) + log.exit(status: .failure) + return [:] // only reachable in unit tests + } + + try fileManager.removeItem(atPath: Constants.statisticsDumpFilePath) + return dumpedStatistics.violationsPerCheck + } + + private func convertGitHubToRemoteSource(source: CheckSource) -> CheckSource? { + guard case let .github(repo, version, variant) = source else { return nil } + log.message("Converting .github source to .remote source ...", level: .debug) + return .remote("https://raw.githubusercontent.com/\(repo)/\(version)/\(variant).swift") + } + + private func downloadRemoteSourceToLocal(source: CheckSource) throws -> CheckSource? { + guard case let .remote(urlString) = source else { return nil } + + log.message("Downloading .remote source from '\(urlString)' ...", level: .debug) + guard let remoteUrl = URL(string: urlString) else { + log.message("`.remote` source URL string '\(urlString)' is not a valid URL.", level: .error) + log.exit(status: .failure) + return nil // only reachable in unit tests + } + + let remoteFileContents = try String(contentsOf: remoteUrl) + let uniqueFileName = ( + remoteUrl.pathComponents.dropFirst().prefix(2) + remoteUrl.deletingPathExtension().pathComponents.suffix(2) + ).joined(separator: "_") + let localFilePath = "\(Constants.tempDirPath)/\(uniqueFileName).swift" + + if !fileManager.fileExists(atPath: Constants.tempDirPath) { + try fileManager.createDirectory(atPath: Constants.tempDirPath, withIntermediateDirectories: true, attributes: nil) + } + + try remoteFileContents.write(toFile: localFilePath, atomically: true, encoding: .utf8) + + return .local(localFilePath) + } +} diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 7bcc39d..815de95 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -6,16 +6,19 @@ 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) + public struct LocationInfo: Codable { + let line: Int + let 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) } + guard let lastPrefixLine = prefixLines.last else { return LocationInfo(line: 1, charInLine: 1) } let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 - return (line: prefixLines.count, charInLine: charInLine) + return LocationInfo(line: prefixLines.count, charInLine: charInLine) } func showNewlines() -> String { diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 463d8e1..49212c1 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -46,7 +46,7 @@ public enum Lint { } guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) + Statistics.default.executedChecks.append(checkInfo) return } @@ -64,7 +64,7 @@ public enum Lint { repeatIfAutoCorrected: repeatIfAutoCorrected ).performCheck() - Statistics.shared.found(violations: violations, in: checkInfo) + Statistics.default.found(violations: violations) } /// Checks the names of files. @@ -109,7 +109,7 @@ public enum Lint { } guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) + Statistics.default.executedChecks.append(checkInfo) return } @@ -127,7 +127,7 @@ public enum Lint { violateIfNoMatchesFound: violateIfNoMatchesFound ).performCheck() - Statistics.shared.found(violations: violations, in: checkInfo) + Statistics.default.found(violations: violations) } /// Run custom logic as checks. @@ -137,11 +137,29 @@ public enum Lint { /// - customClosure: The custom logic to run which produces an array of `Violation` objects for any violations. public static func customCheck(checkInfo: CheckInfo, customClosure: (CheckInfo) -> [Violation]) { guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) + Statistics.default.executedChecks.append(checkInfo) return } - Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) + Statistics.default.found(violations: [checkInfo: customClosure(checkInfo)]) + } + + /// Run checks from a separate configuration file. + /// + /// - Parameters: + /// - source: The source to fetch the configuration file from to run checks. One of .local(String), .remote(String), .community(String, variant: String). + /// - runOnly: Instead of running all checks, only runs the ones listed in here. Acts as a whitelist. Takes precedence over `exclude`. + /// - exclude: Runs all checks except the ones specified here. Will be ignore if `runOnly` is also configured. + public static func runChecks(source: CheckSource, runOnly: [String]? = nil, exclude: [String]? = nil) throws { + guard !Options.validateOnly else { return } + + let violations = try TemplateChecker( + source: source, + runOnly: runOnly, + exclude: exclude, + logDebugLevel: log.logDebugLevel + ).performCheck() + Statistics.default.found(violations: violations) } /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. @@ -159,22 +177,64 @@ public enum Lint { try checksToPerform() guard !Options.validateOnly else { - Statistics.shared.logValidationSummary() + Statistics.default.logValidationSummary() log.exit(status: .success) return // only reachable in unit tests } - Statistics.shared.logCheckSummary() + Statistics.default.logCheckSummary() - if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { + if Statistics.default.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { log.exit(status: .failure) - } else if failOnWarnings && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled { + } else if failOnWarnings && Statistics.default.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled { log.exit(status: .failure) } else { log.exit(status: .success) } } + /// Reports the results of a check to a file for usage in reusable check templates. + public static func reportResultsToFile( + arguments: [String] = [], + afterPerformingChecks checksToPerform: () throws -> Void = {} + ) throws { + log.logDebugLevel = arguments.contains(Constants.debugArgument) + + try checksToPerform() + + let dumpFileUrl = URL(fileURLWithPath: Constants.statisticsDumpFilePath) + let statisticsToDump = Statistics() + + if let dumpFileData = try? Data(contentsOf: dumpFileUrl) { + guard let previouslyDumpedStatistics = try? JSONDecoder().decode(Statistics.self, from: dumpFileData) else { + log.message("Could not decode Statistics JSON at \(dumpFileUrl.path)", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + statisticsToDump.merge(other: previouslyDumpedStatistics) + } + + statisticsToDump.merge(other: Statistics.default) + guard let dataToDump = try? JSONEncoder().encode(statisticsToDump) else { + log.message("Could not encode Statistics JSON", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + if !fileManager.fileExists(atPath: Constants.tempDirPath) { + try fileManager.createDirectory(atPath: Constants.tempDirPath, withIntermediateDirectories: true, attributes: nil) + } + + do { + try dataToDump.write(to: dumpFileUrl) + } catch { + log.message("Could not write Statistics JSON to \(dumpFileUrl.path). Error: \(error)", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + } + static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { if matchingExamples.isFilled { log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift index 195c8a0..ab2d28b 100644 --- a/Sources/AnyLint/Severity.swift +++ b/Sources/AnyLint/Severity.swift @@ -47,3 +47,5 @@ extension Severity: Comparable { lhs.rawValue < rhs.rawValue } } + +extension Severity: Codable {} diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index ad79441..ab8bbb4 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -2,7 +2,7 @@ import Foundation import Utility final class Statistics { - static let shared = Statistics() + static let `default`: Statistics = Statistics() var executedChecks: [CheckInfo] = [] var violationsPerCheck: [CheckInfo: [Violation]] = [:] @@ -13,16 +13,21 @@ final class Statistics { violationsBySeverity.keys.filter { !violationsBySeverity[$0]!.isEmpty }.max { $0.rawValue < $1.rawValue } } - private init() {} - - func checkedFiles(at filePaths: [String]) { + func checkedFiles(at filePaths: Set) { 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 found(violations: [CheckInfo: [Violation]]) { + for (checkInfo, checkViolations) in violations { + executedChecks.append(checkInfo) + violationsPerCheck[checkInfo] = checkViolations + violationsBySeverity[checkInfo.severity]!.append(contentsOf: checkViolations) + } + } + + func merge(other: Statistics) { + checkedFiles(at: other.filesChecked) + found(violations: other.violationsPerCheck) } /// Use for unit testing only. @@ -138,3 +143,10 @@ final class Statistics { } } } + +extension Statistics: Codable { + enum CodingKeys: String, CodingKey { + case violationsPerCheck + case filesChecked + } +} diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index 6eec576..2dfc3b5 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -41,3 +41,5 @@ public struct Violation { return "\(filePath.path(type: pathType)):\(locationInfo.line):\(locationInfo.charInLine):" } } + +extension Violation: Codable {} diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index 949b553..8503e6f 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -25,6 +25,11 @@ extension LintTask: TaskHandler { ValidateOrFail.swiftShInstalled() do { + if fileManager.fileExists(atPath: Constants.statisticsDumpFilePath) { + log.message("Removing statistics dump file from previous run ...", level: .info) + try fileManager.removeItem(atPath: Constants.statisticsDumpFilePath) + } + log.message("Start linting using config file at \(configFilePath) ...", level: .info) var command = "\(configFilePath.absolutePath) \(log.outputType.rawValue)" diff --git a/Sources/Utility/Constants.swift b/Sources/Utility/Constants.swift index df77c8b..14389b5 100644 --- a/Sources/Utility/Constants.swift +++ b/Sources/Utility/Constants.swift @@ -34,4 +34,10 @@ public enum Constants { /// 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 temporary directory to put files into when needed temporarily. + public static let tempDirPath: String = ".anylint" + + /// The file for dumping the statistics of a run. Useful to merge together sub-runs with parent. + public static let statisticsDumpFilePath: String = "\(tempDirPath)/statistics.json" } diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml new file mode 100644 index 0000000..0373cf4 --- /dev/null +++ b/Tests/.swiftlint.yml @@ -0,0 +1,2 @@ +disabled_rules: + - force_try diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift deleted file mode 100644 index 5be114c..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 f554ec5..97c8fad 100644 --- a/Tests/AnyLintTests/AutoCorrectionTests.swift +++ b/Tests/AnyLintTests/AutoCorrectionTests.swift @@ -14,8 +14,8 @@ final class AutoCorrectionTests: XCTestCase { singleLineAutoCorrection.appliedMessageLines, [ "Autocorrection applied, the diff is: (+ added, - removed)", - "- Lisence", - "+ License", + "- Lisence".red, + "+ License".green, ] ) @@ -27,10 +27,10 @@ final class AutoCorrectionTests: XCTestCase { multiLineAutoCorrection.appliedMessageLines, [ "Autocorrection applied, the diff is: (+ added, - removed)", - "- [L3] C", - "+ [L5] F1", - "- [L6] F", - "+ [L6] F2", + "- [L3] C".red, + "+ [L5] F1".green, + "- [L6] F".red, + "+ [L6] F2".green, ] ) } diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index f1eefa7..9aaf38a 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -24,7 +24,7 @@ final class FileContentsCheckerTests: XCTestCase { filePathsToCheck: filePathsToCheck, autoCorrectReplacement: nil, repeatIfAutoCorrected: false - ).performCheck() + ).performCheck()[checkInfo]! XCTAssertEqual(violations.count, 2) @@ -55,7 +55,7 @@ final class FileContentsCheckerTests: XCTestCase { filePathsToCheck: filePathsToCheck, autoCorrectReplacement: nil, repeatIfAutoCorrected: false - ).performCheck() + ).performCheck()[checkInfo]! XCTAssertEqual(violations.count, 2) @@ -87,7 +87,7 @@ final class FileContentsCheckerTests: XCTestCase { filePathsToCheck: filePathsToCheck, autoCorrectReplacement: nil, repeatIfAutoCorrected: false - ).performCheck() + ).performCheck()[checkInfo]! XCTAssertEqual(violations.count, 6) @@ -137,7 +137,7 @@ final class FileContentsCheckerTests: XCTestCase { filePathsToCheck: filePathsToCheck, autoCorrectReplacement: "$1 $2 = $3", repeatIfAutoCorrected: false - ).performCheck() + ).performCheck()[checkInfo]! XCTAssertEqual(violations.count, 2) @@ -167,7 +167,7 @@ final class FileContentsCheckerTests: XCTestCase { filePathsToCheck: filePathsToCheck, autoCorrectReplacement: "$1_$2", repeatIfAutoCorrected: true - ).performCheck() + ).performCheck()[checkInfo]! XCTAssertEqual(violations.count, 7) diff --git a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift index 0d59ca7..0d65d5e 100644 --- a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift @@ -15,12 +15,12 @@ final class FilePathsCheckerTests: XCTestCase { (subpath: "Sources/World.swift", contents: ""), ] ) { filePathsToCheck in - let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck()[sayHelloCheck()]! XCTAssertEqual(violations.count, 0) } withTemporaryFiles([(subpath: "Sources/World.swift", contents: "")]) { filePathsToCheck in - let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck()[sayHelloCheck()]! XCTAssertEqual(violations.count, 1) @@ -36,7 +36,7 @@ final class FilePathsCheckerTests: XCTestCase { (subpath: "Sources/World.swift", contents: ""), ] ) { filePathsToCheck in - let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() + let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck()[noWorldCheck()]! XCTAssertEqual(violations.count, 1) diff --git a/Tests/AnyLintTests/Checkers/TemplateCheckerTests.swift b/Tests/AnyLintTests/Checkers/TemplateCheckerTests.swift new file mode 100644 index 0000000..c5d7132 --- /dev/null +++ b/Tests/AnyLintTests/Checkers/TemplateCheckerTests.swift @@ -0,0 +1,91 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class TemplateCheckerTests: XCTestCase { + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testPerformWithLocalSource() { + withTemporaryFiles( + [ + ( + subpath: "AnyLint/Sample.swift", + contents: """ + #!/usr/local/bin/swift-sh + import AnyLint // @Flinesoft == wip/cg_template-system + + try Lint.reportResultsToFile(arguments: CommandLine.arguments) { + // MARK: PseudoCheck + try Lint.checkFilePaths( + checkInfo: "PseudoCheck: Checks if the file `Pseudo.md` exists.", + regex: #"^Pseudo\\.md$"#, + matchingExamples: ["Pseudo.md"], + nonMatchingExamples: ["Pseudo.markdown", "PSEUDO.md"], + violateIfNoMatchesFound: true + ) + } + + """ + ), + ] + ) { filePaths in + let violations = try! TemplateChecker(source: .local(filePaths[0]), runOnly: nil, exclude: nil, logDebugLevel: false).performCheck() + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "Running local config file at '\(filePaths[0])'") + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .info) + + let check: CheckInfo = "PseudoCheck: Checks if the file `Pseudo.md` exists." + XCTAssert(violations.keys.contains(check)) + XCTAssertEqual(violations[check]!.count, 1) + } + } + + func testPerformWithRemoteSource() { + let violations = try! TemplateChecker( + source: .remote("https://raw.githubusercontent.com/Flinesoft/AnyLint/wip/cg_template-system/Tests/Variants/sample.swift"), + runOnly: nil, + exclude: nil, + logDebugLevel: false + ).performCheck() + + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual( + TestHelper.shared.consoleOutputs[0].message, + "Running local config file at '\(Constants.tempDirPath)/Flinesoft_AnyLint_Variants_Sample.swift'" + ) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .info) + XCTAssert(FileManager.default.fileExists(atPath: "\(Constants.tempDirPath)/Flinesoft_AnyLint_Variants_Sample.swift")) + + let check: CheckInfo = "PseudoCheck: Checks if the file `Pseudo.md` exists." + XCTAssert(violations.keys.contains(check)) + XCTAssertEqual(violations[check]!.count, 1) + } + + func testPerformWithGithubSource() { + let violations = try! TemplateChecker( + source: .github( + repo: "Flinesoft/AnyLint", + version: "wip/cg_template-system", + variant: "Tests/Variants/sample" + ), + runOnly: nil, + exclude: nil, + logDebugLevel: false + ).performCheck() + + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual( + TestHelper.shared.consoleOutputs[0].message, + "Running local config file at '\(Constants.tempDirPath)/Flinesoft_AnyLint_Variants_Sample.swift'" + ) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .info) + XCTAssert(FileManager.default.fileExists(atPath: "\(Constants.tempDirPath)/Flinesoft_AnyLint_Variants_Sample.swift")) + + let check: CheckInfo = "PseudoCheck: Checks if the file `Pseudo.md` exists." + XCTAssert(violations.keys.contains(check)) + XCTAssertEqual(violations[check]!.count, 1) + } +} diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 6d63653..e8b1bcf 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -7,55 +7,56 @@ final class StatisticsTests: XCTestCase { override func setUp() { log = Logger(outputType: .test) TestHelper.shared.reset() - Statistics.shared.reset() + Statistics.default.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) + XCTAssert(Statistics.default.executedChecks.isEmpty) + XCTAssert(Statistics.default.violationsBySeverity[.info]!.isEmpty) + XCTAssert(Statistics.default.violationsBySeverity[.warning]!.isEmpty) + XCTAssert(Statistics.default.violationsBySeverity[.error]!.isEmpty) + XCTAssert(Statistics.default.violationsPerCheck.isEmpty) let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) - Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo1)], - in: checkInfo1 - ) + Statistics.default.found(violations: [checkInfo1: [Violation(checkInfo: 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) + XCTAssertEqual(Statistics.default.executedChecks, [checkInfo1]) + XCTAssertEqual(Statistics.default.violationsBySeverity[.info]!.count, 1) + XCTAssertEqual(Statistics.default.violationsBySeverity[.warning]!.count, 0) + XCTAssertEqual(Statistics.default.violationsBySeverity[.error]!.count, 0) + XCTAssertEqual(Statistics.default.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) + Statistics.default.found( + violations: [ + CheckInfo(id: "id2", hint: "hint2", severity: .warning): + [Violation(checkInfo: checkInfo2), Violation(checkInfo: checkInfo2)], + ] ) - 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) + XCTAssertEqual(Statistics.default.executedChecks, [checkInfo1, checkInfo2]) + XCTAssertEqual(Statistics.default.violationsBySeverity[.info]!.count, 1) + XCTAssertEqual(Statistics.default.violationsBySeverity[.warning]!.count, 2) + XCTAssertEqual(Statistics.default.violationsBySeverity[.error]!.count, 0) + XCTAssertEqual(Statistics.default.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) + Statistics.default.found( + violations: [ + CheckInfo(id: "id3", hint: "hint3", severity: .error): + [Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3)], + ] ) - 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) + XCTAssertEqual(Statistics.default.executedChecks, [checkInfo1, checkInfo2, checkInfo3]) + XCTAssertEqual(Statistics.default.violationsBySeverity[.info]!.count, 1) + XCTAssertEqual(Statistics.default.violationsBySeverity[.warning]!.count, 2) + XCTAssertEqual(Statistics.default.violationsBySeverity[.error]!.count, 3) + XCTAssertEqual(Statistics.default.violationsPerCheck.keys.count, 3) } func testLogSummary() { // swiftlint:disable:this function_body_length - Statistics.shared.logCheckSummary() + Statistics.default.logCheckSummary() XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "No checks found to perform.") @@ -63,35 +64,48 @@ final class StatisticsTests: XCTestCase { TestHelper.shared.reset() let checkInfo1 = CheckInfo(id: "id1", hint: "hint1", severity: .info) - Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo1)], - in: checkInfo1 - ) + Statistics.default.found(violations: [checkInfo1: [Violation(checkInfo: checkInfo1)]]) let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) - Statistics.shared.found( + Statistics.default.found( violations: [ - Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Harry.swift"), - Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Albus.swift"), - ], - in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) + CheckInfo(id: "id2", hint: "hint2", severity: .warning): + [ + Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Harry.swift"), + Violation(checkInfo: checkInfo2, filePath: "Hogwarts/Albus.swift"), + ], + ] ) let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) - Statistics.shared.found( + Statistics.default.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) + CheckInfo(id: "id3", hint: "hint3", severity: .error): + [ + Violation( + checkInfo: checkInfo3, + filePath: "Hogwarts/Harry.swift", + locationInfo: String.LocationInfo(line: 10, charInLine: 30) + ), + Violation( + checkInfo: checkInfo3, + filePath: "Hogwarts/Harry.swift", + locationInfo: String.LocationInfo(line: 72, charInLine: 17) + ), + Violation( + checkInfo: checkInfo3, + filePath: "Hogwarts/Albus.swift", + locationInfo: String.LocationInfo(line: 40, charInLine: 4) + ), + ], + ] ) - 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.default.checkedFiles(at: ["Hogwarts/Harry.swift"]) + Statistics.default.checkedFiles(at: ["Hogwarts/Harry.swift", "Hogwarts/Albus.swift"]) + Statistics.default.checkedFiles(at: ["Hogwarts/Albus.swift"]) - Statistics.shared.logCheckSummary() + Statistics.default.logCheckSummary() XCTAssertEqual( TestHelper.shared.consoleOutputs.map { $0.level }, diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift index c9d0ebd..87a9c2b 100644 --- a/Tests/AnyLintTests/ViolationTests.swift +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -7,7 +7,7 @@ final class ViolationTests: XCTestCase { override func setUp() { log = Logger(outputType: .test) TestHelper.shared.reset() - Statistics.shared.reset() + Statistics.default.reset() } func testLocationMessage() { diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index ae7fdcc..5b64f15 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -72,6 +72,14 @@ extension StatisticsTests { ] } +extension TemplateCheckerTests { + static var allTests: [(String, (TemplateCheckerTests) -> () throws -> Void)] = [ + ("testPerformWithLocalSource", testPerformWithLocalSource), + ("testPerformWithRemoteSource", testPerformWithRemoteSource), + ("testPerformWithGithubSource", testPerformWithGithubSource) + ] +} + extension ViolationTests { static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ ("testLocationMessage", testLocationMessage) @@ -88,5 +96,6 @@ XCTMain([ testCase(LintTests.allTests), testCase(RegexExtTests.allTests), testCase(StatisticsTests.allTests), + testCase(TemplateCheckerTests.allTests), testCase(ViolationTests.allTests) ]) diff --git a/Tests/Variants/Sample.swift b/Tests/Variants/Sample.swift new file mode 100644 index 0000000..741e805 --- /dev/null +++ b/Tests/Variants/Sample.swift @@ -0,0 +1,13 @@ +#!/usr/local/bin/swift-sh +import AnyLint // . + +try Lint.reportResultsToFile(arguments: CommandLine.arguments) { + // MARK: PseudoCheck + try Lint.checkFilePaths( + checkInfo: "PseudoCheck: Checks if the file `Pseudo.md` exists.", + regex: #"^Pseudo\.md$"#, + matchingExamples: ["Pseudo.md"], + nonMatchingExamples: ["Pseudo.markdown", "PSEUDO.md"], + violateIfNoMatchesFound: true + ) +}