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"/>
-
+
= 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"],
+ ]
+ )
}