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