diff --git a/CHANGELOG.md b/CHANGELOG.md index 56babd9..d4a009f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,7 +109,7 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ### Added - Made `AutoCorrection` expressible by Dictionary literals and updated the `README.md` accordingly. Issue: [#5](https://github.com/Flinesoft/AnyLint/issues/5) | PR: [#11](https://github.com/Flinesoft/AnyLint/pull/11) | Author: [Cihat Gündüz](https://github.com/Jeehut) -- Added option to skip checks within file contents by specifying `AnyLint.skipHere: ` or `AnyLint.skipInFile: `. Checkout the [Skip file content checks](https://github.com/Flinesoft/AnyLint#skip-file-content-checks) README section for more info. +- Added option to skip checks within file contents by specifying `AnyLint.skipHere: ` or `AnyLint.skipInFile: `. Checkout the [Skip file content checks](https://github.com/Flinesoft/AnyLint#skip-file-content-checks) README section for more info. Issue: [#9](https://github.com/Flinesoft/AnyLint/issues/9) | PR: [#12](https://github.com/Flinesoft/AnyLint/pull/12) | Author: [Cihat Gündüz](https://github.com/Jeehut) ## [0.2.0] - 2020-04-10 @@ -122,7 +122,7 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se - Added two simple lint check examples in first code sample in README. (Thanks for the pointer, [Dave Verwer](https://github.com/daveverwer)!) Author: [Cihat Gündüz](https://github.com/Jeehut) ### Changed -- Changed `CheckInfo` id casing convention from snake_case to UpperCamelCase in `blank` template. +- Changed `Check` id casing convention from snake_case to UpperCamelCase in `blank` template. Author: [Cihat Gündüz](https://github.com/Jeehut) ## [0.1.0] - 2020-03-22 diff --git a/README.md b/README.md index 114f898..ebc1402 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,9 @@ The `.anchorsMatchLines` option is always activated on literal usage as we stron -#### CheckInfo +#### Check -A `CheckInfo` contains the basic information about a lint check. It consists of: +A `Check` contains the basic information about a lint check. It consists of: 1. `id`: The identifier of your lint check. For example: `EmptyTodo` 2. `hint`: The hint explaining the cause of the violation or the steps to fix it. @@ -217,8 +217,8 @@ While there is an initializer available, we recommend using a String Literal ins ```swift // accepted structure: (@): -let checkInfo: CheckInfo = "ReadmePath: The README file should be named exactly `README.md`." -let checkInfoCustomSeverity: CheckInfo = "ReadmePath@warning: The README file should be named exactly `README.md`." +let check: Check = "ReadmePath: The README file should be named exactly `README.md`." +let checkCustomSeverity: Check = "ReadmePath@warning: The README file should be named exactly `README.md`." ``` #### AutoCorrection @@ -241,12 +241,12 @@ let example: AutoCorrection = ["before": "Lisence", "after": "License"] AnyLint has rich support for checking the contents of a file using a regex. The design follows the approach "make simple things simple and hard things possible". Thus, let's explain the `checkFileContents` method with a simple and a complex example. -In its simplest form, the method just requires a `checkInfo` and a `regex`: +In its simplest form, the method just requires a `check` and a `regex`: ```swift // MARK: EmptyTodo try Lint.checkFileContents( - checkInfo: "EmptyTodo: TODO comments should not be empty.", + check: "EmptyTodo: TODO comments should not be empty.", regex: #"// TODO: *\n"# ) ``` @@ -271,7 +271,7 @@ let swiftTestFiles: Regex = #"Tests/.*\.swift"# // MARK: - Checks // MARK: empty_todo try Lint.checkFileContents( - checkInfo: "EmptyTodo: TODO comments should not be empty.", + check: "EmptyTodo: TODO comments should not be empty.", regex: #"// TODO: *\n"#, matchingExamples: ["// TODO:\n"], nonMatchingExamples: ["// TODO: not yet implemented\n"], @@ -302,7 +302,7 @@ let swiftTestFiles: Regex = #"Tests/.*\.swift"# // MARK: - Checks // MARK: empty_method_body try Lint.checkFileContents( - checkInfo: "EmptyMethodBody: Don't use whitespaces for the body of empty methods.", + check: "EmptyMethodBody: Don't use whitespaces for the body of empty methods.", regex: [ "declaration": #"func [^\(\s]+\([^{]*\)"#, "spacing": #"\s*"#, @@ -347,7 +347,7 @@ While the `includeFilters` and `excludeFilters` arguments in the config file can For such cases, there are **2 ways to skip checks** within the files themselves: -1. `AnyLint.skipHere: `: Will skip the specified check(s) on the same line and the next line. +1. `AnyLint.skipHere: `: Will skip the specified check(s) on the same line and the next line. ```swift var x: Int = 5 // AnyLint.skipHere: MinVarNameLength @@ -358,7 +358,7 @@ For such cases, there are **2 ways to skip checks** within the files themselves: var x: Int = 5 ``` -2. `AnyLint.skipInFile: `: Will skip `All` or specificed check(s) in the entire file. +2. `AnyLint.skipInFile: `: Will skip `All` or specificed check(s) in the entire file. ```swift // AnyLint.skipInFile: MinVarNameLength @@ -398,10 +398,10 @@ TODO: Update to new custom script format supporting all languages as long as the AnyLint allows you to do any kind of lint checks (thus its name) as it gives you the full power of the Swift programming language and it's packages [ecosystem](https://swiftpm.co/). The `customCheck` method needs to be used to profit from this flexibility. And it's actually the simplest of the three methods, consisting of only two parameters: -1. `checkInfo`: Provides some general information on the lint check. +1. `check`: Provides some general information on the lint check. 2. `customClosure`: Your custom logic which produces an array of `Violation` objects. -Note that the `Violation` type just holds some additional information on the file, matched string, location in the file and applied autocorrection and that all these fields are optional. It is a simple struct used by the AnyLint reporter for more detailed output, no logic attached. The only required field is the `CheckInfo` object which caused the violation. +Note that the `Violation` type just holds some additional information on the file, matched string, location in the file and applied autocorrection and that all these fields are optional. It is a simple struct used by the AnyLint reporter for more detailed output, no logic attached. The only required field is the `Check` object which caused the violation. If you want to use regexes in your custom code, you can learn more about how you can match strings with a `Regex` object on [the HandySwift docs](https://github.com/Flinesoft/HandySwift#regex) (the project, the class was taken from) or read the [code documentation comments](https://github.com/Flinesoft/AnyLint/blob/main/Sources/Utility/Regex.swift). @@ -418,7 +418,7 @@ Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: - Checks // MARK: LinuxMainUpToDate - try Lint.customCheck(checkInfo: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { checkInfo in + try Lint.customCheck(check: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { check in var violations: [Violation] = [] let linuxMainFilePath = "Tests/LinuxMain.swift" @@ -436,7 +436,7 @@ Lint.logSummaryAndExit(arguments: CommandLine.arguments) { if linuxMainContentsBeforeRegeneration != linuxMainContentsAfterRegeneration { violations.append( Violation( - checkInfo: checkInfo, + check: check, filePath: linuxMainFilePath, appliedAutoCorrection: AutoCorrection( before: linuxMainContentsBeforeRegeneration, diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift index 98c2f66..bff6037 100644 --- a/Sources/Checkers/FileContentsChecker.swift +++ b/Sources/Checkers/FileContentsChecker.swift @@ -36,7 +36,7 @@ extension FileContentsChecker: Checker { var newFileContents: String = fileContents let linesInFile: [String] = fileContents.components(separatedBy: .newlines) - // skip check in file if contains `AnyLint.skipInFile: ` + // skip check in file if contains `AnyLint.skipInFile: ` let skipInFileRegex = try Regex(#"AnyLint\.skipInFile:[^\n]*([, ]All[,\s]|[, ]\#(id)[,\s])"#) guard !skipInFileRegex.matches(fileContents) else { continue } @@ -45,7 +45,7 @@ extension FileContentsChecker: Checker { for match in regex.matches(in: fileContents).reversed() { let location = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath) - // skip found match if contains `AnyLint.skipHere: ` in same line or one line before + // skip found match if contains `AnyLint.skipHere: ` in same line or one line before guard !linesInFile.containsLine(at: [location.row! - 2, location.row! - 1], matchingRegex: skipHereRegex) else { continue } diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index 612e023..d26739c 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -9,7 +9,7 @@ public enum Lint { /// Checks the contents of files. /// /// - Parameters: - /// - checkInfo: The info object providing some general information on the lint check. + /// - check: 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'. /// - 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. @@ -19,7 +19,7 @@ public enum Lint { /// - 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, + check: Check, regex: Regex, matchingExamples: [String] = [], nonMatchingExamples: [String] = [], @@ -29,11 +29,11 @@ public enum Lint { autoCorrectExamples: [AutoCorrection] = [], repeatIfAutoCorrected: Bool = false ) throws -> [Violation] { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + validate(regex: regex, matchesForEach: matchingExamples, check: check) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, check: check) validateParameterCombinations( - checkInfo: checkInfo, + check: check, autoCorrectReplacement: autoCorrectReplacement, autoCorrectExamples: autoCorrectExamples, violateIfNoMatchesFound: nil @@ -41,7 +41,7 @@ public enum Lint { if let autoCorrectReplacement = autoCorrectReplacement { validateAutocorrectsAll( - checkInfo: checkInfo, + check: check, examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement @@ -55,9 +55,9 @@ public enum Lint { ) let violations = try FileContentsChecker( - id: checkInfo.id, - hint: checkInfo.hint, - severity: checkInfo.severity, + id: check.id, + hint: check.hint, + severity: check.severity, regex: regex, filePathsToCheck: filePathsToCheck, autoCorrectReplacement: autoCorrectReplacement, @@ -71,7 +71,7 @@ public enum Lint { /// Checks the names of files. /// /// - Parameters: - /// - checkInfo: The info object providing some general information on the lint check. + /// - check: 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. @@ -81,7 +81,7 @@ public enum Lint { /// - 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, + check: Check, regex: Regex, matchingExamples: [String] = [], nonMatchingExamples: [String] = [], @@ -91,10 +91,10 @@ public enum Lint { autoCorrectExamples: [AutoCorrection] = [], violateIfNoMatchesFound: Bool = false ) throws -> [Violation] { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + validate(regex: regex, matchesForEach: matchingExamples, check: check) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, check: check) validateParameterCombinations( - checkInfo: checkInfo, + check: check, autoCorrectReplacement: autoCorrectReplacement, autoCorrectExamples: autoCorrectExamples, violateIfNoMatchesFound: violateIfNoMatchesFound @@ -102,7 +102,7 @@ public enum Lint { if let autoCorrectReplacement = autoCorrectReplacement { validateAutocorrectsAll( - checkInfo: checkInfo, + check: check, examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement @@ -116,9 +116,9 @@ public enum Lint { ) let violations = try FilePathsChecker( - id: checkInfo.id, - hint: checkInfo.hint, - severity: checkInfo.severity, + id: check.id, + hint: check.hint, + severity: check.severity, regex: regex, filePathsToCheck: filePathsToCheck, autoCorrectReplacement: autoCorrectReplacement, @@ -132,8 +132,8 @@ public enum Lint { /// Run custom scripts as checks. /// /// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. If the output iis an array of ``Violation`` instances, they will be wrapped in a ``LintResults`` object. Else, it will report exactly one violation if the command has a non-zero exit code with the last line(s) of output. - public static func runCustomScript(checkInfo: CheckInfo, command: String) throws -> LintResults { - let tempScriptFileUrl = URL(fileURLWithPath: "_\(checkInfo.id).tempscript") + public static func runCustomScript(check: Check, command: String) throws -> LintResults { + let tempScriptFileUrl = URL(fileURLWithPath: "_\(check.id).tempscript") try command.write(to: tempScriptFileUrl, atomically: true, encoding: .utf8) let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) @@ -147,18 +147,18 @@ public enum Lint { let jsonData = jsonString.data(using: .utf8), let violations: [Violation] = try? JSONDecoder.iso.decode([Violation].self, from: jsonData) { - return [checkInfo.severity: [checkInfo: violations]] + return [check.severity: [check: violations]] } else { - return [checkInfo.severity: [checkInfo: [Violation()]]] + return [check.severity: [check: [Violation()]]] } } - static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { + static func validate(regex: Regex, matchesForEach matchingExamples: [String], check: Check) { 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)", + "Couldn't find a match for regex \(regex) in check '\(check.id)' within matching example:\n\(example)", level: .error ) log.exit(fail: true) @@ -166,11 +166,11 @@ public enum Lint { } } - static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { + static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], check: Check) { 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)", + "Unexpectedly found a match for regex \(regex) in check '\(check.id)' within non-matching example:\n\(example)", level: .error ) log.exit(fail: true) @@ -179,7 +179,7 @@ public enum Lint { } static func validateAutocorrectsAll( - checkInfo: CheckInfo, + check: Check, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String @@ -189,7 +189,7 @@ public enum Lint { if autocorrected != autocorrect.after { log.message( """ - Autocorrecting example for \(checkInfo.id) did not result in expected output. + Autocorrecting example for \(check.id) did not result in expected output. Before: '\(autocorrect.before.showWhitespacesAndNewlines())' After: '\(autocorrected.showWhitespacesAndNewlines())' Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' @@ -202,21 +202,21 @@ public enum Lint { } static func validateParameterCombinations( - checkInfo: CheckInfo, + check: Check, autoCorrectReplacement: String?, autoCorrectExamples: [AutoCorrection], violateIfNoMatchesFound: Bool? ) { if autoCorrectExamples.isFilled && autoCorrectReplacement == nil { log.message( - "`autoCorrectExamples` provided for check \(checkInfo.id) without specifying an `autoCorrectReplacement`.", + "`autoCorrectExamples` provided for check \(check.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.", + "Incompatible options specified for check \(check.id): `autoCorrectReplacement` and `violateIfNoMatchesFound` can't be used together.", level: .error ) log.exit(fail: true) diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index 2b063d8..81bad83 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -70,7 +70,7 @@ struct LintCommand: ParsableCommand { // run `FileContents` checks for fileContentsConfig in lintConfig.fileContents { let violations = try Lint.checkFileContents( - checkInfo: fileContentsConfig.checkInfo, + check: fileContentsConfig.check, regex: fileContentsConfig.regex, matchingExamples: fileContentsConfig.matchingExamples, nonMatchingExamples: fileContentsConfig.nonMatchingExamples, @@ -81,13 +81,13 @@ struct LintCommand: ParsableCommand { repeatIfAutoCorrected: fileContentsConfig.repeatIfAutoCorrected ) - lintResults.appendViolations(violations, forCheck: fileContentsConfig.checkInfo) + lintResults.appendViolations(violations, forCheck: fileContentsConfig.check) } // run `FilePaths` checks for filePathsConfig in lintConfig.filePaths { let violations = try Lint.checkFilePaths( - checkInfo: filePathsConfig.checkInfo, + check: filePathsConfig.check, regex: filePathsConfig.regex, matchingExamples: filePathsConfig.matchingExamples, nonMatchingExamples: filePathsConfig.nonMatchingExamples, @@ -98,13 +98,13 @@ struct LintCommand: ParsableCommand { violateIfNoMatchesFound: filePathsConfig.violateIfNoMatchesFound ) - lintResults.appendViolations(violations, forCheck: filePathsConfig.checkInfo) + lintResults.appendViolations(violations, forCheck: filePathsConfig.check) } // run `CustomScripts` checks for customScriptConfig in lintConfig.customScripts { let customScriptLintResults = try Lint.runCustomScript( - checkInfo: customScriptConfig.checkInfo, + check: customScriptConfig.check, command: customScriptConfig.command ) @@ -133,7 +133,7 @@ extension Severity: ExpressibleByArgument {} extension OutputFormat: ExpressibleByArgument {} extension CheckConfiguration { - var checkInfo: CheckInfo { + var check: Check { .init(id: id, hint: hint, severity: severity) } } diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/Check.swift similarity index 88% rename from Sources/Core/CheckInfo.swift rename to Sources/Core/Check.swift index 7653e35..000baa0 100644 --- a/Sources/Core/CheckInfo.swift +++ b/Sources/Core/Check.swift @@ -1,7 +1,7 @@ import Foundation /// Provides some basic information needed in each lint check. -public struct CheckInfo { +public struct Check { /// The identifier of the check defined here. Can be used when defining exceptions within files for specific lint checks. public let id: String @@ -23,13 +23,13 @@ public struct CheckInfo { } } -extension CheckInfo: Hashable { +extension Check: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(id) } } -extension CheckInfo: Codable { +extension Check: Codable { public init( from decoder: Decoder ) throws { @@ -44,7 +44,7 @@ extension CheckInfo: Codable { } } -extension CheckInfo: RawRepresentable { +extension Check: RawRepresentable { public var rawValue: String { "\(id)@\(severity.rawValue): \(hint)" } @@ -74,7 +74,7 @@ extension CheckInfo: RawRepresentable { guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawValue) else { log.message( - "Could not convert String literal '\(rawValue)' to type CheckInfo. Please check the structure to be: (@): ", + "Could not convert String literal '\(rawValue)' to type Check. Please check the structure to be: (@): ", level: .error ) log.exit(fail: true) @@ -87,3 +87,9 @@ extension CheckInfo: RawRepresentable { } } } + +extension Check: Comparable { + public static func < (lhs: Check, rhs: Check) -> Bool { + lhs.id < rhs.id + } +} diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index 6f99736..a932c99 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -2,6 +2,9 @@ import Foundation /// A violation found in a check. public struct Violation: Codable, Equatable { + /// The exact time this violation was discovered. Needed for sorting purposes. + public let discoverDate: Date + /// The matched string that violates the check. public let matchedString: String? @@ -13,12 +16,20 @@ public struct Violation: Codable, Equatable { /// Initializes a violation object. public init( + discoverDate: Date = Date(), matchedString: String? = nil, location: Location? = nil, appliedAutoCorrection: AutoCorrection? = nil ) { + self.discoverDate = discoverDate self.matchedString = matchedString self.location = location self.appliedAutoCorrection = appliedAutoCorrection } } + +extension Violation: Comparable { + public static func < (lhs: Violation, rhs: Violation) -> Bool { + lhs.discoverDate < rhs.discoverDate + } +} diff --git a/Sources/Reporting/CodableLintResults.swift b/Sources/Reporting/CodableLintResults.swift new file mode 100644 index 0000000..2b37851 --- /dev/null +++ b/Sources/Reporting/CodableLintResults.swift @@ -0,0 +1,28 @@ +//import Foundation +//import OrderedCollections +//import Core +// +///// A wraper for ``LintResults`` due to a Bug in Swift. (see https://bugs.swift.org/browse/SR-7788) +//public typealias CodableLintResults = CodableOrderedDictionary> +// +//extension CodableLintResults { +// init(lintResults: LintResults) { +// var newCodableSeverityDict: CodableLintResults = .init() +// +// for (severity, checkDict) in lintResults { +// var newCodableCheckDict: CodableOrderedDictionary = .init() +// +// for (check, violations) in checkDict { +// newCodableCheckDict.wrappedValue[check] = violations +// } +// +// newCodableSeverityDict.wrappedValue[severity] = newCodableCheckDict +// } +// +// self = newCodableSeverityDict +// } +// +// var lintResults: LintResults { +// wrappedValue.mapValues { $0.wrappedValue } +// } +//} diff --git a/Sources/Reporting/CodableOrderedDictionary.swift b/Sources/Reporting/CodableOrderedDictionary.swift new file mode 100644 index 0000000..87f130a --- /dev/null +++ b/Sources/Reporting/CodableOrderedDictionary.swift @@ -0,0 +1,45 @@ +import Foundation +import OrderedCollections + +/// Workaround for a Bug in Swift to encode/decode keys properly. See https://bugs.swift.org/browse/SR-7788. +/// Inspired by: https://www.fivestars.blog/articles/codable-swift-dictionaries/ +@propertyWrapper +public struct CodableOrderedDictionary: Codable +where Key.RawValue: Codable & Hashable { + public var wrappedValue: OrderedDictionary + + public init() { + wrappedValue = [:] + } + + public init( + wrappedValue: OrderedDictionary + ) { + self.wrappedValue = wrappedValue + } + + public init( + from decoder: Decoder + ) throws { + let container = try decoder.singleValueContainer() + let rawKeyedDict = try container.decode([Key.RawValue: Value].self) + + wrappedValue = [:] + for (rawKey, value) in rawKeyedDict { + guard let key = Key(rawValue: rawKey) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: + "Invalid key: cannot initialize '\(Key.self)' from invalid '\(Key.RawValue.self)' value '\(rawKey)'" + ) + } + wrappedValue[key] = value + } + } + + public func encode(to encoder: Encoder) throws { + let rawKeyedDictionary = OrderedDictionary(uniqueKeysWithValues: wrappedValue.map { ($0.rawValue, $1) }) + var container = encoder.singleValueContainer() + try container.encode(rawKeyedDictionary) + } +} diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index bbfa606..da42f2d 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -2,25 +2,30 @@ import Foundation import Core import OrderedCollections -/// The linting output type. Can be merged from multiple -public typealias LintResults = OrderedDictionary> +/// The linting output type. Can be merged from multiple instances into one. +public struct LintResults { + /// The checks and their validations accessible by severity level. + public var checkViolationsBySeverity: Dictionary> + + public init() { + self.checkViolationsBySeverity = [:] + } -extension LintResults { /// Returns a list of all executed checks. - public var allExecutedChecks: [CheckInfo] { - values.reduce(into: []) { $0.append(contentsOf: $1.keys) } + public var allExecutedChecks: [Check] { + checkViolationsBySeverity.values.reduce(into: []) { $0.append(contentsOf: $1.keys) }.sorted() } /// Returns a list of all found violations. public var allFoundViolations: [Violation] { - values.reduce(into: []) { $0.append(contentsOf: $1.values.flatMap { $0 }) } + checkViolationsBySeverity.values.reduce(into: []) { $0.append(contentsOf: $1.values.flatMap { $0 }) }.sorted() } /// The highest severity with at least one violation. func maxViolationSeverity(excludeAutocorrected: Bool) -> Severity? { for severity in Severity.allCases.sorted().reversed() { - if let severityViolations = self[severity], - severityViolations.values.elements.contains(where: { !$0.isEmpty }) + if let severityViolations = checkViolationsBySeverity[severity], + severityViolations.values.contains(where: { !$0.isEmpty }) { return severity } @@ -31,7 +36,7 @@ extension LintResults { /// Merges the given lint results into this one. public mutating func mergeResults(_ other: LintResults) { - merge(other) { currentDict, newDict in + checkViolationsBySeverity.merge(other.checkViolationsBySeverity) { currentDict, newDict in currentDict.merging(newDict) { currentViolations, newViolations in currentViolations + newViolations } @@ -39,13 +44,13 @@ extension LintResults { } /// Appends the violations for the provided check to the results. - public mutating func appendViolations(_ violations: [Violation], forCheck checkInfo: CheckInfo) { + public mutating func appendViolations(_ violations: [Violation], forCheck check: Check) { assert( - keys.contains(checkInfo.severity), - "Trying to add violations for severity \(checkInfo.severity) to LintResults without having initialized the severity key." + checkViolationsBySeverity.keys.contains(check.severity), + "Trying to add violations for severity \(check.severity) to LintResults without having initialized the severity key." ) - self[checkInfo.severity]![checkInfo] = violations + checkViolationsBySeverity[check.severity]![check] = violations } /// Logs the summary of the violations in the specified output format. @@ -55,7 +60,7 @@ extension LintResults { if executedChecks.isEmpty { log.message("No checks found to perform.", level: .warning) } - else if values.contains(where: { $0.values.isFilled }) { + else if checkViolationsBySeverity.values.contains(where: { $0.values.isFilled }) { switch outputFormat { case .commandLine: reportToConsole() @@ -82,7 +87,7 @@ extension LintResults { /// - excludeAutocorrected: If `true`, autocorrected violations will not be returned, else returns all violations of the given severity level. /// - Returns: The violations for a specific severity level. public func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { - guard let violations = self[severity]?.values.elements.flatMap({ $0 }) else { return [] } + guard let violations = checkViolationsBySeverity[severity]?.values.flatMap({ $0 }) else { return [] } guard excludeAutocorrected else { return violations } return violations.filter { $0.appliedAutoCorrection == nil } } @@ -90,11 +95,11 @@ extension LintResults { /// Used to get validations for a specific check. /// /// - Parameters: - /// - check: The `CheckInfo` object to filter by. + /// - check: The `Check` object to filter by. /// - excludeAutocorrected: If `true`, autocorrected violations will not be returned, else returns all violations of the given severity level. /// - Returns: The violations for a specific check. - public func violations(check: CheckInfo, excludeAutocorrected: Bool) -> [Violation] { - guard let violations: [Violation] = self[check.severity]?[check] else { return [] } + public func violations(check: Check, excludeAutocorrected: Bool) -> [Violation] { + guard let violations: [Violation] = checkViolationsBySeverity[check.severity]?[check] else { return [] } guard excludeAutocorrected else { return violations } return violations.filter { $0.appliedAutoCorrection == nil } } @@ -165,13 +170,13 @@ extension LintResults { } func reportToXcode() { - for severity in keys.sorted().reversed() { - guard let checkResultsAtSeverity = self[severity] else { continue } + for severity in checkViolationsBySeverity.keys.sorted().reversed() { + guard let checkResultsAtSeverity = checkViolationsBySeverity[severity] else { continue } - for (checkInfo, violations) in checkResultsAtSeverity { + for (check, violations) in checkResultsAtSeverity { for violation in violations where violation.appliedAutoCorrection == nil { log.message( - "[\(checkInfo.id)] \(checkInfo.hint)", + "[\(check.id)] \(check.hint)", level: severity.logLevel, location: violation.location ) @@ -196,3 +201,70 @@ extension LintResults { } } } + +enum LintResultsDecodingError: Error { + case unknownSeverityRawValue(String) + case unknownCheckRawValue(String) +} + +/// Custom ``Codable`` implementation due to a Swift bug with custom key types: https://bugs.swift.org/browse/SR-7788 +extension LintResults: Codable { + public init( + from decoder: Decoder + ) throws { + let rawKeyedDictionary: [String: [String: [Violation]]] = try .init(from: decoder) + + self.checkViolationsBySeverity = [:] + + for (rawSeverity, checkRawValueViolationsDict) in rawKeyedDictionary { + guard let severity = Severity(rawValue: rawSeverity) else { + throw LintResultsDecodingError.unknownSeverityRawValue(rawSeverity) + } + + var checkViolationsDict: [Check: [Violation]] = .init() + + for (checkRawValue, violations) in checkRawValueViolationsDict { + guard let check = Check(rawValue: checkRawValue) else { + throw LintResultsDecodingError.unknownCheckRawValue(checkRawValue) + } + + checkViolationsDict[check] = violations + } + + self.checkViolationsBySeverity[severity] = checkViolationsDict + } + } + + public func encode(to encoder: Encoder) throws { + var rawKeyedOuterDict: Dictionary> = .init() + + for (severity, checkViolationsDict) in checkViolationsBySeverity { + var rawKeyedInnerDict: Dictionary = .init() + + for (check, violations) in checkViolationsDict { + rawKeyedInnerDict[check.rawValue] = violations + } + + rawKeyedOuterDict[severity.rawValue] = rawKeyedInnerDict + } + + var container = encoder.singleValueContainer() + try container.encode(rawKeyedOuterDict) + } +} + +extension LintResults: ExpressibleByDictionaryLiteral { + public init( + dictionaryLiteral elements: (Severity, Dictionary)... + ) { + var newDict: Dictionary> = .init() + + for (key, value) in elements { + newDict[key] = value + } + + self.checkViolationsBySeverity = newDict + } +} + +extension LintResults: Equatable {} diff --git a/Sources/TestSupport/Extensions/DateExt.swift b/Sources/TestSupport/Extensions/DateExt.swift new file mode 100644 index 0000000..7c080b1 --- /dev/null +++ b/Sources/TestSupport/Extensions/DateExt.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Date { + /// Returns a sample Date for testing purposes. Use the same seed to get the same date. + public static func sample(seed: Int) -> Date { + Date(timeIntervalSinceReferenceDate: Double(seed) * 60 * 60) + } +} diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index 2b874ad..d4e043a 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -17,12 +17,12 @@ final class LintTests: XCTestCase { XCTAssertNil(testLogger.exitStatusCode) let regex = try! Regex(#"foo[0-9]?bar"#) - let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) + let check = Check(id: "foo_bar", hint: "do bar", severity: .warning) Lint.validate( regex: regex, matchesForEach: ["foo1bar", "foobar", "myfoo4barbeque"], - checkInfo: checkInfo + check: check ) XCTAssertNil(testLogger.exitStatusCode) @@ -30,7 +30,7 @@ final class LintTests: XCTestCase { // Lint.validate( // regex: regex, // matchesForEach: ["foo1bar", "FooBar", "myfoo4barbeque"], - // checkInfo: checkInfo + // check: check // ) // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) } @@ -39,12 +39,12 @@ final class LintTests: XCTestCase { XCTAssertNil(testLogger.exitStatusCode) let regex = try! Regex(#"foo[0-9]?bar"#) - let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) + let check = Check(id: "foo_bar", hint: "do bar", severity: .warning) Lint.validate( regex: regex, doesNotMatchAny: ["fooLbar", "FooBar", "myfoo40barbeque"], - checkInfo: checkInfo + check: check ) XCTAssertNil(testLogger.exitStatusCode) @@ -52,7 +52,7 @@ final class LintTests: XCTestCase { // Lint.validate( // regex: regex, // doesNotMatchAny: ["fooLbar", "foobar", "myfoo40barbeque"], - // checkInfo: checkInfo + // check: check // ) // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) } @@ -63,7 +63,7 @@ final class LintTests: XCTestCase { let anonymousCaptureRegex = try? Regex(#"([^\.]+)(\.)([^\.]+)(\.)([^\.]+)"#) Lint.validateAutocorrectsAll( - checkInfo: CheckInfo(id: "id", hint: "hint"), + check: Check(id: "id", hint: "hint"), examples: [ AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), @@ -76,7 +76,7 @@ final class LintTests: XCTestCase { // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` // Lint.validateAutocorrectsAll( - // checkInfo: CheckInfo(id: "id", hint: "hint"), + // check: Check(id: "id", hint: "hint"), // examples: [ // AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), // AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), @@ -94,7 +94,7 @@ final class LintTests: XCTestCase { let namedCaptureRegex = try! Regex(#"([^\.]+)\.([^\.]+)\.([^\.]+)"#) Lint.validateAutocorrectsAll( - checkInfo: CheckInfo(id: "id", hint: "hint"), + check: Check(id: "id", hint: "hint"), examples: [ AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), @@ -107,7 +107,7 @@ final class LintTests: XCTestCase { // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` // Lint.validateAutocorrectsAll( - // checkInfo: CheckInfo(id: "id", hint: "hint"), + // check: Check(id: "id", hint: "hint"), // examples: [ // AutoCorrection(before: "prefix.content.suffix", after: "suffix.content.prefix"), // AutoCorrection(before: "forums.swift.org", after: "org.swift.forums"), @@ -121,7 +121,7 @@ final class LintTests: XCTestCase { func testRunCustomScript() throws { var lintResults: LintResults = try Lint.runCustomScript( - checkInfo: .init(id: "1", hint: "hint #1"), + check: .init(id: "1", hint: "hint #1"), command: """ if which echo > /dev/null; then echo 'Executed custom checks with following result: @@ -154,15 +154,15 @@ final class LintTests: XCTestCase { XCTAssertEqual(lintResults.allFoundViolations.dropFirst().first?.location?.filePath, "/some/path") XCTAssertEqual(lintResults.allFoundViolations.dropFirst().first?.location?.row, 5) XCTAssertEqual(lintResults.allFoundViolations.dropFirst().first?.appliedAutoCorrection?.after, "A") - XCTAssertNil(lintResults[.error]?.keys.first) - XCTAssertEqual(lintResults[.info]?.keys.first?.id, "B") + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "B") } func testValidateParameterCombinations() { XCTAssertNoDifference(testLogger.loggedMessages, []) Lint.validateParameterCombinations( - checkInfo: .init(id: "1", hint: "hint #1"), + check: .init(id: "1", hint: "hint #1"), autoCorrectReplacement: nil, autoCorrectExamples: [.init(before: "abc", after: "cba")], violateIfNoMatchesFound: false @@ -175,7 +175,7 @@ final class LintTests: XCTestCase { // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` // Lint.validateParameterCombinations( - // checkInfo: .init(id: "2", hint: "hint #2"), + // check: .init(id: "2", hint: "hint #2"), // autoCorrectReplacement: "$3$2$1", // autoCorrectExamples: [.init(before: "abc", after: "cba")], // violateIfNoMatchesFound: true diff --git a/Tests/CoreTests/CheckInfoTests.swift b/Tests/CoreTests/CheckInfoTests.swift index e6e709d..f8961e0 100644 --- a/Tests/CoreTests/CheckInfoTests.swift +++ b/Tests/CoreTests/CheckInfoTests.swift @@ -1,26 +1,26 @@ @testable import Core import XCTest -final class CheckInfoTests: XCTestCase { +final class CheckTests: XCTestCase { func testInit() { - let checkInfo = CheckInfo(id: "SampleId", hint: "Some hint.", severity: .warning) - XCTAssertEqual(checkInfo.id, "SampleId") - XCTAssertEqual(checkInfo.hint, "Some hint.") - XCTAssertEqual(checkInfo.severity, .warning) + let check = Check(id: "SampleId", hint: "Some hint.", severity: .warning) + XCTAssertEqual(check.id, "SampleId") + XCTAssertEqual(check.hint, "Some hint.") + XCTAssertEqual(check.severity, .warning) - XCTAssertEqual(CheckInfo(id: "id", hint: "hint").severity, .error) + XCTAssertEqual(Check(id: "id", hint: "hint").severity, .error) } func testCodable() throws { - let checkInfo = CheckInfo(id: "SampleId", hint: "Some hint.", severity: .warning) - let encodedData = try JSONEncoder().encode(checkInfo) + let check = Check(id: "SampleId", hint: "Some hint.", severity: .warning) + let encodedData = try JSONEncoder().encode(check) let encodedString = String(data: encodedData, encoding: .utf8)! XCTAssertEqual(encodedString, #""SampleId@warning: Some hint.""#) - let decodedCheckInfo = try JSONDecoder().decode(CheckInfo.self, from: encodedData) - XCTAssertEqual(decodedCheckInfo.id, "SampleId") - XCTAssertEqual(decodedCheckInfo.hint, "Some hint.") - XCTAssertEqual(decodedCheckInfo.severity, .warning) + let decodedCheck = try JSONDecoder().decode(Check.self, from: encodedData) + XCTAssertEqual(decodedCheck.id, "SampleId") + XCTAssertEqual(decodedCheck.hint, "Some hint.") + XCTAssertEqual(decodedCheck.severity, .warning) } } diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index 8e1b077..f1e1737 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -8,28 +8,50 @@ final class LintResultsTests: XCTestCase { private var sampleLintResults: LintResults { [ Severity.error: [ - CheckInfo(id: "1", hint: "hint #1", severity: .error): [ - Violation(matchedString: "oink1", location: .init(filePath: "/sample/path1", row: 4, column: 2)), - Violation(matchedString: "boo1", location: .init(filePath: "/sample/path2", row: 40, column: 20)), + Check(id: "1", hint: "hint #1", severity: .error): [ Violation( + discoverDate: .sample(seed: 0), + matchedString: "oink1", + location: .init(filePath: "/sample/path1", row: 4, column: 2) + ), + Violation( + discoverDate: .sample(seed: 1), + matchedString: "boo1", + location: .init(filePath: "/sample/path2", row: 40, column: 20) + ), + Violation( + discoverDate: .sample(seed: 2), location: .init(filePath: "/sample/path2"), appliedAutoCorrection: .init(before: "foo", after: "bar") ), ] ], Severity.warning: [ - CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ - Violation(matchedString: "oink2", location: .init(filePath: "/sample/path1", row: 5, column: 6)), - Violation(matchedString: "boo2", location: .init(filePath: "/sample/path3", row: 50, column: 60)), + Check(id: "2", hint: "hint #2", severity: .warning): [ + Violation( + discoverDate: .sample(seed: 3), + matchedString: "oink2", + location: .init(filePath: "/sample/path1", row: 5, column: 6) + ), Violation( + discoverDate: .sample(seed: 4), + matchedString: "boo2", + location: .init(filePath: "/sample/path3", row: 50, column: 60) + ), + Violation( + discoverDate: .sample(seed: 5), location: .init(filePath: "/sample/path4"), appliedAutoCorrection: .init(before: "fool", after: "barl") ), ] ], Severity.info: [ - CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) + Check(id: "3", hint: "hint #3", severity: .info): [ + Violation( + discoverDate: .sample(seed: 6), + matchedString: "blubb", + location: .init(filePath: "/sample/path0", row: 10, column: 20) + ) ] ], ] @@ -57,15 +79,15 @@ final class LintResultsTests: XCTestCase { func testMergeResults() { let otherLintResults: LintResults = [ Severity.error: [ - CheckInfo(id: "1", hint: "hint #1", severity: .warning): [ + Check(id: "1", hint: "hint #1", severity: .warning): [ Violation(matchedString: "muuh", location: .init(filePath: "/sample/path4", row: 6, column: 3)), Violation( location: .init(filePath: "/sample/path5"), appliedAutoCorrection: .init(before: "fusion", after: "wario") ), ], - CheckInfo(id: "2", hint: "hint #2 (alternative)", severity: .warning): [], - CheckInfo(id: "4", hint: "hint #4", severity: .error): [ + Check(id: "2", hint: "hint #2 (alternative)", severity: .warning): [], + Check(id: "4", hint: "hint #4", severity: .error): [ Violation(matchedString: "super", location: .init(filePath: "/sample/path1", row: 2, column: 200)) ], ] @@ -77,16 +99,16 @@ final class LintResultsTests: XCTestCase { let allFoundViolations = lintResults.allFoundViolations XCTAssertNoDifference(allExecutedChecks.count, 6) - XCTAssertNoDifference(allExecutedChecks.map(\.id), ["1", "1", "2", "4", "2", "3"]) + XCTAssertNoDifference(allExecutedChecks.map(\.id), ["1", "1", "2", "2", "3", "4"]) XCTAssertNoDifference(allFoundViolations.count, 10) XCTAssertNoDifference( allFoundViolations.map(\.location).map(\.?.filePath).map(\.?.last), - ["1", "2", "2", "4", "5", "1", "1", "3", "4", "0"] + ["1", "2", "2", "1", "3", "4", "0", "4", "5", "1"] ) XCTAssertNoDifference( allFoundViolations.map(\.matchedString), - ["oink1", "boo1", nil, "muuh", nil, "super", "oink2", "boo2", nil, "blubb"] + ["oink1", "boo1", nil, "oink2", "boo2", nil, "blubb", "muuh", nil, "super"] ) } @@ -118,9 +140,9 @@ final class LintResultsTests: XCTestCase { XCTAssertNoDifference(lintResults.allExecutedChecks.count, 4) XCTAssertNoDifference( lintResults.allFoundViolations.map(\.matchedString).map(\.?.first), - ["o", "b", nil, "A", "B", nil, "o", "b", nil, "b"] + ["o", "b", nil, "o", "b", nil, "b", "A", "B", nil] ) - XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1", "Added", "2", "3"]) + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1", "2", "3", "Added"]) } func testReportToConsole() { @@ -200,7 +222,7 @@ final class LintResultsTests: XCTestCase { let reportedContents = try Data(contentsOf: resultFileUrl) let reportedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: reportedContents) - XCTAssertNoDifference(sampleLintResults.map(\.key), reportedLintResults.map(\.key)) + XCTAssertNoDifference(sampleLintResults, reportedLintResults) } func testViolations() { @@ -235,10 +257,10 @@ final class LintResultsTests: XCTestCase { lintResults = [ Severity.error: [ - CheckInfo(id: "1", hint: "hint #1", severity: .error): [] + Check(id: "1", hint: "hint #1", severity: .error): [] ], Severity.warning: [ - CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ + Check(id: "2", hint: "hint #2", severity: .warning): [ Violation(matchedString: "oink2", location: .init(filePath: "/sample/path1", row: 5, column: 6)), Violation(matchedString: "boo2", location: .init(filePath: "/sample/path3", row: 50, column: 60)), Violation( @@ -248,7 +270,7 @@ final class LintResultsTests: XCTestCase { ] ], Severity.info: [ - CheckInfo(id: "3", hint: "hint #3", severity: .info): [ + Check(id: "3", hint: "hint #3", severity: .info): [ Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], @@ -257,13 +279,13 @@ final class LintResultsTests: XCTestCase { lintResults = [ Severity.error: [ - CheckInfo(id: "1", hint: "hint #1", severity: .error): [] + Check(id: "1", hint: "hint #1", severity: .error): [] ], Severity.warning: [ - CheckInfo(id: "2", hint: "hint #2", severity: .warning): [] + Check(id: "2", hint: "hint #2", severity: .warning): [] ], Severity.info: [ - CheckInfo(id: "3", hint: "hint #3", severity: .info): [ + Check(id: "3", hint: "hint #3", severity: .info): [ Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], @@ -272,13 +294,13 @@ final class LintResultsTests: XCTestCase { lintResults = [ Severity.error: [ - CheckInfo(id: "1", hint: "hint #1", severity: .error): [] + Check(id: "1", hint: "hint #1", severity: .error): [] ], Severity.warning: [ - CheckInfo(id: "2", hint: "hint #2", severity: .warning): [] + Check(id: "2", hint: "hint #2", severity: .warning): [] ], Severity.info: [ - CheckInfo(id: "3", hint: "hint #3", severity: .info): [] + Check(id: "3", hint: "hint #3", severity: .info): [] ], ] XCTAssertEqual(lintResults.maxViolationSeverity(excludeAutocorrected: false), nil) @@ -287,41 +309,52 @@ final class LintResultsTests: XCTestCase { } func testCodable() throws { - let expectedJsonOutput = """ + let expectedJsonOutput = #""" { - "warning": { - "1@error: hint for #1": [{ - + "warning" : { + "1@error: hint for #1" : [ + { + "discoverDate" : "2001-01-01T00:00:00Z" }, { - "appliedAutoCorrection": { - "after": "A", - "before": "AAA" + "discoverDate" : "2001-01-01T01:00:00Z", + "appliedAutoCorrection" : { + "after" : "A", + "before" : "AAA" }, - "matchedString": "A" + "matchedString" : "A" }, { - "location": { - "row": 5, - "column": 2, - "filePath": "/some/path" + "discoverDate" : "2001-01-01T02:00:00Z", + "location" : { + "row" : 5, + "column" : 2, + "filePath" : "\/some\/path" }, - "matchedString": "AAA" + "matchedString" : "AAA" } ] }, - "info": { + "info" : { } } - """ + """# let lintResults: LintResults = [ .warning: [ .init(id: "1", hint: "hint for #1"): [ - .init(), - .init(matchedString: "A", appliedAutoCorrection: .init(before: "AAA", after: "A")), - .init(matchedString: "AAA", location: .init(filePath: "/some/path", row: 5, column: 2)), + .init(discoverDate: .sample(seed: 0)), + .init( + discoverDate: .sample(seed: 1), + matchedString: "A", + appliedAutoCorrection: .init(before: "AAA", after: "A") + ), + .init( + discoverDate: .sample(seed: 2), + matchedString: "AAA", + location: .init(filePath: "/some/path", row: 5, column: 2) + ), ] ], .info: [:], diff --git a/lint.swift b/lint.swift index cb0499a..1e6e6f6 100755 --- a/lint.swift +++ b/lint.swift @@ -14,7 +14,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // 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.", + check: "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"], @@ -23,7 +23,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: ChangelogEntryTrailingWhitespaces try Lint.checkFileContents( - checkInfo: "ChangelogEntryTrailingWhitespaces: The summary line of a Changelog entry should end with two whitespaces.", + check: "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:"], @@ -41,7 +41,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: ChangelogEntryLeadingWhitespaces try Lint.checkFileContents( - checkInfo: "ChangelogEntryLeadingWhitespaces: The links line of a Changelog entry should start with two whitespaces.", + check: "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](#)"], @@ -56,7 +56,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: EmptyMethodBody try Lint.checkFileContents( - checkInfo: "EmptyMethodBody: Don't use whitespace or newlines for the body of empty methods.", + check: "EmptyMethodBody: Don't use whitespace or newlines for the body of empty methods.", regex: ["declaration": #"(init|func [^\(\s]+)\([^{}]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], matchingExamples: [ "init() { }", @@ -83,7 +83,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: EmptyTodo try Lint.checkFileContents( - checkInfo: "EmptyTodo: `// TODO:` comments should not be empty.", + check: "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"], @@ -92,7 +92,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: EmptyType try Lint.checkFileContents( - checkInfo: "EmptyType: Don't keep empty types in code without commenting inside why they are needed.", + check: "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 }"], @@ -101,7 +101,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: GuardMultiline2 try Lint.checkFileContents( - checkInfo: "GuardMultiline2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + check: "GuardMultiline2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: [ "newline": #"\n"#, "guardIndent": #" *"#, @@ -170,7 +170,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: GuardMultiline3 try Lint.checkFileContents( - checkInfo: "GuardMultiline3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + check: "GuardMultiline3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: [ "newline": #"\n"#, "guardIndent": #" *"#, @@ -246,7 +246,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: GuardMultiline4 try Lint.checkFileContents( - checkInfo: "GuardMultiline4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + check: "GuardMultiline4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: [ "newline": #"\n"#, "guardIndent": #" *"#, @@ -329,7 +329,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: GuardMultilineN try Lint.checkFileContents( - checkInfo: "GuardMultilineN: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + check: "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: [ """ @@ -370,7 +370,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: IfAsGuard try Lint.checkFileContents( - checkInfo: "IfAsGuard: Don't use an if statement to just return – use guard for such cases instead.", + check: "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 {"], @@ -379,7 +379,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: LateForceUnwrapping3 try Lint.checkFileContents( - checkInfo: "LateForceUnwrapping3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + check: "LateForceUnwrapping3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -402,7 +402,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: LateForceUnwrapping2 try Lint.checkFileContents( - checkInfo: "LateForceUnwrapping2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + check: "LateForceUnwrapping2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -423,7 +423,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: LateForceUnwrapping1 try Lint.checkFileContents( - checkInfo: "LateForceUnwrapping1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + check: "LateForceUnwrapping1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -442,7 +442,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: Logger try Lint.checkFileContents( - checkInfo: "Logger: Don't use `print` – use `log.message` instead.", + check: "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!")"#], @@ -452,7 +452,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: Readme try Lint.checkFilePaths( - checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", + check: "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"], @@ -461,7 +461,7 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { // MARK: ReadmePath try Lint.checkFilePaths( - checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", + check: "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"],