From b63074733b213b75adf628e9b829394869d283ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 22 Jun 2021 22:15:41 +0200 Subject: [PATCH 01/37] Migrate from Swift script to YAML based config The decision to move away from Swift scripts was not done lightly. Swift scripts are just not very pleasant to use yet, swift-sh has several problems and the entire script is too slow. --- .swift-format | 15 +++ .swiftlint.yml | 122 ----------------- Formula/anylint.rb | 1 - Package.resolved | 9 ++ Package.swift | 76 ++++++----- README.md | 107 ++++++++------- Sources/AnyLint/AutoCorrection.swift | 2 +- .../BlankTemplate.swift | 123 ++++++++---------- .../ConfigurationTemplate.swift | 20 --- Sources/AnyLintCLI/Globals/CLIConstants.swift | 3 +- .../AnyLintCLI/Globals/ValidateOrFail.swift | 12 -- .../AnyLintCLI/Models/LintConfiguration.swift | 39 ++++++ Sources/AnyLintCLI/Tasks/LintTask.swift | 97 +++++++++----- Sources/Utility/Regex.swift | 15 +++ Tests/LinuxMain.swift | 3 +- 15 files changed, 301 insertions(+), 343 deletions(-) create mode 100644 .swift-format delete mode 100644 .swiftlint.yml create mode 100644 Sources/AnyLintCLI/Models/LintConfiguration.swift diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..27735f7 --- /dev/null +++ b/.swift-format @@ -0,0 +1,15 @@ +{ + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": true, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineLength": 120, + "prioritizeKeepingFunctionOutputTogether": true, + "rules": { + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoLeadingUnderscores": true, + "ValidateDocumentationComments": true, + }, + "tabWidth": 2, + "version": 1, +} diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index b51b982..0000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,122 +0,0 @@ -# Basic Configuration -opt_in_rules: -- anyobject_protocol -- array_init -- attributes -- closure_end_indentation -- closure_spacing -- collection_alignment -- conditional_returns_on_newline -- contains_over_filter_count -- contains_over_filter_is_empty -- contains_over_first_not_nil -- contains_over_range_nil_comparison -- convenience_type -- empty_collection_literal -- empty_count -- empty_string -- empty_xctest_method -- explicit_init -- explicit_type_interface -- fallthrough -- fatal_error_message -- file_name -- file_name_no_space -- file_types_order -- first_where -- flatmap_over_map_reduce -- function_default_parameter_at_end -- identical_operands -- implicit_return -- implicitly_unwrapped_optional -- indentation_width -- joined_default_parameter -- last_where -- legacy_multiple -- legacy_random -- literal_expression_end_indentation -- lower_acl_than_parent -- missing_docs -- modifier_order -- multiline_arguments -- multiline_arguments_brackets -- multiline_literal_brackets -- multiline_parameters -- multiline_parameters_brackets -- nslocalizedstring_key -- number_separator -- object_literal -- operator_usage_whitespace -- optional_enum_case_matching -- override_in_extension -- pattern_matching_keywords -- prefer_self_type_over_type_of_self -- private_action -- private_outlet -- prohibited_super_call -- reduce_into -- redundant_nil_coalescing -- redundant_type_annotation -- single_test_class -- sorted_first_last -- sorted_imports -- static_operator -- strong_iboutlet -- switch_case_on_newline -- toggle_bool -- trailing_closure -- type_contents_order -- unavailable_function -- unneeded_parentheses_in_closure_argument -- untyped_error_in_catch -- unused_declaration -- unused_import -- vertical_parameter_alignment_on_call -- vertical_whitespace_between_cases -- vertical_whitespace_closing_braces -- vertical_whitespace_opening_braces -- xct_specific_matcher -- yoda_condition - -included: - - Sources - - Tests - -excluded: - - Tests/LinuxMain.swift - -disabled_rules: - - todo - - cyclomatic_complexity - -# Rule Configurations -conditional_returns_on_newline: - if_only: true - -explicit_type_interface: - allow_redundancy: true - excluded: - - local - -file_name: - suffix_pattern: "Ext" - -identifier_name: - max_length: 60 - excluded: - - id - - db - - to - -line_length: - warning: 160 - ignores_comments: true - -nesting: - type_level: 3 - -trailing_comma: - mandatory_comma: true - -trailing_whitespace: - ignores_comments: false diff --git a/Formula/anylint.rb b/Formula/anylint.rb index 6a7fe13..0e43091 100644 --- a/Formula/anylint.rb +++ b/Formula/anylint.rb @@ -5,7 +5,6 @@ class Anylint < Formula head "https://github.com/Flinesoft/AnyLint.git" depends_on :xcode => ["11.4", :build] - depends_on "swift-sh" def install system "make", "install", "prefix=#{prefix}" diff --git a/Package.resolved b/Package.resolved index aa38a47..6a8dae2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,6 +18,15 @@ "revision": "c72c4564f8c0a24700a59824880536aca45a4cae", "version": "6.0.1" } + }, + { + "package": "Yams", + "repositoryURL": "https://github.com/jpsim/Yams.git", + "state": { + "branch": null, + "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", + "version": "4.0.6" + } } ] }, diff --git a/Package.swift b/Package.swift index b266fdf..47b0f0f 100644 --- a/Package.swift +++ b/Package.swift @@ -2,39 +2,45 @@ import PackageDescription let package = Package( - name: "AnyLint", - 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: [ - .target( - name: "AnyLint", - dependencies: ["Utility"] - ), - .testTarget( - name: "AnyLintTests", - dependencies: ["AnyLint"] - ), - .target( - name: "AnyLintCLI", - dependencies: ["Rainbow", "SwiftCLI", "Utility"] - ), - .testTarget( - name: "AnyLintCLITests", - dependencies: ["AnyLintCLI"] - ), - .target( - name: "Utility", - dependencies: ["Rainbow"] - ), - .testTarget( - name: "UtilityTests", - dependencies: ["Utility"] - ) - ] + name: "AnyLint", + products: [ + .library(name: "AnyLint", targets: ["AnyLint", "Utility"]), + .executable(name: "anylint", targets: ["AnyLintCLI"]), + ], + dependencies: [ + // Delightful console output for Swift developers. + .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), + + // A powerful framework for developing CLIs in Swift + .package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.1"), + + // A Sweet and Swifty YAML parser. + .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.6"), + ], + targets: [ + .target( + name: "AnyLint", + dependencies: ["Utility"] + ), + .testTarget( + name: "AnyLintTests", + dependencies: ["AnyLint"] + ), + .target( + name: "AnyLintCLI", + dependencies: ["AnyLint", "Rainbow", "SwiftCLI", "Utility", "Yams"] + ), + .testTarget( + name: "AnyLintCLITests", + dependencies: ["AnyLintCLI"] + ), + .target( + name: "Utility", + dependencies: ["Rainbow"] + ), + .testTarget( + name: "UtilityTests", + dependencies: ["Utility"] + ) + ] ) diff --git a/README.md b/README.md index 8cfcdc3..bc556dd 100644 --- a/README.md +++ b/README.md @@ -87,59 +87,74 @@ To initialize AnyLint in a project, run: anylint --init blank ``` -This will create the Swift script file `lint.swift` with something like the following contents: - -```swift -#!/usr/local/bin/swift-sh -import AnyLint // @Flinesoft - -Lint.logSummaryAndExit(arguments: CommandLine.arguments) { - // 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 the project.", - regex: readmeFile, - matchingExamples: ["README.md"], - nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], - violateIfNoMatchesFound: true - ) - - // MARK: ReadmeTypoLicense - try Lint.checkFileContents( - checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.", - regex: #"([\s#]L|l)isence([\s\.,:;])"#, - matchingExamples: [" license:", "## Lisence\n"], - nonMatchingExamples: [" license:", "## License\n"], - includeFilters: [readmeFile], - autoCorrectReplacement: "$1icense$2", - autoCorrectExamples: [ - ["before": " lisence:", "after": " license:"], - ["before": "## Lisence\n", "after": "## License\n"], - ] - ) -} - +This will create the Swift script file `anylint.yml` with something like the following contents: + +```yaml +CheckFileContents: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + + - id: ReadmeTopLevelTitle + hint: 'The README.md file should only contain a single top level title.' + regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + includeFilter: ['^README\.md$'] + matchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + nonMatchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + + - id: ReadmeTypoLicense + hint: 'ReadmeTypoLicense: Misspelled word `license`.' + regex: '([\s#]L|l)isence([\s\.,:;])' + matchingExamples: [' lisence:', '## Lisence\n'] + nonMatchingExamples: [' license:', '## License\n'] + includeFilters: ['^README\.md$'] + autoCorrectReplacement: '$1icense$2' + autoCorrectExamples: + - { before: ' lisence:', after: ' license:' } + - { before: '## Lisence\n', after: '## License\n' } + +CheckFilePaths: + - id: 'ReadmePath' + hint: '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' } ``` -The most important thing to note is that the **first three lines are required** for AnyLint to work properly. - -All the other code can be adjusted and that's actually where you configure your lint checks (a few examples are provided by default in the `blank` template). Note that the first two lines declare the file to be a Swift script using [swift-sh](https://github.com/mxcl/swift-sh). Thus, you can run any Swift code and even import Swift packages (see the [swift-sh docs](https://github.com/mxcl/swift-sh#usage)) if you need to. The third line makes sure that all violations found in the process of running the code in the completion block are reported properly and exits the script with the proper exit code at the end. - Having this configuration file, you can now run `anylint` to run your lint checks. By default, if any check fails, the entire command fails and reports the violation reason. To learn more about how to configure your own checks, see the [Configuration](#configuration) section below. If you want to create and run multiple configuration files or if you want a different name or location for the default config file, you can pass the `--path` option, which can be used multiple times as well like this: Initializes the configuration files at the given locations: ```bash -anylint --init blank --path Sources/lint.swift --path Tests/lint.swift +anylint --init blank --path Sources/anylint.yml --path Tests/anylint.yml ``` Runs the lint checks for both configuration files: ```bash -anylint --path Sources/lint.swift --path Tests/lint.swift +anylint --path Sources/anylint.yml --path Tests/anylint.yml ``` There are also several flags you can pass to `anylint`: @@ -154,11 +169,11 @@ There are also several flags you can pass to `anylint`: AnyLint provides three different kinds of lint checks: -1. `checkFileContents`: Matches the contents of a text file to a given regex. -2. `checkFilePaths`: Matches the file paths of the current directory to a given regex. -3. `customCheck`: Allows to write custom Swift code to do other kinds of checks. +1. `CheckFileContents`: Matches the contents of a text file to a given regex. +2. `CheckFilePaths`: Matches the file paths of the current directory to a given regex. +3. `CustomScripts`: Allows to write custom scripts in any language to do other kinds of checks. (TODO) -Several examples of lint checks can be found in the [`lint.swift` file of this very project](https://github.com/Flinesoft/AnyLint/blob/main/lint.swift). +Several examples of lint checks can be found in the [`anylint.yml` file of this very project](https://github.com/Flinesoft/AnyLint/blob/main/anylint.yml). ### Basic Types @@ -387,6 +402,8 @@ By default, `checkFilePaths` will fail if the given `regex` matches a file. If y ### Custom Checks +TODO: Update to new custom script format supporting all languages as long as they output Violation JOSN format. + 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. diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift index eff4213..62415cc 100644 --- a/Sources/AnyLint/AutoCorrection.swift +++ b/Sources/AnyLint/AutoCorrection.swift @@ -2,7 +2,7 @@ import Foundation import Utility /// Information about an autocorrection. -public struct AutoCorrection { +public struct AutoCorrection: Codable { /// The matching text before applying the autocorrection. public let before: String diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 13b2d97..bf84365 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -5,74 +5,59 @@ import Utility enum BlankTemplate: ConfigurationTemplate { static func fileContents() -> String { - commonPrefix + #""" - // MARK: - Variables - let readmeFile: Regex = #"^README\.md$"# - - // MARK: - Checks - // MARK: Readme - try Lint.checkFilePaths( - checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", - regex: readmeFile, - matchingExamples: ["README.md"], - nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], - violateIfNoMatchesFound: true - ) - - // MARK: ReadmePath - try Lint.checkFilePaths( - checkInfo: "ReadmePath: The README file should be named exactly `README.md`.", - regex: #"^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$"#, - matchingExamples: ["README.markdown", "readme.md", "ReadMe.md"], - nonMatchingExamples: ["README.md", "CHANGELOG.md", "CONTRIBUTING.md", "api/help.md"], - autoCorrectReplacement: "$1README.md", - autoCorrectExamples: [ - ["before": "api/readme.md", "after": "api/README.md"], - ["before": "ReadMe.md", "after": "README.md"], - ["before": "README.markdown", "after": "README.md"], - ] - ) - - // MARK: ReadmeTopLevelTitle - try Lint.checkFileContents( - checkInfo: "ReadmeTopLevelTitle: The README.md file should only contain a single top level title.", - regex: #"(^|\n)#[^#](.*\n)*\n#[^#]"#, - matchingExamples: [ - """ - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - """, - ], - nonMatchingExamples: [ - """ - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle - """, - ], - includeFilters: [readmeFile] - ) - - // MARK: ReadmeTypoLicense - try Lint.checkFileContents( - checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.", - regex: #"([\s#]L|l)isence([\s\.,:;])"#, - matchingExamples: [" lisence:", "## Lisence\n"], - nonMatchingExamples: [" license:", "## License\n"], - includeFilters: [readmeFile], - autoCorrectReplacement: "$1icense$2", - autoCorrectExamples: [ - ["before": " lisence:", "after": " license:"], - ["before": "## Lisence\n", "after": "## License\n"], - ] - ) - """# + commonSuffix + #""" + CheckFileContents: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + + - id: ReadmeTopLevelTitle + hint: 'The README.md file should only contain a single top level title.' + regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + includeFilter: ['^README\.md$'] + matchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + nonMatchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + + - id: ReadmeTypoLicense + hint: 'ReadmeTypoLicense: Misspelled word `license`.' + regex: '([\s#]L|l)isence([\s\.,:;])' + matchingExamples: [' lisence:', '## Lisence\n'] + nonMatchingExamples: [' license:', '## License\n'] + includeFilters: ['^README\.md$'] + autoCorrectReplacement: '$1icense$2' + autoCorrectExamples: + - { before: ' lisence:', after: ' license:' } + - { before: '## Lisence\n', after: '## License\n' } + + CheckFilePaths: + - id: 'ReadmePath' + hint: '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' } + + """# } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index 76b9122..f4842ce 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -4,23 +4,3 @@ import Utility protocol ConfigurationTemplate { static func fileContents() -> String } - -extension ConfigurationTemplate { - static var commonPrefix: String { - """ - #!\(CLIConstants.swiftShPath) - import AnyLint // @Flinesoft - - try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { - - """ - } - - static var commonSuffix: String { - """ - - } - - """ - } -} diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift index 07111e3..9dc0b34 100644 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -2,7 +2,6 @@ import Foundation enum CLIConstants { static let commandName: String = "anylint" - static let defaultConfigFileName: String = "lint.swift" + static let defaultConfigFileName: String = "anylint.yml" static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") - static let swiftShPath: String = "/usr/local/bin/swift-sh" } diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index f83a5cb..44e07c4 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -3,18 +3,6 @@ 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 follow instructions on https://github.com/mxcl/swift-sh#installation to install.", - 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( diff --git a/Sources/AnyLintCLI/Models/LintConfiguration.swift b/Sources/AnyLintCLI/Models/LintConfiguration.swift new file mode 100644 index 0000000..537c3ff --- /dev/null +++ b/Sources/AnyLintCLI/Models/LintConfiguration.swift @@ -0,0 +1,39 @@ +import AnyLint +import Foundation +import Utility + +struct LintConfiguration: Codable { + enum CodingKeys: String, CodingKey { + case checkFileContents = "CheckFileContents" + case checkFilePaths = "CheckFilePaths" + } + + let checkFileContents: [CheckFileContentsConfiguration] + let checkFilePaths: [CheckFilePathsConfiguration] +} + +struct CheckFileContentsConfiguration: Codable { + let id: String + let hint: String + let regex: String + let matchingExamples: [String]? + let nonMatchingExamples: [String]? + let includeFilters: [Regex]? + let excludeFilters: [Regex]? + let autoCorrectReplacement: String? + let autoCorrectExamples: [AutoCorrection]? + let repeatIfAutoCorrected: Bool? +} + +struct CheckFilePathsConfiguration: Codable { + let id: String + let hint: String + let regex: String + let matchingExamples: [String]? + let nonMatchingExamples: [String]? + let includeFilters: [Regex]? + let excludeFilters: [Regex]? + let autoCorrectReplacement: String? + let autoCorrectExamples: [AutoCorrection]? + let violateIfNoMatchesFound: Bool? +} diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index 949b553..cf9e608 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -1,54 +1,83 @@ +import AnyLint import Foundation import SwiftCLI import Utility +import Yams struct LintTask { - let configFilePath: String - let logDebugLevel: Bool - let failOnWarnings: Bool - let validateOnly: Bool + let configFilePath: String + let logDebugLevel: Bool + let failOnWarnings: Bool + let validateOnly: Bool } extension LintTask: TaskHandler { - enum LintError: Error { - case configFileFailed - } - - /// - Throws: `LintError.configFileFailed` if running a configuration file fails - func perform() throws { - try ValidateOrFail.configFileExists(at: configFilePath) + enum LintError: Error { + case configFileFailed + } - if !fileManager.isExecutableFile(atPath: configFilePath) { - try Task.run(bash: "chmod +x '\(configFilePath)'") - } + /// - Throws: `LintError.configFileFailed` if running a configuration file fails + func perform() throws { + try ValidateOrFail.configFileExists(at: configFilePath) - ValidateOrFail.swiftShInstalled() + let configFileUrl = URL(fileURLWithPath: configFilePath) + let configFileData = try Data(contentsOf: configFileUrl) + let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) - 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 arguments: [String] = [log.outputType.rawValue] - if logDebugLevel { - command += " \(Constants.debugArgument)" - } + if logDebugLevel { + arguments.append(Constants.debugArgument) + } - if failOnWarnings { - command += " \(Constants.strictArgument)" - } + if failOnWarnings { + arguments.append(Constants.strictArgument) + } - if validateOnly { - command += " \(Constants.validateArgument)" - } + if validateOnly { + arguments.append(Constants.validateArgument) + } - 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) - } + try Lint.logSummaryAndExit(arguments: arguments) { + for checkFileContent in lintConfig.checkFileContents { + try Lint.checkFileContents( + checkInfo: .init(id: checkFileContent.hint, hint: checkFileContent.hint), + regex: .init(checkFileContent.regex), + matchingExamples: checkFileContent.matchingExamples ?? [], + nonMatchingExamples: checkFileContent.nonMatchingExamples ?? [], + includeFilters: checkFileContent.includeFilters ?? [Regex(".*")], + excludeFilters: checkFileContent.excludeFilters ?? [], + autoCorrectReplacement: checkFileContent.autoCorrectReplacement, + autoCorrectExamples: checkFileContent.autoCorrectExamples ?? [], + repeatIfAutoCorrected: checkFileContent.repeatIfAutoCorrected ?? false + ) + } - throw LintError.configFileFailed + for checkFilePath in lintConfig.checkFilePaths { + try Lint.checkFilePaths( + checkInfo: .init(id: checkFilePath.id, hint: checkFilePath.hint), + regex: .init(checkFilePath.regex), + matchingExamples: checkFilePath.matchingExamples ?? [], + nonMatchingExamples: checkFilePath.nonMatchingExamples ?? [], + includeFilters: checkFilePath.includeFilters ?? [Regex(".*")], + excludeFilters: checkFilePath.excludeFilters ?? [], + autoCorrectReplacement: checkFilePath.autoCorrectReplacement, + autoCorrectExamples: checkFilePath.autoCorrectExamples ?? [], + violateIfNoMatchesFound: checkFilePath.violateIfNoMatchesFound ?? false + ) } + } + + log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success) + } catch is RunError { + if log.outputType != .xcode { + log.message("Linting failed using config file at \(configFilePath).", level: .error) + } + + throw LintError.configFileFailed } + } } diff --git a/Sources/Utility/Regex.swift b/Sources/Utility/Regex.swift index f2231d1..e71f59a 100644 --- a/Sources/Utility/Regex.swift +++ b/Sources/Utility/Regex.swift @@ -133,6 +133,21 @@ extension Regex: Hashable { } } +extension Regex: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let pattern = try container.decode(String.self) + try self.init(pattern) + } +} + +extension Regex: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(pattern) + } +} + // MARK: - Options extension Regex { /// `Options` defines alternate behaviours of regular expressions when matching. diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index ae7fdcc..32b329e 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 1.0.3 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT - @testable import AnyLintTests @testable import Utility import XCTest From 0fd6de3755874e9049c0892ecd98901aa31d0bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 22 Jun 2021 22:19:48 +0200 Subject: [PATCH 02/37] Migrate from SwiftLint to Apples Swift-Format --- .sourcery/LinuxMain.stencil | 8 +- Sources/AnyLint/AutoCorrection.swift | 140 ++--- Sources/AnyLint/CheckInfo.swift | 118 ++-- Sources/AnyLint/Checkers/Checker.swift | 2 +- .../Checkers/FileContentsChecker.swift | 192 +++---- .../AnyLint/Checkers/FilePathsChecker.swift | 88 +-- Sources/AnyLint/Extensions/ArrayExt.swift | 10 +- .../AnyLint/Extensions/FileManagerExt.swift | 61 +-- Sources/AnyLint/Extensions/StringExt.swift | 38 +- Sources/AnyLint/Extensions/URLExt.swift | 8 +- Sources/AnyLint/FilesSearch.swift | 186 ++++--- Sources/AnyLint/Lint.swift | 494 ++++++++--------- Sources/AnyLint/Options.swift | 2 +- Sources/AnyLint/Severity.swift | 58 +- Sources/AnyLint/Statistics.swift | 257 ++++----- Sources/AnyLint/Violation.swift | 60 +-- .../AnyLintCLI/Commands/SingleCommand.swift | 132 +++-- .../BlankTemplate.swift | 112 ++-- .../ConfigurationTemplate.swift | 2 +- Sources/AnyLintCLI/Globals/CLIConstants.swift | 6 +- .../AnyLintCLI/Globals/ValidateOrFail.swift | 18 +- .../AnyLintCLI/Models/LintConfiguration.swift | 2 +- Sources/AnyLintCLI/Tasks/InitTask.swift | 64 +-- Sources/AnyLintCLI/Tasks/LintTask.swift | 3 +- Sources/AnyLintCLI/Tasks/TaskHandler.swift | 2 +- Sources/AnyLintCLI/Tasks/VersionTask.swift | 8 +- Sources/Utility/Constants.swift | 36 +- .../Utility/Extensions/CollectionExt.swift | 8 +- .../Utility/Extensions/FileManagerExt.swift | 18 +- Sources/Utility/Extensions/RegexExt.swift | 136 +++-- Sources/Utility/Extensions/StringExt.swift | 92 ++-- Sources/Utility/Logger.swift | 253 ++++----- Sources/Utility/Regex.swift | 507 +++++++++--------- Sources/Utility/TestHelper.swift | 26 +- Tests/AnyLintCLITests/AnyLintCLITests.swift | 6 +- Tests/AnyLintTests/AutoCorrectionTests.swift | 60 +-- Tests/AnyLintTests/CheckInfoTests.swift | 46 +- .../Checkers/FileContentsCheckerTests.swift | 438 ++++++++------- .../Checkers/FilePathsCheckerTests.swift | 118 ++-- .../Extensions/ArrayExtTests.swift | 22 +- .../Extensions/XCTestCaseExt.swift | 37 +- Tests/AnyLintTests/FilesSearchTests.swift | 104 ++-- Tests/AnyLintTests/LintTests.swift | 224 ++++---- Tests/AnyLintTests/RegexExtTests.swift | 22 +- Tests/AnyLintTests/StatisticsTests.swift | 230 ++++---- Tests/AnyLintTests/ViolationTests.swift | 34 +- Tests/LinuxMain.swift | 102 ++-- .../Extensions/RegexExtTests.swift | 72 +-- Tests/UtilityTests/LoggerTests.swift | 38 +- 49 files changed, 2443 insertions(+), 2257 deletions(-) diff --git a/.sourcery/LinuxMain.stencil b/.sourcery/LinuxMain.stencil index f5ff980..b64042f 100644 --- a/.sourcery/LinuxMain.stencil +++ b/.sourcery/LinuxMain.stencil @@ -6,12 +6,12 @@ import XCTest {% for type in types.classes|based:"XCTestCase" %} extension {{ type.name }} { - static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [ - {% for method in type.methods where method.parameters.count == 0 and method.shortName|hasPrefix:"test" and method|!annotated:"skipTestOnLinux" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %} - {% endfor %}] + static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [ + {% for method in type.methods where method.parameters.count == 0 and method.shortName|hasPrefix:"test" and method|!annotated:"skipTestOnLinux" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %} + {% endfor %}] } {% endfor %} XCTMain([ -{% for type in types.classes|based:"XCTestCase" %} testCase({{ type.name }}.allTests){% if not forloop.last %},{% endif %} +{% for type in types.classes|based:"XCTestCase" %} testCase({{ type.name }}.allTests){% if not forloop.last %},{% endif %} {% endfor %}]) diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift index 62415cc..60f762b 100644 --- a/Sources/AnyLint/AutoCorrection.swift +++ b/Sources/AnyLint/AutoCorrection.swift @@ -3,89 +3,97 @@ import Utility /// Information about an autocorrection. public struct AutoCorrection: Codable { - /// The matching text before applying the autocorrection. - public let before: String - - /// The matching text after applying the autocorrection. - public let after: String - - var appliedMessageLines: [String] { - if useDiffOutput, #available(OSX 10.15, *) { - var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] - - let beforeLines = before.components(separatedBy: .newlines) - let afterLines = after.components(separatedBy: .newlines) - - for difference in afterLines.difference(from: beforeLines).sorted() { - switch difference { - case let .insert(offset, element, _): - lines.append("+ [L\(offset + 1)] \(element)".green) - - case let .remove(offset, element, _): - lines.append("- [L\(offset + 1)] \(element)".red) - } - } - - return lines - } else { - return [ - "Autocorrection applied, the diff is: (+ added, - removed)", - "- \(before.showWhitespacesAndNewlines())".red, - "+ \(after.showWhitespacesAndNewlines())".green, - ] + /// The matching text before applying the autocorrection. + public let before: String + + /// The matching text after applying the autocorrection. + public let after: String + + var appliedMessageLines: [String] { + if useDiffOutput, #available(OSX 10.15, *) { + var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] + + let beforeLines = before.components(separatedBy: .newlines) + let afterLines = after.components(separatedBy: .newlines) + + for difference in afterLines.difference(from: beforeLines).sorted() { + switch difference { + case let .insert(offset, element, _): + lines.append("+ [L\(offset + 1)] \(element)".green) + + case let .remove(offset, element, _): + lines.append("- [L\(offset + 1)] \(element)".red) } - } + } - var useDiffOutput: Bool { - before.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing || - after.components(separatedBy: .newlines).count >= Constants.newlinesRequiredForDiffing + return lines } - - /// Initializes an autocorrection. - public init(before: String, after: String) { - self.before = before - self.after = after + 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 + 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 (.remove(leftOffset, _, _), .insert(rightOffset, _, _)): + return leftOffset < rightOffset || true - case let (.insert(leftOffset, _, _), .remove(rightOffset, _, _)): - return leftOffset < rightOffset || false - } + 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 + 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 - } + case (.remove, .insert), (.insert, .remove): + return false } + } } diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index 9cd3080..0c6f60a 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -3,77 +3,87 @@ 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 = .error) { - 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 = .error + ) { + 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..72d60b2 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 5b03e2e..179554f 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -2,107 +2,117 @@ import Foundation import Utility struct FileContentsChecker { - let checkInfo: CheckInfo - let regex: Regex - let filePathsToCheck: [String] - let autoCorrectReplacement: String? - let repeatIfAutoCorrected: Bool + let checkInfo: CheckInfo + let regex: Regex + 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 = fileContents.locationInfo(of: match.range.lowerBound) - - 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 + } + + let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(checkInfo.id)"#) + + for match in regex.matches(in: fileContents).reversed() { + let locationInfo = fileContents.locationInfo(of: match.range.lowerBound) + + 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 } - Statistics.shared.checkedFiles(at: [filePath]) + // 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 + ) + ) } - violations = violations.reversed() + 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]) + } - if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { - log.message("Repeating check \(checkInfo) because auto-corrections were applied on last run.", level: .debug) + violations = violations.reversed() - // 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() + if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { + log.message("Repeating check \(checkInfo) because auto-corrections were applied on last run.", level: .debug) - let violationsOnRechecks = try FileContentsChecker( - checkInfo: checkInfo, - regex: regex, - filePathsToCheck: filePathsToReCheck, - autoCorrectReplacement: autoCorrectReplacement, - repeatIfAutoCorrected: repeatIfAutoCorrected - ).performCheck() - violations.append(contentsOf: violationsOnRechecks) - } + // 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() - return violations + let violationsOnRechecks = try FileContentsChecker( + checkInfo: checkInfo, + regex: regex, + 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..ff3518f 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -2,51 +2,57 @@ 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) - ) - } - } else { - for filePath in filePathsToCheck where regex.matches(filePath) { - log.message("Found violating match for \(checkInfo) ...", level: .debug) - - let appliedAutoCorrection: AutoCorrection? = try { - guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } - - let newFilePath = regex.replaceAllCaptures(in: filePath, with: autoCorrectReplacement) - try fileManager.moveFileSafely(from: filePath, to: newFilePath) - - return AutoCorrection(before: filePath, after: newFilePath) - }() - - if appliedAutoCorrection != nil { - log.message("Applied autocorrection for last match ...", level: .debug) - } - - log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) - violations.append( - Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil, appliedAutoCorrection: appliedAutoCorrection) - ) - } - - Statistics.shared.checkedFiles(at: filePathsToCheck) + 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) } - return violations + log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) + violations.append( + Violation( + checkInfo: checkInfo, + filePath: filePath, + locationInfo: nil, + appliedAutoCorrection: appliedAutoCorrection + ) + ) + } + + Statistics.shared.checkedFiles(at: filePathsToCheck) } + + return violations + } } diff --git a/Sources/AnyLint/Extensions/ArrayExt.swift b/Sources/AnyLint/Extensions/ArrayExt.swift index 6067e9f..9acb876 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..d998797 100644 --- a/Sources/AnyLint/Extensions/FileManagerExt.swift +++ b/Sources/AnyLint/Extensions/FileManagerExt.swift @@ -2,39 +2,40 @@ 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 - } - - 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 - } + /// 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 + } - let targetParentDirectoryPath = targetPath.parentDirectoryPath - if !fileExists(atPath: targetParentDirectoryPath) { - try createDirectory(atPath: targetParentDirectoryPath, withIntermediateDirectories: true, attributes: nil) - } + 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 fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { - log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) - log.exit(status: .failure) - return // only reachable in unit tests - } + let targetParentDirectoryPath = targetPath.parentDirectoryPath + if !fileExists(atPath: targetParentDirectoryPath) { + try createDirectory(atPath: targetParentDirectoryPath, withIntermediateDirectories: true, attributes: nil) + } - 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) - } + 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 + } - FilesSearch.shared.invalidateCache() + 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() + } } diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 7bcc39d..6349e47 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.. 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..36930ef 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..08f105b 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -3,101 +3,119 @@ 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] + 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 } - /// The shared instance. - public static let shared = FilesSearch() - - private var cachedFilePaths: [SearchOptions: [String]] = [:] - - private init() {} + 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 + } - /// Should be called whenever files within the current directory are renamed, moved, added or deleted. - func invalidateCache() { - cachedFilePaths = [:] + 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 } - /// 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 + 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() } - 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 - } + continue + } - 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 + // 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() + } - 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() - } - - 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) + continue } + #endif + + guard isRegularFilePath, includeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) else { + continue + } - cachedFilePaths[searchOptions] = filePaths - return filePaths + filePaths.append(fileUrl.relativePathFromCurrent) } + + cachedFilePaths[searchOptions] = filePaths + return filePaths + } } diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 463d8e1..89d53d4 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -3,252 +3,266 @@ 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'. - /// - 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, - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [], - includeFilters: [Regex] = [#".*"#], - excludeFilters: [Regex] = [], - autoCorrectReplacement: String? = nil, - autoCorrectExamples: [AutoCorrection] = [], - repeatIfAutoCorrected: Bool = false - ) throws { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - - validateParameterCombinations( - checkInfo: checkInfo, - autoCorrectReplacement: autoCorrectReplacement, - autoCorrectExamples: autoCorrectExamples, - violateIfNoMatchesFound: nil - ) + /// 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'. + /// - 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, + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [], + includeFilters: [Regex] = [#".*"#], + excludeFilters: [Regex] = [], + autoCorrectReplacement: String? = nil, + autoCorrectExamples: [AutoCorrection] = [], + repeatIfAutoCorrected: Bool = false + ) throws { + 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 + } + + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + let violations = try FileContentsChecker( + checkInfo: checkInfo, + regex: regex, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, + repeatIfAutoCorrected: repeatIfAutoCorrected + ) + .performCheck() + + Statistics.shared.found(violations: violations, in: checkInfo) + } + + /// 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 { + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + validateParameterCombinations( + checkInfo: checkInfo, + autoCorrectReplacement: autoCorrectReplacement, + autoCorrectExamples: autoCorrectExamples, + violateIfNoMatchesFound: violateIfNoMatchesFound + ) + + if let autoCorrectReplacement = autoCorrectReplacement { + validateAutocorrectsAll( + checkInfo: checkInfo, + examples: autoCorrectExamples, + regex: regex, + autocorrectReplacement: autoCorrectReplacement + ) + } + + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } + + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + let violations = try FilePathsChecker( + checkInfo: checkInfo, + regex: regex, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, + violateIfNoMatchesFound: violateIfNoMatchesFound + ) + .performCheck() + + Statistics.shared.found(violations: violations, in: checkInfo) + } - if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( - checkInfo: checkInfo, - examples: autoCorrectExamples, - regex: regex, - autocorrectReplacement: autoCorrectReplacement - ) - } - - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters + /// 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) -> [Violation]) { + guard !Options.validateOnly else { + Statistics.shared.executedChecks.append(checkInfo) + return + } + + Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) + } + + /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. + public static func logSummaryAndExit( + arguments: [String] = [], + afterPerformingChecks checksToPerform: () throws -> Void = {} + ) throws { + let failOnWarnings = arguments.contains(Constants.strictArgument) + let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) + + if targetIsXcode { + log = Logger(outputType: .xcode) + } + + log.logDebugLevel = arguments.contains(Constants.debugArgument) + Options.validateOnly = arguments.contains(Constants.validateArgument) + + try checksToPerform() + + guard !Options.validateOnly else { + Statistics.shared.logValidationSummary() + log.exit(status: .success) + return // only reachable in unit tests + } + + Statistics.shared.logCheckSummary() + + 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, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { + if matchingExamples.isFilled { + log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) + } + + for example in matchingExamples { + if !regex.matches(example) { + log.message( + "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", + level: .error ) + log.exit(status: .failure) + } + } + } - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: regex, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: autoCorrectReplacement, - repeatIfAutoCorrected: repeatIfAutoCorrected - ).performCheck() - - Statistics.shared.found(violations: violations, in: checkInfo) - } - - /// 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 { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - validateParameterCombinations( - checkInfo: checkInfo, - autoCorrectReplacement: autoCorrectReplacement, - autoCorrectExamples: autoCorrectExamples, - violateIfNoMatchesFound: violateIfNoMatchesFound + static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { + if nonMatchingExamples.isFilled { + log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) + } + + for example in nonMatchingExamples { + if regex.matches(example) { + log.message( + "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", + level: .error ) + log.exit(status: .failure) + } + } + } - if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( - checkInfo: checkInfo, - examples: autoCorrectExamples, - regex: regex, - autocorrectReplacement: autoCorrectReplacement - ) - } - - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters + static func validateAutocorrectsAll( + checkInfo: CheckInfo, + examples: [AutoCorrection], + regex: Regex, + autocorrectReplacement: String + ) { + if examples.isFilled { + log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) + } + + for autocorrect in examples { + let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) + if autocorrected != autocorrect.after { + log.message( + """ + Autocorrecting example for \(checkInfo.id) did not result in expected output. + Before: '\(autocorrect.before.showWhitespacesAndNewlines())' + After: '\(autocorrected.showWhitespacesAndNewlines())' + Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' + """, + level: .error ) + 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 + ) + } - let violations = try FilePathsChecker( - checkInfo: checkInfo, - regex: regex, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: autoCorrectReplacement, - violateIfNoMatchesFound: violateIfNoMatchesFound - ).performCheck() - - Statistics.shared.found(violations: violations, in: checkInfo) - } - - /// 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) -> [Violation]) { - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) - } - - /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. - public static func logSummaryAndExit(arguments: [String] = [], afterPerformingChecks checksToPerform: () throws -> Void = {}) throws { - let failOnWarnings = arguments.contains(Constants.strictArgument) - let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) - - if targetIsXcode { - log = Logger(outputType: .xcode) - } - - log.logDebugLevel = arguments.contains(Constants.debugArgument) - Options.validateOnly = arguments.contains(Constants.validateArgument) - - try checksToPerform() - - guard !Options.validateOnly else { - Statistics.shared.logValidationSummary() - log.exit(status: .success) - return // only reachable in unit tests - } - - Statistics.shared.logCheckSummary() - - 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, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { - if matchingExamples.isFilled { - log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) - } - - for example in matchingExamples { - if !regex.matches(example) { - log.message( - "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { - if nonMatchingExamples.isFilled { - log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) - } - - for example in nonMatchingExamples { - if regex.matches(example) { - log.message( - "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validateAutocorrectsAll(checkInfo: CheckInfo, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { - if examples.isFilled { - log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) - } - - for autocorrect in examples { - let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) - if autocorrected != autocorrect.after { - log.message( - """ - Autocorrecting example for \(checkInfo.id) did not result in expected output. - Before: '\(autocorrect.before.showWhitespacesAndNewlines())' - After: '\(autocorrected.showWhitespacesAndNewlines())' - Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' - """, - level: .error - ) - 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 - ) - } - - 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 - } + 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..9f47c30 100644 --- a/Sources/AnyLint/Options.swift +++ b/Sources/AnyLint/Options.swift @@ -1,5 +1,5 @@ import Foundation enum Options { - static var validateOnly: Bool = false + static var validateOnly: Bool = false } diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift index 195c8a0..8cfe0ac 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 ad79441..421e698 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -2,139 +2,154 @@ 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 maxViolationSeverity: Severity? { - violationsBySeverity.keys.filter { !violationsBySeverity[$0]!.isEmpty }.max { $0.rawValue < $1.rawValue } + static let shared = Statistics() + + var executedChecks: [CheckInfo] = [] + var violationsPerCheck: [CheckInfo: [Violation]] = [:] + var violationsBySeverity: [Severity: [Violation]] = [.info: [], .warning: [], .error: []] + var filesChecked: Set = [] + + 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) + } + + /// 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 } - private init() {} - - func checkedFiles(at filePaths: [String]) { - filePaths.forEach { filesChecked.insert($0) } + if executedChecks.isEmpty { + log.message("No checks found to validate.", level: .warning) } - - func found(violations: [Violation], in check: CheckInfo) { - executedChecks.append(check) - violationsPerCheck[check] = violations - violationsBySeverity[check.severity]!.append(contentsOf: violations) + else { + log.message( + "Performed \(executedChecks.count) validation(s) in \(filesChecked.count) file(s) without any issues.", + level: .success + ) } + } - /// Use for unit testing only. - func reset() { - executedChecks = [] - violationsPerCheck = [:] - violationsBySeverity = [.info: [], .warning: [], .error: []] - filesChecked = [] + func logCheckSummary() { + if executedChecks.isEmpty { + log.message("No checks found to perform.", level: .warning) } - - 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() { - if executedChecks.isEmpty { - log.message("No checks found to perform.", level: .warning) - } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { - switch log.outputType { - case .console, .test: - logViolationsToConsole() - - case .xcode: - showViolationsInXcode() - } - } else { - log.message( - "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", - level: .success - ) - } + else if violationsBySeverity.values.contains(where: { $0.isFilled }) { + switch log.outputType { + case .console, .test: + logViolationsToConsole() + + case .xcode: + showViolationsInXcode() + } } - - func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { - let violations: [Violation] = violationsBySeverity[severity]! - guard excludeAutocorrected else { return violations } - return violations.filter { $0.appliedAutoCorrection == nil } + else { + log.message( + "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", + level: .success + ) } - - 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) + } + + 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.. " + matchedStringOutput, level: .info) } + } + } + else { + log.message( + "\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", + 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 - ) + log.message(">> Hint: \(check.hint)".bold.italic, level: check.severity.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) - ) - } - } + 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..4d6b58c 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/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index e1b1b45..cdc491e 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -3,74 +3,96 @@ 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: - 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 + // 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( + "-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( + "-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("-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( + "-l", + "--validate", + description: "Runs only validations for `matchingExamples`, `nonMatchingExamples` and `autoCorrectExamples`." + ) + var validate: Bool - @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(CLIConstants.initTemplateCases)]") - var initTemplateName: String? + @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: - 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) - } + // MARK: - Execution + func execute() throws { + if xcode { + log = Logger(outputType: .xcode) + } - log.logDebugLevel = debug + log.logDebugLevel = debug - // version subcommand - if version { - try VersionTask().perform() - log.exit(status: .success) - } + // version subcommand + if version { + try VersionTask().perform() + log.exit(status: .success) + } - let configurationPaths = customPaths.isEmpty - ? [fileManager.currentDirectoryPath.appendingPathComponent(CLIConstants.defaultConfigFileName)] - : customPaths + 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 - } + // 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) - } + 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: debug, failOnWarnings: strict, validateOnly: validate).perform() - } catch LintTask.LintError.configFileFailed { - anyConfigFileFailed = true - } - } - exit(anyConfigFileFailed ? EXIT_FAILURE : EXIT_SUCCESS) + // lint main command + var anyConfigFileFailed = false + for configPath in configurationPaths { + do { + try LintTask(configFilePath: configPath, logDebugLevel: debug, failOnWarnings: strict, validateOnly: validate) + .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 bf84365..1f93e43 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -4,60 +4,60 @@ import Utility // swiftlint:disable function_body_length enum BlankTemplate: ConfigurationTemplate { - static func fileContents() -> String { - #""" - CheckFileContents: - - id: Readme - hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' - regex: '^README\.md$' - violateIfNoMatchesFound: true - matchingExamples: ['README.md'] - nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] - - - id: ReadmeTopLevelTitle - hint: 'The README.md file should only contain a single top level title.' - regex: '(^|\n)#[^#](.*\n)*\n#[^#]' - includeFilter: ['^README\.md$'] - matchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - nonMatchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle - - - id: ReadmeTypoLicense - hint: 'ReadmeTypoLicense: Misspelled word `license`.' - regex: '([\s#]L|l)isence([\s\.,:;])' - matchingExamples: [' lisence:', '## Lisence\n'] - nonMatchingExamples: [' license:', '## License\n'] - includeFilters: ['^README\.md$'] - autoCorrectReplacement: '$1icense$2' - autoCorrectExamples: - - { before: ' lisence:', after: ' license:' } - - { before: '## Lisence\n', after: '## License\n' } - - CheckFilePaths: - - id: 'ReadmePath' - hint: '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' } - - """# - } + static func fileContents() -> String { + #""" + CheckFileContents: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + + - id: ReadmeTopLevelTitle + hint: 'The README.md file should only contain a single top level title.' + regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + includeFilter: ['^README\.md$'] + matchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + nonMatchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + + - id: ReadmeTypoLicense + hint: 'ReadmeTypoLicense: Misspelled word `license`.' + regex: '([\s#]L|l)isence([\s\.,:;])' + matchingExamples: [' lisence:', '## Lisence\n'] + nonMatchingExamples: [' license:', '## License\n'] + includeFilters: ['^README\.md$'] + autoCorrectReplacement: '$1icense$2' + autoCorrectExamples: + - { before: ' lisence:', after: ' license:' } + - { before: '## Lisence\n', after: '## License\n' } + + CheckFilePaths: + - id: 'ReadmePath' + hint: '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' } + + """# + } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index f4842ce..37035ca 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -2,5 +2,5 @@ import Foundation import Utility protocol ConfigurationTemplate { - static func fileContents() -> String + static func fileContents() -> String } diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift index 9dc0b34..83a5327 100644 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -1,7 +1,7 @@ import Foundation enum CLIConstants { - static let commandName: String = "anylint" - static let defaultConfigFileName: String = "anylint.yml" - static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") + static let commandName: String = "anylint" + static let defaultConfigFileName: String = "anylint.yml" + static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") } diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index 44e07c4..dfac6db 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -3,14 +3,14 @@ import SwiftCLI import Utility enum ValidateOrFail { - static func configFileExists(at configFilePath: String) throws { - guard fileManager.fileExists(atPath: configFilePath) else { - log.message( - "No configuration file found at \(configFilePath) – consider running `\(CLIConstants.commandName) --init` with a template.", - 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 `\(CLIConstants.commandName) --init` with a template.", + level: .error + ) + log.exit(status: .failure) + return // only reachable in unit tests } + } } diff --git a/Sources/AnyLintCLI/Models/LintConfiguration.swift b/Sources/AnyLintCLI/Models/LintConfiguration.swift index 537c3ff..118e949 100644 --- a/Sources/AnyLintCLI/Models/LintConfiguration.swift +++ b/Sources/AnyLintCLI/Models/LintConfiguration.swift @@ -4,7 +4,7 @@ import Utility struct LintConfiguration: Codable { enum CodingKeys: String, CodingKey { - case checkFileContents = "CheckFileContents" + case checkFileContents = "CheckFileContents" case checkFilePaths = "CheckFilePaths" } diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index dbcf06f..5bb7094 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -3,42 +3,42 @@ import SwiftCLI import Utility struct InitTask { - enum Template: String, CaseIterable { - case blank - - var configFileContents: String { - switch self { - case .blank: - return BlankTemplate.fileContents() - } - } + enum Template: String, CaseIterable { + case blank + + var configFileContents: String { + switch self { + case .blank: + return BlankTemplate.fileContents() + } } + } - let configFilePath: String - let template: Template + 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 - } - - 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 } + + 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 cf9e608..712672a 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -72,7 +72,8 @@ extension LintTask: TaskHandler { } log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success) - } catch is RunError { + } + catch is RunError { if log.outputType != .xcode { log.message("Linting failed using config file at \(configFilePath).", level: .error) } diff --git a/Sources/AnyLintCLI/Tasks/TaskHandler.swift b/Sources/AnyLintCLI/Tasks/TaskHandler.swift index 9986c03..b2b651f 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..2718eff 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -1,10 +1,10 @@ import Foundation import Utility -struct VersionTask { /* for extension purposes only */ } +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 df77c8b..9bd297e 100644 --- a/Sources/Utility/Constants.swift +++ b/Sources/Utility/Constants.swift @@ -8,30 +8,30 @@ 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.8.2" + /// The current tool version string. Conforms to SemVer 2.0. + public static let currentVersion: String = "0.8.2" - /// 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 separator indicating that next come regex options. - public static let regexOptionsSeparator: String = #"\"# + /// The separator indicating that next come regex options. + public static let regexOptionsSeparator: String = #"\"# - /// Hint that the case insensitive option should be active on a Regex. - public static let caseInsensitiveRegexOption: String = "i" + /// Hint that the case insensitive option should be active on a Regex. + public static let caseInsensitiveRegexOption: String = "i" - /// Hint that the case dot matches newline option should be active on a Regex. - public static let dotMatchesNewlinesRegexOption: String = "m" + /// 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 + /// 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..4c16304 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..e9fdffb 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..b69cf1c 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -1,76 +1,96 @@ 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..e08600a 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 + /// 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 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 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 } - /// 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 - } + return pathUrl.appendingPathComponent(pathComponent).absoluteString + } } diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index 260afc3..4d05d39 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -3,157 +3,160 @@ 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 + /// 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 any kind of information potentially interesting to users. + case info - /// Print information that might potentially be problematic. - case warning + /// Print information that might potentially be problematic. + case warning - /// Print information that probably is problematic. - case error + /// Print information that probably is problematic. + case error - /// Print detailed information for debugging purposes. - case debug + /// Print detailed information for debugging purposes. + case debug - var color: Color { - switch self { - case .success: - return Color.lightGreen + var color: Color { + switch self { + case .success: + return Color.lightGreen - case .info: - return Color.lightBlue + case .info: + return Color.lightBlue - case .warning: - return Color.yellow + case .warning: + return Color.yellow - case .error: - return Color.red + case .error: + return Color.red - case .debug: - return Color.default - } - } + 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 + /// 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 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 + /// Output is targeted for unit tests. Collect into globally accessible TestHelper. + case test + } - var statusCode: Int32 { - switch self { - case .success: - return EXIT_SUCCESS + /// The exit status. + public enum ExitStatus { + /// Successfully finished task. + case success - case .failure: - return EXIT_FAILURE - } - } - } - - /// The output type of the logger. - public let outputType: OutputType + /// Failed to finish task. + case failure - /// Defines if the log should include debug logs. - public var logDebugLevel: Bool = false + var statusCode: Int32 { + switch self { + case .success: + return EXIT_SUCCESS - /// Initializes a new Logger object with a given output type. - public init(outputType: OutputType) { - self.outputType = outputType + case .failure: + return EXIT_FAILURE + } } - - /// 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)) - } + } + + /// 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 - } + } + + /// 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) + private func consoleMessage(_ message: String, level: PrintLevel) { + switch level { + case .success: + print(formattedCurrentTime(), "✅", message.green) - case .info: - print(formattedCurrentTime(), "ℹ️ ", message.lightBlue) + case .info: + print(formattedCurrentTime(), "ℹ️ ", message.lightBlue) - case .warning: - print(formattedCurrentTime(), "⚠️ ", message.yellow) + case .warning: + print(formattedCurrentTime(), "⚠️ ", message.yellow) - case .error: - print(formattedCurrentTime(), "❌", message.red) + case .error: + print(formattedCurrentTime(), "❌", message.red) - case .debug: - print(formattedCurrentTime(), "💬", message) - } + 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)") - } + } + + /// 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)") } - - private func formattedCurrentTime() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss.SSS" - let dateTime = dateFormatter.string(from: Date()) - return "\(dateTime):" + 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 e71f59a..04e663e 100644 --- a/Sources/Utility/Regex.swift +++ b/Sources/Utility/Regex.swift @@ -4,137 +4,144 @@ 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 + /// 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: - 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 + } - // 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 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 + } - /// 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.. 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 - } + 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) + } } extension Regex: Decodable { - public init(from decoder: Decoder) throws { + public init( + from decoder: Decoder + ) throws { let container = try decoder.singleValueContainer() let pattern = try container.decode(String.self) try self.init(pattern) @@ -150,158 +157,164 @@ extension Regex: Encodable { // 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 - } + /// `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) - // MARK: - Initializers - /// The raw value init for the `OptionSet` - public init(rawValue: Int) { - self.rawValue = rawValue - } + /// 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]) - } - }() - - 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 + /// 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) } - // 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 - } + return captureRanges.map { [unowned self] captureRange in + guard let captureRange = captureRange else { return nil } + return String(describing: self.baseString[captureRange]) + } + }() - // MARK: - CustomStringConvertible - /// Returns a string describing the match. - public var description: String { - "Match<\"\(string)\">" - } + 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..3a48686 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 index 5be114c..46746f2 100644 --- a/Tests/AnyLintCLITests/AnyLintCLITests.swift +++ b/Tests/AnyLintCLITests/AnyLintCLITests.swift @@ -1,7 +1,7 @@ import XCTest final class AnyLintCLITests: XCTestCase { - func testExample() { - // TODO: [cg_2020-03-07] not yet implemented - } + func testExample() { + // TODO: [cg_2020-03-07] not yet implemented + } } diff --git a/Tests/AnyLintTests/AutoCorrectionTests.swift b/Tests/AnyLintTests/AutoCorrectionTests.swift index f554ec5..dd97d23 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 ad69b2e..b41b87f 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, .error) - } + let checkInfo4: CheckInfo = "test4: hint4" + XCTAssertEqual(checkInfo4.id, "test4") + XCTAssertEqual(checkInfo4.hint, "hint4") + XCTAssertEqual(checkInfo4.severity, .error) + } } diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index f1eefa7..3029ca0 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -5,213 +5,243 @@ import XCTest // swiftlint:disable function_body_length final class FileContentsCheckerTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() + 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+"#, + 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 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+"#, - 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+"#, - 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 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+"#, + 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+"#, - 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 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+"#, + 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+)"#, - 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 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+)"#, + 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..73f48e5 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..7bb0835 100644 --- a/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift +++ b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift @@ -3,23 +3,32 @@ 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..852f81b 100644 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -5,58 +5,62 @@ import XCTest // swiftlint:disable force_try final class FilesSearchTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } + 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"]) - 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"]) - } + 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"#)] + ) + } + + // first run + XCTAssertEqual(Set(fileSearchCode()), Set(swiftSourcesFilePaths.map { "\(tempDir)/\($0.subpath)" })) - 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)" })) - - 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..fce220a 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..8d3cd08 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 6d63653..1881b02 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -4,119 +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() - 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() - - XCTAssertEqual( - TestHelper.shared.consoleOutputs.map { $0.level }, - [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] - ) - - let expectedOutputs = [ - "\("[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() + 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() + + XCTAssertEqual( + TestHelper.shared.consoleOutputs.map { $0.level }, + [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] + ) + + let expectedOutputs = [ + "\("[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..c4d64ed 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 32b329e..17d672f 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -7,85 +7,85 @@ import XCTest // swiftlint:disable line_length file_length extension ArrayExtTests { - static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ - ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) - ] + static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ + ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) + ] } extension AutoCorrectionTests { - static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), - ("testAppliedMessageLines", testAppliedMessageLines) - ] + static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ + ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), + ("testAppliedMessageLines", testAppliedMessageLines), + ] } extension CheckInfoTests { - static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral) - ] + static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ + ("testInitWithStringLiteral", testInitWithStringLiteral) + ] } extension FileContentsCheckerTests { - static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck), - ("testSkipInFile", testSkipInFile), - ("testSkipHere", testSkipHere), - ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), - ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected) - ] + static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ + ("testPerformCheck", testPerformCheck), + ("testSkipInFile", testSkipInFile), + ("testSkipHere", testSkipHere), + ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), + ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected), + ] } extension FilePathsCheckerTests { - static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck) - ] + static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ + ("testPerformCheck", testPerformCheck) + ] } extension FilesSearchTests { - static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ - ("testAllFilesWithinPath", testAllFilesWithinPath), - ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions) - ] + static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ + ("testAllFilesWithinPath", testAllFilesWithinPath), + ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions), + ] } extension LintTests { - static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ - ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), - ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), - ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), - ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups) - ] + static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ + ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), + ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), + ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), + ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups), + ] } extension RegexExtTests { - static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral), - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral) - ] + static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ + ("testInitWithStringLiteral", testInitWithStringLiteral), + ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), + ] } extension StatisticsTests { - static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ - ("testFoundViolationsInCheck", testFoundViolationsInCheck), - ("testLogSummary", testLogSummary) - ] + static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ + ("testFoundViolationsInCheck", testFoundViolationsInCheck), + ("testLogSummary", testLogSummary), + ] } extension ViolationTests { - static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ - ("testLocationMessage", testLocationMessage) - ] + static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ + ("testLocationMessage", testLocationMessage) + ] } XCTMain([ - testCase(ArrayExtTests.allTests), - testCase(AutoCorrectionTests.allTests), - testCase(CheckInfoTests.allTests), - testCase(FileContentsCheckerTests.allTests), - testCase(FilePathsCheckerTests.allTests), - testCase(FilesSearchTests.allTests), - testCase(LintTests.allTests), - testCase(RegexExtTests.allTests), - testCase(StatisticsTests.allTests), - testCase(ViolationTests.allTests) + testCase(ArrayExtTests.allTests), + testCase(AutoCorrectionTests.allTests), + testCase(CheckInfoTests.allTests), + testCase(FileContentsCheckerTests.allTests), + testCase(FilePathsCheckerTests.allTests), + testCase(FilesSearchTests.allTests), + testCase(LintTests.allTests), + testCase(RegexExtTests.allTests), + testCase(StatisticsTests.allTests), + testCase(ViolationTests.allTests), ]) diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/UtilityTests/Extensions/RegexExtTests.swift index 5add852..a9a0f28 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..e3c9552 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") + } } From f8778f249bf40dde1eef23ca41c26e65b1867b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 28 Jun 2021 18:05:05 +0200 Subject: [PATCH 03/37] Start refactoring: Fully migrate to ArgumentParser --- .github/workflows/main.yml | 68 +------------- .sourcery/LinuxMain.stencil | 17 ---- CHANGELOG.md | 16 +++- Formula/anylint.rb | 4 +- Package.resolved | 17 +--- Package.swift | 56 ++++++------ README.md | 10 +- Sources/AnyLint/AutoCorrection.swift | 1 - Sources/AnyLint/CheckInfo.swift | 1 - .../Checkers/FileContentsChecker.swift | 1 - .../AnyLint/Checkers/FilePathsChecker.swift | 1 - Sources/{Utility => AnyLint}/Constants.swift | 0 .../Extensions/CollectionExt.swift | 0 .../AnyLint/Extensions/FileManagerExt.swift | 14 ++- .../Extensions/RegexExt.swift | 0 Sources/AnyLint/Extensions/StringExt.swift | 53 ++++++++++- Sources/AnyLint/Extensions/URLExt.swift | 1 - Sources/AnyLint/FilesSearch.swift | 1 - Sources/AnyLint/Lint.swift | 1 - Sources/{Utility => AnyLint}/Logger.swift | 16 +++- Sources/{Utility => AnyLint}/Regex.swift | 0 Sources/AnyLint/Severity.swift | 49 ---------- Sources/AnyLint/Statistics.swift | 1 - Sources/{Utility => AnyLint}/TestHelper.swift | 0 Sources/AnyLint/Violation.swift | 2 - .../AnyLintCLI/Commands/SingleCommand.swift | 2 - .../BlankTemplate.swift | 5 +- .../ConfigurationTemplate.swift | 1 - .../AnyLintCLI/Globals/ValidateOrFail.swift | 2 - .../AnyLintCLI/Models/LintConfiguration.swift | 1 - Sources/AnyLintCLI/Tasks/InitTask.swift | 2 - Sources/AnyLintCLI/Tasks/LintTask.swift | 2 - Sources/AnyLintCLI/Tasks/VersionTask.swift | 1 - Sources/AnyLintCLI/main.swift | 1 - Sources/Commands/AnyLint.swift | 19 ++++ Sources/Commands/InitCommand.swift | 29 ++++++ Sources/Commands/LintCommand.swift | 55 +++++++++++ .../Commands/OptionsStringConvertible.swift | 26 ++++++ Sources/Configuration/Template.swift | 10 ++ Sources/Core/Severity.swift | 43 +++++++++ Sources/Reporting/OutputFormat.swift | 13 +++ .../Utility/Extensions/FileManagerExt.swift | 14 --- Sources/Utility/Extensions/StringExt.swift | 53 ----------- Tests/AnyLintTests/CheckInfoTests.swift | 1 - .../Checkers/FileContentsCheckerTests.swift | 1 - .../Checkers/FilePathsCheckerTests.swift | 1 - .../Extensions/ArrayExtTests.swift | 1 - .../Extensions/RegexExtTests.swift | 1 - Tests/AnyLintTests/FilesSearchTests.swift | 1 - Tests/AnyLintTests/LintTests.swift | 1 - .../LoggerTests.swift | 1 - Tests/AnyLintTests/RegexExtTests.swift | 1 - Tests/AnyLintTests/StatisticsTests.swift | 2 - Tests/AnyLintTests/ViolationTests.swift | 2 - Tests/LinuxMain.swift | 91 ------------------- 55 files changed, 325 insertions(+), 388 deletions(-) delete mode 100644 .sourcery/LinuxMain.stencil rename Sources/{Utility => AnyLint}/Constants.swift (100%) rename Sources/{Utility => AnyLint}/Extensions/CollectionExt.swift (100%) rename Sources/{Utility => AnyLint}/Extensions/RegexExt.swift (100%) rename Sources/{Utility => AnyLint}/Logger.swift (95%) rename Sources/{Utility => AnyLint}/Regex.swift (100%) delete mode 100644 Sources/AnyLint/Severity.swift rename Sources/{Utility => AnyLint}/TestHelper.swift (100%) create mode 100644 Sources/Commands/AnyLint.swift create mode 100644 Sources/Commands/InitCommand.swift create mode 100644 Sources/Commands/LintCommand.swift create mode 100644 Sources/Commands/OptionsStringConvertible.swift create mode 100644 Sources/Configuration/Template.swift create mode 100644 Sources/Core/Severity.swift create mode 100644 Sources/Reporting/OutputFormat.swift delete mode 100644 Sources/Utility/Extensions/FileManagerExt.swift delete mode 100644 Sources/Utility/Extensions/StringExt.swift rename Tests/{UtilityTests => AnyLintTests}/Extensions/RegexExtTests.swift (98%) rename Tests/{UtilityTests => AnyLintTests}/LoggerTests.swift (97%) delete mode 100644 Tests/LinuxMain.swift diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f545b6..d33c42a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,67 +22,12 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Export latest tool versions - run: | - latest_version() { - curl --silent "https://api.github.com/repos/$1/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' - } - echo "::set-env name=ANYLINT_LATEST_VERSION::$( latest_version Flinesoft/AnyLint )" - echo "::set-env name=SWIFT_SH_LATEST_VERSION::$( latest_version mxcl/swift-sh )" - - - name: AnyLint Cache - uses: actions/cache@v1 - id: anylint-cache - with: - path: anylint-cache - key: ${{ runner.os }}-v1-anylint-${{ env.ANYLINT_LATEST_VERSION }}-swift-sh-${{ env.SWIFT_SH_LATEST_VERSION }} - - - name: Copy from cache - if: steps.anylint-cache.outputs.cache-hit - run: | - sudo cp -f anylint-cache/anylint /usr/local/bin/anylint - sudo cp -f anylint-cache/swift-sh /usr/local/bin/swift-sh - - name: Install AnyLint - if: steps.anylint-cache.outputs.cache-hit != 'true' - run: | - git clone https://github.com/Flinesoft/AnyLint.git - cd AnyLint - swift build -c release - sudo cp -f .build/release/anylint /usr/local/bin/anylint - - - name: Install swift-sh - if: steps.anylint-cache.outputs.cache-hit != 'true' - run: | - git clone https://github.com/mxcl/swift-sh.git - cd swift-sh - swift build -c release - sudo cp -f .build/release/swift-sh /usr/local/bin/swift-sh - - - name: Copy to cache - if: steps.anylint-cache.outputs.cache-hit != 'true' - run: | - mkdir -p anylint-cache - cp -f /usr/local/bin/anylint anylint-cache/anylint - cp -f /usr/local/bin/swift-sh anylint-cache/swift-sh - - - name: Cleanup checkouts - run: rm -rf AnyLint && rm -rf swift-sh + run: brew tap Flinesoft/AnyLint https://github.com/Flinesoft/AnyLint.git && brew install anylint - name: Run AnyLint run: anylint - swiftlint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Run SwiftLint - uses: norio-nomura/action-swiftlint@3.1.0 - with: - args: --strict - test-linux: runs-on: ubuntu-latest @@ -90,8 +35,7 @@ jobs: - uses: actions/checkout@v2 - name: Run tests - run: swift test -v - + run: swift test -v --enable-test-discovery test-macos: runs-on: macos-latest @@ -101,11 +45,3 @@ jobs: - 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 }} diff --git a/.sourcery/LinuxMain.stencil b/.sourcery/LinuxMain.stencil deleted file mode 100644 index b64042f..0000000 --- a/.sourcery/LinuxMain.stencil +++ /dev/null @@ -1,17 +0,0 @@ -@testable import AnyLintTests -@testable import Utility -import XCTest - -// swiftlint:disable line_length file_length - -{% for type in types.classes|based:"XCTestCase" %} -extension {{ type.name }} { - static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [ - {% for method in type.methods where method.parameters.count == 0 and method.shortName|hasPrefix:"test" and method|!annotated:"skipTestOnLinux" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %} - {% endfor %}] -} - -{% endfor %} -XCTMain([ -{% for type in types.classes|based:"XCTestCase" %} testCase({{ type.name }}.allTests){% if not forloop.last %},{% endif %} -{% endfor %}]) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5663247..8b005ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,15 +19,23 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ## [Unreleased] ### Added -- None. +- YAML-based configuration file. Supportts `FilePaths` and `FileContents` as well as `CustomScripts` for full feature parity with previous Swift-based configuration file. + Author: [Cihat Gündüz](https://github.com/Jeehut) ### Changed -- None. +- Migrated from jakeheis/SwiftCLI to apple/swift-argument-parser for improved reliability & reduced maintenance. + Author: [Cihat Gündüz](https://github.com/Jeehut) +- Vastly improved configuration reading performance by migrating over to a YAML-based approach rather than a Swift config file. + Author: [Cihat Gündüz](https://github.com/Jeehut) ### Deprecated - None. ### Removed -- None. +- Swift-based configuration file support removed in favor of YAML-based configuration. All features supported via the Swift file still supported via YAML file. See README.md for more details. Parameters were not renamed to keep migration simple. + Author: [Cihat Gündüz](https://github.com/Jeehut) +- Support for Swift versions below 5.5 was dropped to make use of the latest improvements in concurrency & SwiftPM plugin system. Use version `0.8.2` if you need to stay on lower Swift versions. + Author: [Cihat Gündüz](https://github.com/Jeehut) ### Fixed -- None. +- Issues with paths due to Swift scripting not being as easy to use now fixed by moving over to YAML-based configuration. For custom scripts, responsibility is moved to the user side by allowing to specify the exact command to run. + Author: [Cihat Gündüz](https://github.com/Jeehut) ### Security - None. diff --git a/Formula/anylint.rb b/Formula/anylint.rb index 0e43091..dda76e0 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" + desc "Lint anything by combining the power of scripts & regular expressions" homepage "https://github.com/Flinesoft/AnyLint" url "https://github.com/Flinesoft/AnyLint.git", :tag => "0.8.2", :revision => "73cb2c9de3ed8e027fddf8df16e998c552dd7823" head "https://github.com/Flinesoft/AnyLint.git" - depends_on :xcode => ["11.4", :build] + depends_on :xcode => ["12.0", :build] def install system "make", "install", "prefix=#{prefix}" diff --git a/Package.resolved b/Package.resolved index 6a8dae2..84842de 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,21 +2,12 @@ "object": { "pins": [ { - "package": "Rainbow", - "repositoryURL": "https://github.com/onevcat/Rainbow.git", + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser.git", "state": { "branch": null, - "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", - "version": "3.1.5" - } - }, - { - "package": "SwiftCLI", - "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", - "state": { - "branch": null, - "revision": "c72c4564f8c0a24700a59824880536aca45a4cae", - "version": "6.0.1" + "revision": "986d191f94cec88f6350056da59c2e59e83d1229", + "version": "0.4.3" } }, { diff --git a/Package.swift b/Package.swift index 47b0f0f..5e60ae6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,46 +1,44 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.5 import PackageDescription let package = Package( name: "AnyLint", products: [ - .library(name: "AnyLint", targets: ["AnyLint", "Utility"]), - .executable(name: "anylint", targets: ["AnyLintCLI"]), + .executable(name: "anylint", targets: ["Commands"]), ], dependencies: [ - // Delightful console output for Swift developers. - .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), - - // A powerful framework for developing CLIs in Swift - .package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.1"), + // Straightforward, type-safe argument parsing for Swift + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.4.3"), // A Sweet and Swifty YAML parser. .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.6"), ], targets: [ + .target(name: "Core"), + .target(name: "Checkers", dependencies: ["Core"]), .target( - name: "AnyLint", - dependencies: ["Utility"] - ), - .testTarget( - name: "AnyLintTests", - dependencies: ["AnyLint"] - ), - .target( - name: "AnyLintCLI", - dependencies: ["AnyLint", "Rainbow", "SwiftCLI", "Utility", "Yams"] + name: "Configuration", + dependencies: [ + "Core", + .product(name: "Yams", package: "Yams"), + ] ), - .testTarget( - name: "AnyLintCLITests", - dependencies: ["AnyLintCLI"] + .target(name: "Reporting", dependencies: ["Core"]), + .executableTarget( + name: "Commands", + dependencies: [ + "Configuration", + "Core", + "Reporting", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] ), - .target( - name: "Utility", - dependencies: ["Rainbow"] - ), - .testTarget( - name: "UtilityTests", - dependencies: ["Utility"] - ) + + // test targets + .testTarget(name: "CoreTests", dependencies: ["Core"]), + .testTarget(name: "CheckersTests", dependencies: ["Checkers"]), + .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), + .testTarget(name: "ReportingTests", dependencies: ["Reporting"]), + .testTarget(name: "CommandsTests", dependencies: ["Commands"]), ] ) diff --git a/README.md b/README.md index bc556dd..a5be570 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,6 @@ CI - - Code Quality - - - Coverage - Version: 0.8.2 @@ -52,7 +44,7 @@ # AnyLint -Lint any project in any language using Swift and regular expressions. With built-in support for matching and non-matching examples validation & autocorrect replacement. Replaces SwiftLint custom rules & works for other languages as well! 🎉 +Lint anything by combining the power of scripts & regular expressions. With built-in support for matching and non-matching examples validation & autocorrect replacement. Replaces SwiftLint custom rules & works for/with other languages as well! 🎉 ## Installation diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift index 60f762b..1db1a15 100644 --- a/Sources/AnyLint/AutoCorrection.swift +++ b/Sources/AnyLint/AutoCorrection.swift @@ -1,5 +1,4 @@ import Foundation -import Utility /// Information about an autocorrection. public struct AutoCorrection: Codable { diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index 0c6f60a..3bbd83e 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -1,5 +1,4 @@ import Foundation -import Utility /// Provides some basic information needed in each lint check. public struct CheckInfo { diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index 179554f..5ede46a 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -1,5 +1,4 @@ import Foundation -import Utility struct FileContentsChecker { let checkInfo: CheckInfo diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index ff3518f..3d0f42a 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -1,5 +1,4 @@ import Foundation -import Utility struct FilePathsChecker { let checkInfo: CheckInfo diff --git a/Sources/Utility/Constants.swift b/Sources/AnyLint/Constants.swift similarity index 100% rename from Sources/Utility/Constants.swift rename to Sources/AnyLint/Constants.swift diff --git a/Sources/Utility/Extensions/CollectionExt.swift b/Sources/AnyLint/Extensions/CollectionExt.swift similarity index 100% rename from Sources/Utility/Extensions/CollectionExt.swift rename to Sources/AnyLint/Extensions/CollectionExt.swift diff --git a/Sources/AnyLint/Extensions/FileManagerExt.swift b/Sources/AnyLint/Extensions/FileManagerExt.swift index d998797..1c21106 100644 --- a/Sources/AnyLint/Extensions/FileManagerExt.swift +++ b/Sources/AnyLint/Extensions/FileManagerExt.swift @@ -1,5 +1,4 @@ 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. @@ -39,3 +38,16 @@ extension FileManager { FilesSearch.shared.invalidateCache() } } + +extension FileManager { + /// 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 + } +} diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/AnyLint/Extensions/RegexExt.swift similarity index 100% rename from Sources/Utility/Extensions/RegexExt.swift rename to Sources/AnyLint/Extensions/RegexExt.swift diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 6349e47..92b5013 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -1,5 +1,4 @@ import Foundation -import Utility /// `Regex` is a swifty regex engine built on top of the NSRegularExpression api. public typealias Regex = Utility.Regex @@ -30,3 +29,55 @@ extension String { showNewlines().showWhitespaces() } } + +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 + } +} diff --git a/Sources/AnyLint/Extensions/URLExt.swift b/Sources/AnyLint/Extensions/URLExt.swift index 36930ef..c2902ee 100644 --- a/Sources/AnyLint/Extensions/URLExt.swift +++ b/Sources/AnyLint/Extensions/URLExt.swift @@ -1,5 +1,4 @@ import Foundation -import Utility extension URL { /// Returns the relative path of from the current path. diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index 08f105b..10cb036 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -1,5 +1,4 @@ import Foundation -import Utility /// Helper to search for files and filter using Regexes. public final class FilesSearch { diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 89d53d4..629b064 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -1,5 +1,4 @@ import Foundation -import Utility /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { diff --git a/Sources/Utility/Logger.swift b/Sources/AnyLint/Logger.swift similarity index 95% rename from Sources/Utility/Logger.swift rename to Sources/AnyLint/Logger.swift index 4d05d39..59259c0 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/AnyLint/Logger.swift @@ -1,5 +1,4 @@ import Foundation -import Rainbow /// Helper to log output to console or elsewhere. public final class Logger { @@ -160,3 +159,18 @@ public final class Logger { return "\(dateTime):" } } + +extension Severity { + var logLevel: Logger.PrintLevel { + switch self { + case .info: + return .info + + case .warning: + return .warning + + case .error: + return .error + } + } +} diff --git a/Sources/Utility/Regex.swift b/Sources/AnyLint/Regex.swift similarity index 100% rename from Sources/Utility/Regex.swift rename to Sources/AnyLint/Regex.swift diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift deleted file mode 100644 index 8cfe0ac..0000000 --- a/Sources/AnyLint/Severity.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -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 might potentially be problematic. - case warning - - /// Use for checks that probably are problematic. - case error - - var logLevel: Logger.PrintLevel { - switch self { - case .info: - return .info - - case .warning: - return .warning - - case .error: - return .error - } - } - - static func from(string: String) -> Severity? { - switch string { - case "info", "i": - return .info - - case "warning", "w": - return .warning - - case "error", "e": - return .error - - default: - return nil - } - } -} - -extension Severity: Comparable { - 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 421e698..b459dea 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -1,5 +1,4 @@ import Foundation -import Utility final class Statistics { static let shared = Statistics() diff --git a/Sources/Utility/TestHelper.swift b/Sources/AnyLint/TestHelper.swift similarity index 100% rename from Sources/Utility/TestHelper.swift rename to Sources/AnyLint/TestHelper.swift diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index 4d6b58c..e95f9a6 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -1,6 +1,4 @@ import Foundation -import Rainbow -import Utility /// A violation found in a check. public struct Violation { diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index cdc491e..6478b69 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -1,6 +1,4 @@ import Foundation -import SwiftCLI -import Utility class SingleCommand: Command { // MARK: - Basics diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 1f93e43..ce91b73 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -1,12 +1,11 @@ import Foundation -import Utility // swiftlint:disable function_body_length enum BlankTemplate: ConfigurationTemplate { static func fileContents() -> String { #""" - CheckFileContents: + FileContents: - id: Readme hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' regex: '^README\.md$' @@ -46,7 +45,7 @@ enum BlankTemplate: ConfigurationTemplate { - { before: ' lisence:', after: ' license:' } - { before: '## Lisence\n', after: '## License\n' } - CheckFilePaths: + FilePaths: - id: 'ReadmePath' hint: 'The README file should be named exactly `README.md`.' regex: '^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$' diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index 37035ca..c185166 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -1,5 +1,4 @@ import Foundation -import Utility protocol ConfigurationTemplate { static func fileContents() -> String diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index dfac6db..d7bd936 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -1,6 +1,4 @@ import Foundation -import SwiftCLI -import Utility enum ValidateOrFail { static func configFileExists(at configFilePath: String) throws { diff --git a/Sources/AnyLintCLI/Models/LintConfiguration.swift b/Sources/AnyLintCLI/Models/LintConfiguration.swift index 118e949..8eedc74 100644 --- a/Sources/AnyLintCLI/Models/LintConfiguration.swift +++ b/Sources/AnyLintCLI/Models/LintConfiguration.swift @@ -1,6 +1,5 @@ import AnyLint import Foundation -import Utility struct LintConfiguration: Codable { enum CodingKeys: String, CodingKey { diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index 5bb7094..328ba64 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -1,6 +1,4 @@ import Foundation -import SwiftCLI -import Utility struct InitTask { enum Template: String, CaseIterable { diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index 712672a..7fba56f 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -1,7 +1,5 @@ import AnyLint import Foundation -import SwiftCLI -import Utility import Yams struct LintTask { diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift index 2718eff..bde9f22 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -1,5 +1,4 @@ import Foundation -import Utility struct VersionTask { /* for extension purposes only */ } diff --git a/Sources/AnyLintCLI/main.swift b/Sources/AnyLintCLI/main.swift index d4cc92b..04d1e04 100644 --- a/Sources/AnyLintCLI/main.swift +++ b/Sources/AnyLintCLI/main.swift @@ -1,5 +1,4 @@ import Foundation -import SwiftCLI let singleCommand = CLI(singleCommand: SingleCommand()) singleCommand.goAndExit() diff --git a/Sources/Commands/AnyLint.swift b/Sources/Commands/AnyLint.swift new file mode 100644 index 0000000..4c39846 --- /dev/null +++ b/Sources/Commands/AnyLint.swift @@ -0,0 +1,19 @@ +import ArgumentParser +import Foundation + +@main +struct AnyLint: ParsableCommand { + static var configuration: CommandConfiguration = .init( + commandName: "anylint", + abstract: "Lint anything by combining the power of scripts & regular expressions.", + discussion: """ + Configure regex or script based rules in AnyLint expected YAML configuration format. + + AnyLint supports `FileContents` and `FilePaths` checks based on regexes with autocorrection & test support. + Additionally, you can use `CustomScripts` to specify your own commands or scripts, e.g. other linters. + """, + version: "1.0.0", + subcommands: [LintCommand.self, InitCommand.self], + defaultSubcommand: LintCommand.self + ) +} diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift new file mode 100644 index 0000000..e649026 --- /dev/null +++ b/Sources/Commands/InitCommand.swift @@ -0,0 +1,29 @@ +import ArgumentParser +import Configuration +import Core +import Foundation + +struct InitCommand: ParsableCommand { + static var configuration: CommandConfiguration = .init( + commandName: "init", + abstract: "Initializes a new AnyLint configuration file (at specified path & using the specified template)." + ) + + @Option( + name: .shortAndLong, + help: "The template to create the initial config file from. One of: \(Template.optionsDescription)." + ) + var template: Template = .blank + + @Option( + name: .shortAndLong, + help: "Path to the new config file to initialize it at." + ) + var path: String = URL(fileURLWithPath: ".").appendingPathComponent("anylint.yml").path + + mutating func run() throws { + // TODO: [cg_2021-06-28] not yet implemented + } +} + +extension Template: ExpressibleByArgument {} diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift new file mode 100644 index 0000000..c0830f8 --- /dev/null +++ b/Sources/Commands/LintCommand.swift @@ -0,0 +1,55 @@ +import ArgumentParser +import Core +import Foundation +import Reporting + +struct LintCommand: ParsableCommand { + static var configuration: CommandConfiguration = .init( + commandName: "lint", + abstract: "Runs the configured checks & reports the results in the specified format." + ) + + @Option( + name: .shortAndLong, + parsing: .upToNextOption, + help: .init("The path(s) to run the checks from.", valueName: "path") + ) + var paths: [String] = [URL(fileURLWithPath: ".").path] + + @Option( + name: .shortAndLong, + help: .init("Path to the config file to execute.", valueName: "path") + ) + var config: String = URL(fileURLWithPath: ".").appendingPathComponent("anylint.yml").path + + @Option( + name: .shortAndLong, + help: .init( + "The minimum severity level to fail on if any checks produce violations. One of: \(Severity.optionsDescription).", + valueName: "severity" + ) + ) + var failLevel: Severity = .error + + @Option( + name: .shortAndLong, + help: .init( + "The expected format of the output. One of: \(OutputFormat.optionsDescription).", + valueName: "format" + ) + ) + var outputFormat: OutputFormat = .commandLine + + @Flag( + name: .shortAndLong, + help: "Enables more verbose output for more details." + ) + var verbose: Bool = false + + mutating func run() throws { + // TODO: [cg_2021-06-28] not yet implemented + } +} + +extension Severity: ExpressibleByArgument {} +extension OutputFormat: ExpressibleByArgument {} diff --git a/Sources/Commands/OptionsStringConvertible.swift b/Sources/Commands/OptionsStringConvertible.swift new file mode 100644 index 0000000..028bcbf --- /dev/null +++ b/Sources/Commands/OptionsStringConvertible.swift @@ -0,0 +1,26 @@ +import Foundation +import Core +import Configuration +import Reporting + +protocol OptionsStringConvertible { + static var optionsDescription: String { get } +} + +extension Template: OptionsStringConvertible { + static var optionsDescription: String { + allCases.map(\.rawValue).joined(separator: ", ") + } +} + +extension Severity: OptionsStringConvertible { + static var optionsDescription: String { + allCases.map(\.rawValue).joined(separator: ", ") + } +} + +extension OutputFormat: OptionsStringConvertible { + static var optionsDescription: String { + allCases.map(\.rawValue).joined(separator: ", ") + } +} diff --git a/Sources/Configuration/Template.swift b/Sources/Configuration/Template.swift new file mode 100644 index 0000000..73ed0c1 --- /dev/null +++ b/Sources/Configuration/Template.swift @@ -0,0 +1,10 @@ +import Foundation + +/// The template for setting up configuration initially. +public enum Template: String, CaseIterable { + /// The blank template with all existing checks and one 'Hello world' kind of example per check. + case blank + + /// The template with some useful checks setup for open source projects. + case openSource +} diff --git a/Sources/Core/Severity.swift b/Sources/Core/Severity.swift new file mode 100644 index 0000000..fa6f8e6 --- /dev/null +++ b/Sources/Core/Severity.swift @@ -0,0 +1,43 @@ +import Foundation +import AppKit + +/// Defines the severity of a lint check. +public enum Severity: String, CaseIterable { + /// Use for checks that are mostly informational and not necessarily problematic. + case info + + /// Use for checks that might potentially be problematic. + case warning + + /// Use for checks that probably are problematic. + case error +// +// static func from(string: String) -> Severity? { +// switch string { +// case "info", "i": +// return .info +// +// case "warning", "w": +// return .warning +// +// case "error", "e": +// return .error +// +// default: +// return nil +// } +// } +} +// +//extension Severity: Comparable { +// public static func < (lhs: Severity, rhs: Severity) -> Bool { +// switch (lhs, rhs) { +// case (.info, .warning), (.warning, .error), (.info, .error): +// return true +// +// default: +// return false +// } +// } +//} + diff --git a/Sources/Reporting/OutputFormat.swift b/Sources/Reporting/OutputFormat.swift new file mode 100644 index 0000000..3a8a4d0 --- /dev/null +++ b/Sources/Reporting/OutputFormat.swift @@ -0,0 +1,13 @@ +import Foundation + +/// The output format of violations and other statistics. +public enum OutputFormat: String, CaseIterable { + /// Output to the command line. Includes both violations & statistics summary at end. + case commandLine + + /// Output targeted to Xcode IDE. Includes only violations in the Xcode warning/error format. No statistics. + case xcode + + /// Output targeted to further usage from other tools or configurations. Output format same as script output, both violations & statistics. + case json +} diff --git a/Sources/Utility/Extensions/FileManagerExt.swift b/Sources/Utility/Extensions/FileManagerExt.swift deleted file mode 100644 index e9fdffb..0000000 --- a/Sources/Utility/Extensions/FileManagerExt.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -extension FileManager { - /// 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 - } -} diff --git a/Sources/Utility/Extensions/StringExt.swift b/Sources/Utility/Extensions/StringExt.swift deleted file mode 100644 index e08600a..0000000 --- a/Sources/Utility/Extensions/StringExt.swift +++ /dev/null @@ -1,53 +0,0 @@ -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 - } -} diff --git a/Tests/AnyLintTests/CheckInfoTests.swift b/Tests/AnyLintTests/CheckInfoTests.swift index b41b87f..6d30f31 100644 --- a/Tests/AnyLintTests/CheckInfoTests.swift +++ b/Tests/AnyLintTests/CheckInfoTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest final class CheckInfoTests: XCTestCase { diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index 3029ca0..478584b 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest // swiftlint:disable function_body_length diff --git a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift index 35c8a1e..e30e377 100644 --- a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest final class FilePathsCheckerTests: XCTestCase { diff --git a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift b/Tests/AnyLintTests/Extensions/ArrayExtTests.swift index 73f48e5..2594899 100644 --- a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift +++ b/Tests/AnyLintTests/Extensions/ArrayExtTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest final class ArrayExtTests: XCTestCase { diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/AnyLintTests/Extensions/RegexExtTests.swift similarity index 98% rename from Tests/UtilityTests/Extensions/RegexExtTests.swift rename to Tests/AnyLintTests/Extensions/RegexExtTests.swift index a9a0f28..f6253e7 100644 --- a/Tests/UtilityTests/Extensions/RegexExtTests.swift +++ b/Tests/AnyLintTests/Extensions/RegexExtTests.swift @@ -1,4 +1,3 @@ -@testable import Utility import XCTest final class RegexExtTests: XCTestCase { diff --git a/Tests/AnyLintTests/FilesSearchTests.swift b/Tests/AnyLintTests/FilesSearchTests.swift index 852f81b..7a1d539 100644 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest // swiftlint:disable force_try diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index fce220a..2a17f6b 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest final class LintTests: XCTestCase { diff --git a/Tests/UtilityTests/LoggerTests.swift b/Tests/AnyLintTests/LoggerTests.swift similarity index 97% rename from Tests/UtilityTests/LoggerTests.swift rename to Tests/AnyLintTests/LoggerTests.swift index e3c9552..5fdc282 100644 --- a/Tests/UtilityTests/LoggerTests.swift +++ b/Tests/AnyLintTests/LoggerTests.swift @@ -1,4 +1,3 @@ -@testable import Utility import XCTest final class LoggerTests: XCTestCase { diff --git a/Tests/AnyLintTests/RegexExtTests.swift b/Tests/AnyLintTests/RegexExtTests.swift index 8d3cd08..b3b9397 100644 --- a/Tests/AnyLintTests/RegexExtTests.swift +++ b/Tests/AnyLintTests/RegexExtTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -@testable import Utility import XCTest final class RegexExtTests: XCTestCase { diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 1881b02..5bd311c 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -1,6 +1,4 @@ @testable import AnyLint -import Rainbow -@testable import Utility import XCTest final class StatisticsTests: XCTestCase { diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift index c4d64ed..059f486 100644 --- a/Tests/AnyLintTests/ViolationTests.swift +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -1,6 +1,4 @@ @testable import AnyLint -import Rainbow -@testable import Utility import XCTest final class ViolationTests: XCTestCase { diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 17d672f..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,91 +0,0 @@ -// Generated using Sourcery 1.0.3 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT -@testable import AnyLintTests -@testable import Utility -import XCTest - -// swiftlint:disable line_length file_length - -extension ArrayExtTests { - static var allTests: [(String, (ArrayExtTests) -> () throws -> Void)] = [ - ("testContainsLineAtIndexesMatchingRegex", testContainsLineAtIndexesMatchingRegex) - ] -} - -extension AutoCorrectionTests { - static var allTests: [(String, (AutoCorrectionTests) -> () throws -> Void)] = [ - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), - ("testAppliedMessageLines", testAppliedMessageLines), - ] -} - -extension CheckInfoTests { - static var allTests: [(String, (CheckInfoTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral) - ] -} - -extension FileContentsCheckerTests { - static var allTests: [(String, (FileContentsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck), - ("testSkipInFile", testSkipInFile), - ("testSkipHere", testSkipHere), - ("testSkipIfEqualsToAutocorrectReplacement", testSkipIfEqualsToAutocorrectReplacement), - ("testRepeatIfAutoCorrected", testRepeatIfAutoCorrected), - ] -} - -extension FilePathsCheckerTests { - static var allTests: [(String, (FilePathsCheckerTests) -> () throws -> Void)] = [ - ("testPerformCheck", testPerformCheck) - ] -} - -extension FilesSearchTests { - static var allTests: [(String, (FilesSearchTests) -> () throws -> Void)] = [ - ("testAllFilesWithinPath", testAllFilesWithinPath), - ("testPerformanceOfSameSearchOptions", testPerformanceOfSameSearchOptions), - ] -} - -extension LintTests { - static var allTests: [(String, (LintTests) -> () throws -> Void)] = [ - ("testValidateRegexMatchesForEach", testValidateRegexMatchesForEach), - ("testValidateRegexDoesNotMatchAny", testValidateRegexDoesNotMatchAny), - ("testValidateAutocorrectsAllExamplesWithAnonymousGroups", testValidateAutocorrectsAllExamplesWithAnonymousGroups), - ("testValidateAutocorrectsAllExamplesWithNamedGroups", testValidateAutocorrectsAllExamplesWithNamedGroups), - ] -} - -extension RegexExtTests { - static var allTests: [(String, (RegexExtTests) -> () throws -> Void)] = [ - ("testInitWithStringLiteral", testInitWithStringLiteral), - ("testInitWithDictionaryLiteral", testInitWithDictionaryLiteral), - ] -} - -extension StatisticsTests { - static var allTests: [(String, (StatisticsTests) -> () throws -> Void)] = [ - ("testFoundViolationsInCheck", testFoundViolationsInCheck), - ("testLogSummary", testLogSummary), - ] -} - -extension ViolationTests { - static var allTests: [(String, (ViolationTests) -> () throws -> Void)] = [ - ("testLocationMessage", testLocationMessage) - ] -} - -XCTMain([ - testCase(ArrayExtTests.allTests), - testCase(AutoCorrectionTests.allTests), - testCase(CheckInfoTests.allTests), - testCase(FileContentsCheckerTests.allTests), - testCase(FilePathsCheckerTests.allTests), - testCase(FilesSearchTests.allTests), - testCase(LintTests.allTests), - testCase(RegexExtTests.allTests), - testCase(StatisticsTests.allTests), - testCase(ViolationTests.allTests), -]) From d8497a7e20c4aee168b4fd312438a3b2962aa936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 2 Jul 2021 13:34:46 +0200 Subject: [PATCH 04/37] Move most reusable files to correct new module --- Package.resolved | 9 + Package.swift | 10 +- Sources/AnyLint/AutoCorrection.swift | 98 ------- Sources/AnyLint/CheckInfo.swift | 88 ------ Sources/AnyLint/Checkers/Checker.swift | 5 - .../Checkers/FileContentsChecker.swift | 117 -------- .../AnyLint/Checkers/FilePathsChecker.swift | 57 ---- Sources/AnyLint/Constants.swift | 37 --- Sources/AnyLint/FilesSearch.swift | 120 -------- Sources/AnyLint/Lint.swift | 267 ------------------ Sources/AnyLint/Options.swift | 5 - Sources/AnyLint/Statistics.swift | 154 ---------- Sources/Checkers/Checker.swift | 7 + Sources/Checkers/CustomScriptsChecker.swift | 23 ++ .../Extensions/ArrayExt.swift | 0 .../Extensions/RegexExt.swift | 16 ++ Sources/Checkers/FileContentsChecker.swift | 135 +++++++++ Sources/Checkers/FilePathsChecker.swift | 75 +++++ .../Helpers}/FileManagerExt.swift | 14 +- Sources/Checkers/Helpers/FilesSearch.swift | 121 ++++++++ .../{AnyLint => Checkers/Helpers}/Regex.swift | 0 Sources/Checkers/Lint.swift | 267 ++++++++++++++++++ Sources/Core/AutoCorrection.swift | 98 +++++++ Sources/Core/CheckInfo.swift | 88 ++++++ .../Extensions/CollectionExt.swift | 0 Sources/Core/Extensions/FileManagerExt.swift | 14 + .../Extensions/StringExt.swift | 22 +- .../{AnyLint => Core}/Extensions/URLExt.swift | 2 +- Sources/{AnyLint => Core}/Logger.swift | 8 +- Sources/Core/Severity.swift | 33 ++- Sources/{AnyLint => Core}/TestHelper.swift | 0 Sources/{AnyLint => Core}/Violation.swift | 0 Sources/Reporting/Statistics.swift | 154 ++++++++++ 33 files changed, 1051 insertions(+), 993 deletions(-) delete mode 100644 Sources/AnyLint/AutoCorrection.swift delete mode 100644 Sources/AnyLint/CheckInfo.swift delete mode 100644 Sources/AnyLint/Checkers/Checker.swift delete mode 100644 Sources/AnyLint/Checkers/FileContentsChecker.swift delete mode 100644 Sources/AnyLint/Checkers/FilePathsChecker.swift delete mode 100644 Sources/AnyLint/Constants.swift delete mode 100644 Sources/AnyLint/FilesSearch.swift delete mode 100644 Sources/AnyLint/Lint.swift delete mode 100644 Sources/AnyLint/Options.swift delete mode 100644 Sources/AnyLint/Statistics.swift create mode 100644 Sources/Checkers/Checker.swift create mode 100644 Sources/Checkers/CustomScriptsChecker.swift rename Sources/{AnyLint => Checkers}/Extensions/ArrayExt.swift (100%) rename Sources/{AnyLint => Checkers}/Extensions/RegexExt.swift (85%) create mode 100644 Sources/Checkers/FileContentsChecker.swift create mode 100644 Sources/Checkers/FilePathsChecker.swift rename Sources/{AnyLint/Extensions => Checkers/Helpers}/FileManagerExt.swift (79%) create mode 100644 Sources/Checkers/Helpers/FilesSearch.swift rename Sources/{AnyLint => Checkers/Helpers}/Regex.swift (100%) create mode 100644 Sources/Checkers/Lint.swift create mode 100644 Sources/Core/AutoCorrection.swift create mode 100644 Sources/Core/CheckInfo.swift rename Sources/{AnyLint => Core}/Extensions/CollectionExt.swift (100%) create mode 100644 Sources/Core/Extensions/FileManagerExt.swift rename Sources/{AnyLint => Core}/Extensions/StringExt.swift (72%) rename Sources/{AnyLint => Core}/Extensions/URLExt.swift (57%) rename Sources/{AnyLint => Core}/Logger.swift (94%) rename Sources/{AnyLint => Core}/TestHelper.swift (100%) rename Sources/{AnyLint => Core}/Violation.swift (100%) create mode 100644 Sources/Reporting/Statistics.swift diff --git a/Package.resolved b/Package.resolved index 84842de..27fbd81 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "Rainbow", + "repositoryURL": "https://github.com/onevcat/Rainbow.git", + "state": { + "branch": null, + "revision": "7c3dad0e918534c6d19dd1048bde734c246d05fe", + "version": "4.0.0" + } + }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser.git", diff --git a/Package.swift b/Package.swift index 5e60ae6..52244f6 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,9 @@ let package = Package( .executable(name: "anylint", targets: ["Commands"]), ], dependencies: [ + // Delightful console output for Swift developers. + .package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.0"), + // Straightforward, type-safe argument parsing for Swift .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.4.3"), @@ -14,7 +17,12 @@ let package = Package( .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.6"), ], targets: [ - .target(name: "Core"), + .target( + name: "Core", + dependencies: [ + .product(name: "Rainbow", package: "Rainbow") + ] + ), .target(name: "Checkers", dependencies: ["Core"]), .target( name: "Configuration", diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift deleted file mode 100644 index 1db1a15..0000000 --- a/Sources/AnyLint/AutoCorrection.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation - -/// Information about an autocorrection. -public struct AutoCorrection: Codable { - /// The matching text before applying the autocorrection. - public let before: String - - /// The matching text after applying the autocorrection. - public let after: String - - var appliedMessageLines: [String] { - if useDiffOutput, #available(OSX 10.15, *) { - var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] - - let beforeLines = before.components(separatedBy: .newlines) - let afterLines = after.components(separatedBy: .newlines) - - for difference in afterLines.difference(from: beforeLines).sorted() { - switch difference { - case let .insert(offset, element, _): - lines.append("+ [L\(offset + 1)] \(element)".green) - - case let .remove(offset, element, _): - lines.append("- [L\(offset + 1)] \(element)".red) - } - } - - return lines - } - else { - return [ - "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) - } -} - -// 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 - } - } -} diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift deleted file mode 100644 index 3bbd83e..0000000 --- a/Sources/AnyLint/CheckInfo.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation - -/// 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 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 - - /// Initializes a new info object for the lint check. - public init( - id: String, - hint: String, - severity: Severity = .error - ) { - self.id = id - self.hint = hint - self.severity = severity - } -} - -extension CheckInfo: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -extension CheckInfo: CustomStringConvertible { - public var description: String { - "check '\(id)'" - } -} - -extension CheckInfo: ExpressibleByStringLiteral { - 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]! - - 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": #".*$"#, - ] - - 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]! - - self = CheckInfo(id: id, hint: hint) - } - } -} diff --git a/Sources/AnyLint/Checkers/Checker.swift b/Sources/AnyLint/Checkers/Checker.swift deleted file mode 100644 index 72d60b2..0000000 --- a/Sources/AnyLint/Checkers/Checker.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -protocol Checker { - func performCheck() throws -> [Violation] -} diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift deleted file mode 100644 index 5ede46a..0000000 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation - -struct FileContentsChecker { - let checkInfo: CheckInfo - let regex: Regex - 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 = fileContents.locationInfo(of: match.range.lowerBound) - - 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 - ) - } - - 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, - 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 deleted file mode 100644 index 3d0f42a..0000000 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Foundation - -struct FilePathsChecker { - 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) - ) - } - } - else { - for filePath in filePathsToCheck where regex.matches(filePath) { - log.message("Found violating match for \(checkInfo) ...", level: .debug) - - let appliedAutoCorrection: AutoCorrection? = try { - guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } - - let newFilePath = regex.replaceAllCaptures(in: filePath, with: autoCorrectReplacement) - try fileManager.moveFileSafely(from: filePath, to: newFilePath) - - return AutoCorrection(before: filePath, after: newFilePath) - }() - - if appliedAutoCorrection != nil { - log.message("Applied autocorrection for last match ...", level: .debug) - } - - log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) - violations.append( - Violation( - checkInfo: checkInfo, - filePath: filePath, - locationInfo: nil, - appliedAutoCorrection: appliedAutoCorrection - ) - ) - } - - Statistics.shared.checkedFiles(at: filePathsToCheck) - } - - return violations - } -} diff --git a/Sources/AnyLint/Constants.swift b/Sources/AnyLint/Constants.swift deleted file mode 100644 index 9bd297e..0000000 --- a/Sources/AnyLint/Constants.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation - -/// Shortcut to access the default `FileManager` within this project. -public let fileManager = FileManager.default - -/// Shortcut to access the `Logger` within this project. -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.8.2" - - /// The name of this tool. - public static let toolName: String = "AnyLint" - - /// The debug mode argument for command line pass-through. - public static let debugArgument: String = "debug" - - /// The strict mode argument for command-line pass-through. - public static let strictArgument: String = "strict" - - /// The validate-only mode argument for command-line pass-through. - public static let validateArgument: String = "validate" - - /// The separator indicating that next come regex options. - public static let regexOptionsSeparator: String = #"\"# - - /// Hint that the case insensitive option should be active on a Regex. - public static let caseInsensitiveRegexOption: String = "i" - - /// Hint that the case dot matches newline option should be active on a Regex. - public static let dotMatchesNewlinesRegexOption: String = "m" - - /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. - public static let newlinesRequiredForDiffing: Int = 3 -} diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift deleted file mode 100644 index 10cb036..0000000 --- a/Sources/AnyLint/FilesSearch.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation - -/// 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) - 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() - } - - 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) - } - - cachedFilePaths[searchOptions] = filePaths - return filePaths - } -} diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift deleted file mode 100644 index 629b064..0000000 --- a/Sources/AnyLint/Lint.swift +++ /dev/null @@ -1,267 +0,0 @@ -import Foundation - -/// 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'. - /// - 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, - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [], - includeFilters: [Regex] = [#".*"#], - excludeFilters: [Regex] = [], - autoCorrectReplacement: String? = nil, - autoCorrectExamples: [AutoCorrection] = [], - repeatIfAutoCorrected: Bool = false - ) throws { - 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 - } - - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters - ) - - let violations = try FileContentsChecker( - checkInfo: checkInfo, - regex: regex, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: autoCorrectReplacement, - repeatIfAutoCorrected: repeatIfAutoCorrected - ) - .performCheck() - - Statistics.shared.found(violations: violations, in: checkInfo) - } - - /// 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 { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - validateParameterCombinations( - checkInfo: checkInfo, - autoCorrectReplacement: autoCorrectReplacement, - autoCorrectExamples: autoCorrectExamples, - violateIfNoMatchesFound: violateIfNoMatchesFound - ) - - if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( - checkInfo: checkInfo, - examples: autoCorrectExamples, - regex: regex, - autocorrectReplacement: autoCorrectReplacement - ) - } - - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - within: fileManager.currentDirectoryPath, - includeFilters: includeFilters, - excludeFilters: excludeFilters - ) - - let violations = try FilePathsChecker( - checkInfo: checkInfo, - regex: regex, - filePathsToCheck: filePathsToCheck, - autoCorrectReplacement: autoCorrectReplacement, - violateIfNoMatchesFound: violateIfNoMatchesFound - ) - .performCheck() - - Statistics.shared.found(violations: violations, in: checkInfo) - } - - /// 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) -> [Violation]) { - guard !Options.validateOnly else { - Statistics.shared.executedChecks.append(checkInfo) - return - } - - Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) - } - - /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. - public static func logSummaryAndExit( - arguments: [String] = [], - afterPerformingChecks checksToPerform: () throws -> Void = {} - ) throws { - let failOnWarnings = arguments.contains(Constants.strictArgument) - let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) - - if targetIsXcode { - log = Logger(outputType: .xcode) - } - - log.logDebugLevel = arguments.contains(Constants.debugArgument) - Options.validateOnly = arguments.contains(Constants.validateArgument) - - try checksToPerform() - - guard !Options.validateOnly else { - Statistics.shared.logValidationSummary() - log.exit(status: .success) - return // only reachable in unit tests - } - - Statistics.shared.logCheckSummary() - - 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, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { - if matchingExamples.isFilled { - log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) - } - - for example in matchingExamples { - if !regex.matches(example) { - log.message( - "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { - if nonMatchingExamples.isFilled { - log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) - } - - for example in nonMatchingExamples { - if regex.matches(example) { - log.message( - "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", - level: .error - ) - log.exit(status: .failure) - } - } - } - - static func validateAutocorrectsAll( - checkInfo: CheckInfo, - examples: [AutoCorrection], - regex: Regex, - autocorrectReplacement: String - ) { - if examples.isFilled { - log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) - } - - for autocorrect in examples { - let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) - if autocorrected != autocorrect.after { - log.message( - """ - Autocorrecting example for \(checkInfo.id) did not result in expected output. - Before: '\(autocorrect.before.showWhitespacesAndNewlines())' - After: '\(autocorrected.showWhitespacesAndNewlines())' - Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' - """, - level: .error - ) - 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 - ) - } - - 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 deleted file mode 100644 index 9f47c30..0000000 --- a/Sources/AnyLint/Options.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -enum Options { - static var validateOnly: Bool = false -} diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift deleted file mode 100644 index b459dea..0000000 --- a/Sources/AnyLint/Statistics.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation - -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 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) - } - - /// 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() { - if executedChecks.isEmpty { - log.message("No checks found to perform.", level: .warning) - } - else if violationsBySeverity.values.contains(where: { $0.isFilled }) { - switch log.outputType { - case .console, .test: - logViolationsToConsole() - - case .xcode: - showViolationsInXcode() - } - } - else { - log.message( - "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", - level: .success - ) - } - } - - 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.. " + 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/Checkers/Checker.swift b/Sources/Checkers/Checker.swift new file mode 100644 index 0000000..5997fe5 --- /dev/null +++ b/Sources/Checkers/Checker.swift @@ -0,0 +1,7 @@ +import Foundation +import Core + +/// Defines how a checker algorithm behaves to produce violations results. +public protocol Checker { + func performCheck() throws -> [Violation] +} diff --git a/Sources/Checkers/CustomScriptsChecker.swift b/Sources/Checkers/CustomScriptsChecker.swift new file mode 100644 index 0000000..80d8418 --- /dev/null +++ b/Sources/Checkers/CustomScriptsChecker.swift @@ -0,0 +1,23 @@ +import Foundation +import Core + +/// The checker for the `CustomScripts` configuration. Runs custom commands and checks their output & status for determining violations. +public struct CustomScriptsChecker { + /// 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 severity level for the report in case the check fails. + public let severity: Severity + + /// The script to execute. Expected output can be AnyLint standardized JSON or, if not, will use exit code to determine failed or not. + public let script: String +} + +extension CustomScriptsChecker: Checker { + public func performCheck() throws -> [Violation] { + fatalError() // TODO: [cg_2021-07-02] not yet implemented + } +} diff --git a/Sources/AnyLint/Extensions/ArrayExt.swift b/Sources/Checkers/Extensions/ArrayExt.swift similarity index 100% rename from Sources/AnyLint/Extensions/ArrayExt.swift rename to Sources/Checkers/Extensions/ArrayExt.swift diff --git a/Sources/AnyLint/Extensions/RegexExt.swift b/Sources/Checkers/Extensions/RegexExt.swift similarity index 85% rename from Sources/AnyLint/Extensions/RegexExt.swift rename to Sources/Checkers/Extensions/RegexExt.swift index b69cf1c..87fd9ad 100644 --- a/Sources/AnyLint/Extensions/RegexExt.swift +++ b/Sources/Checkers/Extensions/RegexExt.swift @@ -1,6 +1,22 @@ import Foundation +import Core extension Regex: ExpressibleByStringLiteral { + /// Constants to reference across the project. + enum Constants { + /// The separator indicating that next come regex options. + static let regexOptionsSeparator: String = #"\"# + + /// Hint that the case insensitive option should be active on a Regex. + static let caseInsensitiveRegexOption: String = "i" + + /// Hint that the case dot matches newline option should be active on a Regex. + static let dotMatchesNewlinesRegexOption: String = "m" + + // /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. + // static let newlinesRequiredForDiffing: Int = 3 + } + public init( stringLiteral value: String ) { diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift new file mode 100644 index 0000000..f0c28c3 --- /dev/null +++ b/Sources/Checkers/FileContentsChecker.swift @@ -0,0 +1,135 @@ +import Foundation +import Core + +/// The checker for the `FileContents` configuration. Runs regex-based chacks on contents of files. +public struct FileContentsChecker { + /// 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 severity level for the report in case the check fails. + public let severity: Severity + + /// The regular expression to use. + public let regex: Regex + + /// The file paths to check. + public let filePathsToCheck: [String] + + /// The optional replacement template string for replacing using capture groups. + public let autoCorrectReplacement: String? + + /// If set to `true`, the contents check will be repeated until there are no longer any changes when applying autocorrection. + public let repeatIfAutoCorrected: Bool +} + +extension FileContentsChecker: Checker { + public func performCheck() throws -> [Violation] { + fatalError() // TODO: [cg_2021-07-02] not yet implemented + // 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 = fileContents.locationInfo(of: match.range.lowerBound) + // + // 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 + // ) + // } + // + // 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, + // filePathsToCheck: filePathsToReCheck, + // autoCorrectReplacement: autoCorrectReplacement, + // repeatIfAutoCorrected: repeatIfAutoCorrected + // ) + // .performCheck() + // violations.append(contentsOf: violationsOnRechecks) + // } + // + // return violations + } +} diff --git a/Sources/Checkers/FilePathsChecker.swift b/Sources/Checkers/FilePathsChecker.swift new file mode 100644 index 0000000..073a5f3 --- /dev/null +++ b/Sources/Checkers/FilePathsChecker.swift @@ -0,0 +1,75 @@ +import Foundation +import Core + +/// The checker for the `FilePaths` configuration. Runs regex-based chacks on file paths. +public struct FilePathsChecker { + /// 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 severity level for the report in case the check fails. + public let severity: Severity + + /// The regular expression to use. + public let regex: Regex + + /// The file paths to check. + public let filePathsToCheck: [String] + + /// The optional replacement template string for replacing using capture groups. + public let autoCorrectReplacement: String? + + /// If set to `true`, then the check will violate if no matches found, otherwise it will report every match as a violation. + public let violateIfNoMatchesFound: Bool +} + +extension FilePathsChecker: Checker { + public func performCheck() throws -> [Violation] { + fatalError() // TODO: [cg_2021-07-02] not yet implemented + // 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) + // } + // + // log.message("Reporting violation for \(checkInfo) in file \(filePath) ...", level: .debug) + // violations.append( + // Violation( + // checkInfo: checkInfo, + // filePath: filePath, + // locationInfo: nil, + // appliedAutoCorrection: appliedAutoCorrection + // ) + // ) + // } + // + // Statistics.shared.checkedFiles(at: filePathsToCheck) + // } + // + // return violations + } +} diff --git a/Sources/AnyLint/Extensions/FileManagerExt.swift b/Sources/Checkers/Helpers/FileManagerExt.swift similarity index 79% rename from Sources/AnyLint/Extensions/FileManagerExt.swift rename to Sources/Checkers/Helpers/FileManagerExt.swift index 1c21106..fe3a6cc 100644 --- a/Sources/AnyLint/Extensions/FileManagerExt.swift +++ b/Sources/Checkers/Helpers/FileManagerExt.swift @@ -1,4 +1,5 @@ import Foundation +import Core extension FileManager { /// Moves a file from one path to another, making sure that all directories are created and no files are overwritten. @@ -38,16 +39,3 @@ extension FileManager { FilesSearch.shared.invalidateCache() } } - -extension FileManager { - /// 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 - } -} diff --git a/Sources/Checkers/Helpers/FilesSearch.swift b/Sources/Checkers/Helpers/FilesSearch.swift new file mode 100644 index 0000000..88f7a03 --- /dev/null +++ b/Sources/Checkers/Helpers/FilesSearch.swift @@ -0,0 +1,121 @@ +import Foundation + +/// 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] { + fatalError() // TODO: [cg_2021-07-02] not yet implemented + // 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 + // } + // + // // skip if any exclude filter applies + // if excludeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { + // if !isRegularFilePath { + // enumerator.skipDescendants() + // } + // + // continue + // } + // + // // skip hidden files and directories + // #if os(Linux) + // if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { + // if !isRegularFilePath { + // enumerator.skipDescendants() + // } + // + // 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 + } +} diff --git a/Sources/AnyLint/Regex.swift b/Sources/Checkers/Helpers/Regex.swift similarity index 100% rename from Sources/AnyLint/Regex.swift rename to Sources/Checkers/Helpers/Regex.swift diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift new file mode 100644 index 0000000..9d6334d --- /dev/null +++ b/Sources/Checkers/Lint.swift @@ -0,0 +1,267 @@ +//import Foundation +// +///// 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'. +// /// - 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, +// matchingExamples: [String] = [], +// nonMatchingExamples: [String] = [], +// includeFilters: [Regex] = [#".*"#], +// excludeFilters: [Regex] = [], +// autoCorrectReplacement: String? = nil, +// autoCorrectExamples: [AutoCorrection] = [], +// repeatIfAutoCorrected: Bool = false +// ) throws { +// 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 +// } +// +// let filePathsToCheck: [String] = FilesSearch.shared.allFiles( +// within: fileManager.currentDirectoryPath, +// includeFilters: includeFilters, +// excludeFilters: excludeFilters +// ) +// +// let violations = try FileContentsChecker( +// checkInfo: checkInfo, +// regex: regex, +// filePathsToCheck: filePathsToCheck, +// autoCorrectReplacement: autoCorrectReplacement, +// repeatIfAutoCorrected: repeatIfAutoCorrected +// ) +// .performCheck() +// +// Statistics.shared.found(violations: violations, in: checkInfo) +// } +// +// /// 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 { +// validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) +// validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) +// validateParameterCombinations( +// checkInfo: checkInfo, +// autoCorrectReplacement: autoCorrectReplacement, +// autoCorrectExamples: autoCorrectExamples, +// violateIfNoMatchesFound: violateIfNoMatchesFound +// ) +// +// if let autoCorrectReplacement = autoCorrectReplacement { +// validateAutocorrectsAll( +// checkInfo: checkInfo, +// examples: autoCorrectExamples, +// regex: regex, +// autocorrectReplacement: autoCorrectReplacement +// ) +// } +// +// guard !Options.validateOnly else { +// Statistics.shared.executedChecks.append(checkInfo) +// return +// } +// +// let filePathsToCheck: [String] = FilesSearch.shared.allFiles( +// within: fileManager.currentDirectoryPath, +// includeFilters: includeFilters, +// excludeFilters: excludeFilters +// ) +// +// let violations = try FilePathsChecker( +// checkInfo: checkInfo, +// regex: regex, +// filePathsToCheck: filePathsToCheck, +// autoCorrectReplacement: autoCorrectReplacement, +// violateIfNoMatchesFound: violateIfNoMatchesFound +// ) +// .performCheck() +// +// Statistics.shared.found(violations: violations, in: checkInfo) +// } +// +// /// 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) -> [Violation]) { +// guard !Options.validateOnly else { +// Statistics.shared.executedChecks.append(checkInfo) +// return +// } +// +// Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) +// } +// +// /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. +// public static func logSummaryAndExit( +// arguments: [String] = [], +// afterPerformingChecks checksToPerform: () throws -> Void = {} +// ) throws { +// let failOnWarnings = arguments.contains(Constants.strictArgument) +// let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) +// +// if targetIsXcode { +// log = Logger(outputType: .xcode) +// } +// +// log.logDebugLevel = arguments.contains(Constants.debugArgument) +// Options.validateOnly = arguments.contains(Constants.validateArgument) +// +// try checksToPerform() +// +// guard !Options.validateOnly else { +// Statistics.shared.logValidationSummary() +// log.exit(status: .success) +// return // only reachable in unit tests +// } +// +// Statistics.shared.logCheckSummary() +// +// 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, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { +// if matchingExamples.isFilled { +// log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) +// } +// +// for example in matchingExamples { +// if !regex.matches(example) { +// log.message( +// "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", +// level: .error +// ) +// log.exit(status: .failure) +// } +// } +// } +// +// static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { +// if nonMatchingExamples.isFilled { +// log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) +// } +// +// for example in nonMatchingExamples { +// if regex.matches(example) { +// log.message( +// "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", +// level: .error +// ) +// log.exit(status: .failure) +// } +// } +// } +// +// static func validateAutocorrectsAll( +// checkInfo: CheckInfo, +// examples: [AutoCorrection], +// regex: Regex, +// autocorrectReplacement: String +// ) { +// if examples.isFilled { +// log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) +// } +// +// for autocorrect in examples { +// let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) +// if autocorrected != autocorrect.after { +// log.message( +// """ +// Autocorrecting example for \(checkInfo.id) did not result in expected output. +// Before: '\(autocorrect.before.showWhitespacesAndNewlines())' +// After: '\(autocorrected.showWhitespacesAndNewlines())' +// Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' +// """, +// level: .error +// ) +// 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 +// ) +// } +// +// 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/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift new file mode 100644 index 0000000..5baccd9 --- /dev/null +++ b/Sources/Core/AutoCorrection.swift @@ -0,0 +1,98 @@ +import Foundation + +/// Information about an autocorrection. +public struct AutoCorrection: Codable { + /// The matching text before applying the autocorrection. + public let before: String + + /// The matching text after applying the autocorrection. + public let after: String + + // var appliedMessageLines: [String] { + // if useDiffOutput, #available(OSX 10.15, *) { + // var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] + // + // let beforeLines = before.components(separatedBy: .newlines) + // let afterLines = after.components(separatedBy: .newlines) + // + // for difference in afterLines.difference(from: beforeLines).sorted() { + // switch difference { + // case let .insert(offset, element, _): + // lines.append("+ [L\(offset + 1)] \(element)".green) + // + // case let .remove(offset, element, _): + // lines.append("- [L\(offset + 1)] \(element)".red) + // } + // } + // + // return lines + // } + // else { + // return [ + // "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) +// } +//} +// +//// 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 +// } +// } +//} diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/CheckInfo.swift new file mode 100644 index 0000000..a8e6425 --- /dev/null +++ b/Sources/Core/CheckInfo.swift @@ -0,0 +1,88 @@ +import Foundation + +/// 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 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 + + /// Initializes a new info object for the lint check. + public init( + id: String, + hint: String, + severity: Severity = .error + ) { + self.id = id + self.hint = hint + self.severity = severity + } +} + +//extension CheckInfo: Hashable { +// public func hash(into hasher: inout Hasher) { +// hasher.combine(id) +// } +//} +// +//extension CheckInfo: CustomStringConvertible { +// public var description: String { +// "check '\(id)'" +// } +//} +// +//extension CheckInfo: ExpressibleByStringLiteral { +// 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]! +// +// 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": #".*$"#, +// ] +// +// 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]! +// +// self = CheckInfo(id: id, hint: hint) +// } +// } +//} diff --git a/Sources/AnyLint/Extensions/CollectionExt.swift b/Sources/Core/Extensions/CollectionExt.swift similarity index 100% rename from Sources/AnyLint/Extensions/CollectionExt.swift rename to Sources/Core/Extensions/CollectionExt.swift diff --git a/Sources/Core/Extensions/FileManagerExt.swift b/Sources/Core/Extensions/FileManagerExt.swift new file mode 100644 index 0000000..e9fdffb --- /dev/null +++ b/Sources/Core/Extensions/FileManagerExt.swift @@ -0,0 +1,14 @@ +import Foundation + +extension FileManager { + /// 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 + } +} diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/Core/Extensions/StringExt.swift similarity index 72% rename from Sources/AnyLint/Extensions/StringExt.swift rename to Sources/Core/Extensions/StringExt.swift index 92b5013..564d261 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/Core/Extensions/StringExt.swift @@ -1,8 +1,5 @@ import Foundation -/// `Regex` is a swifty regex engine built on top of the NSRegularExpression api. -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) @@ -17,15 +14,18 @@ extension String { return (line: prefixLines.count, charInLine: charInLine) } - func showNewlines() -> String { + /// Returns a string that shows newlines as `\n`. + public func showNewlines() -> String { components(separatedBy: .newlines).joined(separator: #"\n"#) } - func showWhitespaces() -> String { + /// Returns a string that shows whitespaces as `␣`. + public func showWhitespaces() -> String { components(separatedBy: .whitespaces).joined(separator: "␣") } - func showWhitespacesAndNewlines() -> String { + /// Returns a string that shows newlines as `\n` and whitespaces as `␣`. + public func showWhitespacesAndNewlines() -> String { showNewlines().showWhitespaces() } } @@ -42,20 +42,20 @@ extension String { /// 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 + guard !self.starts(with: FileManager.default.currentDirectoryUrl.path) else { return self } + return FileManager.default.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: "") + guard self.starts(with: FileManager.default.currentDirectoryUrl.path) else { return self } + return replacingOccurrences(of: FileManager.default.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 } + guard url.pathComponents.count > 1 else { return FileManager.default.currentDirectoryPath } return url.deletingLastPathComponent().absoluteString } diff --git a/Sources/AnyLint/Extensions/URLExt.swift b/Sources/Core/Extensions/URLExt.swift similarity index 57% rename from Sources/AnyLint/Extensions/URLExt.swift rename to Sources/Core/Extensions/URLExt.swift index c2902ee..53e3f70 100644 --- a/Sources/AnyLint/Extensions/URLExt.swift +++ b/Sources/Core/Extensions/URLExt.swift @@ -3,6 +3,6 @@ import Foundation extension URL { /// Returns the relative path of from the current path. public var relativePathFromCurrent: String { - String(path.replacingOccurrences(of: fileManager.currentDirectoryPath, with: "").dropFirst()) + String(path.replacingOccurrences(of: FileManager.default.currentDirectoryPath, with: "").dropFirst()) } } diff --git a/Sources/AnyLint/Logger.swift b/Sources/Core/Logger.swift similarity index 94% rename from Sources/AnyLint/Logger.swift rename to Sources/Core/Logger.swift index 59259c0..72eb62d 100644 --- a/Sources/AnyLint/Logger.swift +++ b/Sources/Core/Logger.swift @@ -1,4 +1,8 @@ import Foundation +import Rainbow + +/// Shortcut to access the `Logger` within this project. +public var log = Logger(outputType: .console) /// Helper to log output to console or elsewhere. public final class Logger { @@ -145,10 +149,10 @@ public final class Logger { /// - 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)") + print("\(location) \(level.rawValue): AnyLint: \(message)") } else { - print("\(level.rawValue): \(Constants.toolName): \(message)") + print("\(level.rawValue): AnyLint: \(message)") } } diff --git a/Sources/Core/Severity.swift b/Sources/Core/Severity.swift index fa6f8e6..41ac362 100644 --- a/Sources/Core/Severity.swift +++ b/Sources/Core/Severity.swift @@ -11,22 +11,22 @@ public enum Severity: String, CaseIterable { /// Use for checks that probably are problematic. case error -// -// static func from(string: String) -> Severity? { -// switch string { -// case "info", "i": -// return .info -// -// case "warning", "w": -// return .warning -// -// case "error", "e": -// return .error -// -// default: -// return nil -// } -// } + // + // static func from(string: String) -> Severity? { + // switch string { + // case "info", "i": + // return .info + // + // case "warning", "w": + // return .warning + // + // case "error", "e": + // return .error + // + // default: + // return nil + // } + // } } // //extension Severity: Comparable { @@ -40,4 +40,3 @@ public enum Severity: String, CaseIterable { // } // } //} - diff --git a/Sources/AnyLint/TestHelper.swift b/Sources/Core/TestHelper.swift similarity index 100% rename from Sources/AnyLint/TestHelper.swift rename to Sources/Core/TestHelper.swift diff --git a/Sources/AnyLint/Violation.swift b/Sources/Core/Violation.swift similarity index 100% rename from Sources/AnyLint/Violation.swift rename to Sources/Core/Violation.swift diff --git a/Sources/Reporting/Statistics.swift b/Sources/Reporting/Statistics.swift new file mode 100644 index 0000000..7a4e5d6 --- /dev/null +++ b/Sources/Reporting/Statistics.swift @@ -0,0 +1,154 @@ +//import Foundation +// +//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 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) +// } +// +// /// 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() { +// if executedChecks.isEmpty { +// log.message("No checks found to perform.", level: .warning) +// } +// else if violationsBySeverity.values.contains(where: { $0.isFilled }) { +// switch log.outputType { +// case .console, .test: +// logViolationsToConsole() +// +// case .xcode: +// showViolationsInXcode() +// } +// } +// else { +// log.message( +// "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", +// level: .success +// ) +// } +// } +// +// 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.. " + 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) +// ) +// } +// } +// } +//} From fc6c0e3f0fbec83559fa726833eb6c789070c4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 2 Jul 2021 14:16:54 +0200 Subject: [PATCH 05/37] Implement basic init command with blank template --- Package.resolved | 9 +++ Package.swift | 4 ++ .../BlankTemplate.swift | 62 ------------------ .../ConfigurationTemplate.swift | 5 -- Sources/Commands/InitCommand.swift | 30 ++++++++- Sources/Commands/LintCommand.swift | 4 +- Sources/Configuration/Template.swift | 64 +++++++++++++++++++ 7 files changed, 107 insertions(+), 71 deletions(-) delete mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift delete mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift diff --git a/Package.resolved b/Package.resolved index 27fbd81..3e1246b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "version": "4.0.0" } }, + { + "package": "ShellOut", + "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", + "state": { + "branch": null, + "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version": "2.3.0" + } + }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser.git", diff --git a/Package.swift b/Package.swift index 52244f6..824c676 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,9 @@ let package = Package( // Straightforward, type-safe argument parsing for Swift .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.4.3"), + // Easily run shell commands from a Swift script or command line tool + .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.3.0"), + // A Sweet and Swifty YAML parser. .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.6"), ], @@ -38,6 +41,7 @@ let package = Package( "Configuration", "Core", "Reporting", + .product(name: "ShellOut", package: "ShellOut"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift deleted file mode 100644 index ce91b73..0000000 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -// swiftlint:disable function_body_length - -enum BlankTemplate: ConfigurationTemplate { - static func fileContents() -> String { - #""" - FileContents: - - id: Readme - hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' - regex: '^README\.md$' - violateIfNoMatchesFound: true - matchingExamples: ['README.md'] - nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] - - - id: ReadmeTopLevelTitle - hint: 'The README.md file should only contain a single top level title.' - regex: '(^|\n)#[^#](.*\n)*\n#[^#]' - includeFilter: ['^README\.md$'] - matchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - nonMatchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle - - - id: ReadmeTypoLicense - hint: 'ReadmeTypoLicense: Misspelled word `license`.' - regex: '([\s#]L|l)isence([\s\.,:;])' - matchingExamples: [' lisence:', '## Lisence\n'] - nonMatchingExamples: [' license:', '## License\n'] - includeFilters: ['^README\.md$'] - autoCorrectReplacement: '$1icense$2' - autoCorrectExamples: - - { before: ' lisence:', after: ' license:' } - - { before: '## Lisence\n', after: '## License\n' } - - FilePaths: - - id: 'ReadmePath' - hint: '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' } - - """# - } -} diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift deleted file mode 100644 index c185166..0000000 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -protocol ConfigurationTemplate { - static func fileContents() -> String -} diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift index e649026..95f23c4 100644 --- a/Sources/Commands/InitCommand.swift +++ b/Sources/Commands/InitCommand.swift @@ -2,6 +2,7 @@ import ArgumentParser import Configuration import Core import Foundation +import ShellOut struct InitCommand: ParsableCommand { static var configuration: CommandConfiguration = .init( @@ -19,10 +20,35 @@ struct InitCommand: ParsableCommand { name: .shortAndLong, help: "Path to the new config file to initialize it at." ) - var path: String = URL(fileURLWithPath: ".").appendingPathComponent("anylint.yml").path + var path: String = FileManager.default.currentDirectoryUrl.appendingPathComponent("anylint.yml").path mutating func run() throws { - // TODO: [cg_2021-06-28] not yet implemented + // if the specified path is a directory, assume the user wants the default file name + if FileManager.default.fileExistsAndIsDirectory(atPath: path) { + path = path.appendingPathComponent("anylint.yml") + } + + guard !FileManager.default.fileExists(atPath: path) else { + log.message("Configuration file already exists at path '\(path)'.", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + log.message("Making sure config file directory exists ...", level: .info) + try shellOut(to: "mkdir", arguments: ["-p", path.parentDirectoryPath]) + + log.message("Creating config file using template '\(template.rawValue)' ...", level: .info) + FileManager.default.createFile( + atPath: path, + contents: template.fileContents.data(using: .utf8), + attributes: nil + ) + + log.message("Making config file executable ...", level: .info) + try shellOut(to: "chmod", arguments: ["+x", path]) + + log.message("Successfully created config file at \(path)", level: .success) + } } diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index c0830f8..6f66fd2 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -14,13 +14,13 @@ struct LintCommand: ParsableCommand { parsing: .upToNextOption, help: .init("The path(s) to run the checks from.", valueName: "path") ) - var paths: [String] = [URL(fileURLWithPath: ".").path] + var paths: [String] = [FileManager.default.currentDirectoryUrl.path] @Option( name: .shortAndLong, help: .init("Path to the config file to execute.", valueName: "path") ) - var config: String = URL(fileURLWithPath: ".").appendingPathComponent("anylint.yml").path + var config: String = FileManager.default.currentDirectoryUrl.appendingPathComponent("anylint.yml").path @Option( name: .shortAndLong, diff --git a/Sources/Configuration/Template.swift b/Sources/Configuration/Template.swift index 73ed0c1..c71e815 100644 --- a/Sources/Configuration/Template.swift +++ b/Sources/Configuration/Template.swift @@ -7,4 +7,68 @@ public enum Template: String, CaseIterable { /// The template with some useful checks setup for open source projects. case openSource + + /// Returns the file contents for the chosen template. + public var fileContents: String { + switch self { + case .blank: + return #""" + FileContents: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + + - id: ReadmeTopLevelTitle + hint: 'The README.md file should only contain a single top level title.' + regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + includeFilter: ['^README\.md$'] + matchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + nonMatchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + + - id: ReadmeTypoLicense + hint: 'ReadmeTypoLicense: Misspelled word `license`.' + regex: '([\s#]L|l)isence([\s\.,:;])' + matchingExamples: [' lisence:', '## Lisence\n'] + nonMatchingExamples: [' license:', '## License\n'] + includeFilters: ['^README\.md$'] + autoCorrectReplacement: '$1icense$2' + autoCorrectExamples: + - { before: ' lisence:', after: ' license:' } + - { before: '## Lisence\n', after: '## License\n' } + + FilePaths: + - id: 'ReadmePath' + hint: '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' } + + """# + + case .openSource: + fatalError() // TODO: [cg_2021-07-02] not yet implemented + } + } } From 8e474e35dd517a19c39aa82f3e2e384eafa9dc16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 2 Jul 2021 14:52:03 +0200 Subject: [PATCH 06/37] [README.md] Add YAML cheat sheet --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/README.md b/README.md index a5be570..f87af9e 100644 --- a/README.md +++ b/README.md @@ -500,6 +500,91 @@ Here are some **advanced Regex features** you might want to use or learn more ab For example, consider a regex violating if there's an empty line after an opening curly brace like so: `{\n\s*\n\s*\S`. This would match the lines of `func do() {\n\n return 5}`, but what you actually want is it to start matching on the empty newline like so: `(?<={\n)\s*\n\s*\S`. See also [#3](https://github.com/Flinesoft/AnyLint/issues/3) + +## YAML Cheat Sheet + +Please be aware that in YAML indentation (whitespaces) and newlines are actually important. +Natively supported types are: String, Integer, Float, Bool and Date. + +**Strings** (unlike in most other languages) don't need to be put between quotes, but can: +```yaml +string1: This is without quotes. +string2: 'This is with single quotes.' +string3: "This is with double quotes." +``` + +**Multi-line strings** can be written by specifying `|` and then a newline: +```yaml +multiline1: | + This is a multi line string. + Newlines are going to be preserved. + By default, only one trailing newline is kept. +``` + +An additional `+` or `-` specified what to do with trailing newlines: +```yaml +multiline2: |+ + This will make sure both trailing newlines are kept (ends with ".\n\n"). + + +multiline3: |- + This will ignore any trailing newlines and + will end with the lest non-newline character (the following dot in this case -->). + + +``` + +**Arrays** can be written in two ways: +```yaml +array1: [1, 2, 3] +array2: + - 1 + - 2 + - 3 +``` + +**Dictionaries**, too, can be written in two similar ways: +```yaml +array1: { key1: 1, key2: 2, key3: 3 } +array2: + - 1 + - 2 + - 3 +``` + +Dictionaries and Arrays can be nested indefinitely. Dictionaries within Arrays are denoted with one `-` before the keys: +```yaml +level1dict: + level2dict: + level3dict1: + leaf1: foo + leaf2: bar + level3dict2: + array1: [a, b, c] + array2: + - [a, b, c] + - [x, y, z] + level3array: + - level4dict1: a + level4dict2: b + level4dict3: c +``` + +You can also reuse Dictionaries defined earlier (at the top) down the file via `<<: *`: +```yaml +swiftFileFilters: + includeFilters: ['.*\.swift'] + excludeFilters: ['.*\.generated\.swift'] + +FileContents: + - id: Check1 + regex: 'a*b*' + <<: *swiftFileFilters + + - id: Check2 + regex: 'c*d*' + <<: *swiftFileFilters +``` ## Donation From 3e5adcae63f325f099d24c556087b90f3b261b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 8 Jul 2021 16:42:09 +0200 Subject: [PATCH 07/37] [Incomplete] Move all previous files to new places --- Package.resolved | 9 + Package.swift | 12 +- .../AnyLintCLI/Commands/SingleCommand.swift | 96 ---- Sources/AnyLintCLI/Globals/CLIConstants.swift | 7 - .../AnyLintCLI/Globals/ValidateOrFail.swift | 14 - .../AnyLintCLI/Models/LintConfiguration.swift | 38 -- Sources/AnyLintCLI/Tasks/InitTask.swift | 42 -- Sources/AnyLintCLI/Tasks/LintTask.swift | 82 --- Sources/AnyLintCLI/Tasks/TaskHandler.swift | 5 - Sources/AnyLintCLI/Tasks/VersionTask.swift | 9 - Sources/AnyLintCLI/main.swift | 4 - Sources/Checkers/Extensions/ArrayExt.swift | 1 + Sources/Checkers/Extensions/RegexExt.swift | 4 +- Sources/Checkers/FileContentsChecker.swift | 2 +- Sources/Checkers/FilePathsChecker.swift | 2 +- Sources/Checkers/Helpers/FileManagerExt.swift | 4 +- Sources/Checkers/Helpers/FilesSearch.swift | 7 +- Sources/Checkers/Lint.swift | 542 +++++++++--------- Sources/Commands/AnyLint.swift | 2 +- Sources/Commands/InitCommand.swift | 6 +- Sources/Commands/LintCommand.swift | 88 ++- Sources/Configuration/LintConfiguration.swift | 149 +++++ Sources/Core/AutoCorrection.swift | 2 +- Sources/Core/CheckInfo.swift | 16 +- Sources/Core/Extensions/StringExt.swift | 11 +- Sources/Core/FileLocation.swift | 19 + Sources/Core/Logger.swift | 97 +--- .../{Reporting => Core}/OutputFormat.swift | 0 .../{Checkers/Helpers => Core}/Regex.swift | 0 Sources/Core/Severity.swift | 28 +- Sources/Core/TestHelper.swift | 22 - Sources/Core/Violation.swift | 17 +- Sources/Reporting/LintResults.swift | 139 +++++ 33 files changed, 759 insertions(+), 717 deletions(-) delete mode 100644 Sources/AnyLintCLI/Commands/SingleCommand.swift delete mode 100644 Sources/AnyLintCLI/Globals/CLIConstants.swift delete mode 100644 Sources/AnyLintCLI/Globals/ValidateOrFail.swift delete mode 100644 Sources/AnyLintCLI/Models/LintConfiguration.swift delete mode 100644 Sources/AnyLintCLI/Tasks/InitTask.swift delete mode 100644 Sources/AnyLintCLI/Tasks/LintTask.swift delete mode 100644 Sources/AnyLintCLI/Tasks/TaskHandler.swift delete mode 100644 Sources/AnyLintCLI/Tasks/VersionTask.swift delete mode 100644 Sources/AnyLintCLI/main.swift create mode 100644 Sources/Configuration/LintConfiguration.swift create mode 100644 Sources/Core/FileLocation.swift rename Sources/{Reporting => Core}/OutputFormat.swift (100%) rename Sources/{Checkers/Helpers => Core}/Regex.swift (100%) delete mode 100644 Sources/Core/TestHelper.swift create mode 100644 Sources/Reporting/LintResults.swift diff --git a/Package.resolved b/Package.resolved index 3e1246b..859b046 100644 --- a/Package.resolved +++ b/Package.resolved @@ -28,6 +28,15 @@ "version": "0.4.3" } }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "d45e63421d3dff834949ac69d3c37691e994bd69", + "version": "0.0.3" + } + }, { "package": "Yams", "repositoryURL": "https://github.com/jpsim/Yams.git", diff --git a/Package.swift b/Package.swift index 824c676..dfa877d 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,9 @@ let package = Package( // Straightforward, type-safe argument parsing for Swift .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.4.3"), + // Commonly used data structures for Swift` + .package(url: "https://github.com/apple/swift-collections.git", from: "0.0.3"), + // Easily run shell commands from a Swift script or command line tool .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.3.0"), @@ -34,10 +37,17 @@ let package = Package( .product(name: "Yams", package: "Yams"), ] ), - .target(name: "Reporting", dependencies: ["Core"]), + .target( + name: "Reporting", + dependencies: [ + "Core", + .product(name: "OrderedCollections", package: "swift-collections"), + ] + ), .executableTarget( name: "Commands", dependencies: [ + "Checkers", "Configuration", "Core", "Reporting", diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift deleted file mode 100644 index 6478b69..0000000 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Foundation - -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 - - @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: debug, failOnWarnings: strict, validateOnly: validate) - .perform() - } - catch LintTask.LintError.configFileFailed { - anyConfigFileFailed = true - } - } - exit(anyConfigFileFailed ? EXIT_FAILURE : EXIT_SUCCESS) - } -} diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift deleted file mode 100644 index 83a5327..0000000 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -enum CLIConstants { - static let commandName: String = "anylint" - static let defaultConfigFileName: String = "anylint.yml" - static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") -} diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift deleted file mode 100644 index d7bd936..0000000 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -enum ValidateOrFail { - static func configFileExists(at configFilePath: String) throws { - guard fileManager.fileExists(atPath: configFilePath) else { - log.message( - "No configuration file found at \(configFilePath) – consider running `\(CLIConstants.commandName) --init` with a template.", - level: .error - ) - log.exit(status: .failure) - return // only reachable in unit tests - } - } -} diff --git a/Sources/AnyLintCLI/Models/LintConfiguration.swift b/Sources/AnyLintCLI/Models/LintConfiguration.swift deleted file mode 100644 index 8eedc74..0000000 --- a/Sources/AnyLintCLI/Models/LintConfiguration.swift +++ /dev/null @@ -1,38 +0,0 @@ -import AnyLint -import Foundation - -struct LintConfiguration: Codable { - enum CodingKeys: String, CodingKey { - case checkFileContents = "CheckFileContents" - case checkFilePaths = "CheckFilePaths" - } - - let checkFileContents: [CheckFileContentsConfiguration] - let checkFilePaths: [CheckFilePathsConfiguration] -} - -struct CheckFileContentsConfiguration: Codable { - let id: String - let hint: String - let regex: String - let matchingExamples: [String]? - let nonMatchingExamples: [String]? - let includeFilters: [Regex]? - let excludeFilters: [Regex]? - let autoCorrectReplacement: String? - let autoCorrectExamples: [AutoCorrection]? - let repeatIfAutoCorrected: Bool? -} - -struct CheckFilePathsConfiguration: Codable { - let id: String - let hint: String - let regex: String - let matchingExamples: [String]? - let nonMatchingExamples: [String]? - let includeFilters: [Regex]? - let excludeFilters: [Regex]? - let autoCorrectReplacement: String? - let autoCorrectExamples: [AutoCorrection]? - let violateIfNoMatchesFound: Bool? -} diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift deleted file mode 100644 index 328ba64..0000000 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -struct InitTask { - 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 - } - - 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 deleted file mode 100644 index 7fba56f..0000000 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ /dev/null @@ -1,82 +0,0 @@ -import AnyLint -import Foundation -import Yams - -struct LintTask { - let configFilePath: String - let logDebugLevel: Bool - let failOnWarnings: Bool - let validateOnly: Bool -} - -extension LintTask: TaskHandler { - enum LintError: Error { - case configFileFailed - } - - /// - Throws: `LintError.configFileFailed` if running a configuration file fails - func perform() throws { - try ValidateOrFail.configFileExists(at: configFilePath) - - let configFileUrl = URL(fileURLWithPath: configFilePath) - let configFileData = try Data(contentsOf: configFileUrl) - let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) - - do { - log.message("Start linting using config file at \(configFilePath) ...", level: .info) - - var arguments: [String] = [log.outputType.rawValue] - - if logDebugLevel { - arguments.append(Constants.debugArgument) - } - - if failOnWarnings { - arguments.append(Constants.strictArgument) - } - - if validateOnly { - arguments.append(Constants.validateArgument) - } - - try Lint.logSummaryAndExit(arguments: arguments) { - for checkFileContent in lintConfig.checkFileContents { - try Lint.checkFileContents( - checkInfo: .init(id: checkFileContent.hint, hint: checkFileContent.hint), - regex: .init(checkFileContent.regex), - matchingExamples: checkFileContent.matchingExamples ?? [], - nonMatchingExamples: checkFileContent.nonMatchingExamples ?? [], - includeFilters: checkFileContent.includeFilters ?? [Regex(".*")], - excludeFilters: checkFileContent.excludeFilters ?? [], - autoCorrectReplacement: checkFileContent.autoCorrectReplacement, - autoCorrectExamples: checkFileContent.autoCorrectExamples ?? [], - repeatIfAutoCorrected: checkFileContent.repeatIfAutoCorrected ?? false - ) - } - - for checkFilePath in lintConfig.checkFilePaths { - try Lint.checkFilePaths( - checkInfo: .init(id: checkFilePath.id, hint: checkFilePath.hint), - regex: .init(checkFilePath.regex), - matchingExamples: checkFilePath.matchingExamples ?? [], - nonMatchingExamples: checkFilePath.nonMatchingExamples ?? [], - includeFilters: checkFilePath.includeFilters ?? [Regex(".*")], - excludeFilters: checkFilePath.excludeFilters ?? [], - autoCorrectReplacement: checkFilePath.autoCorrectReplacement, - autoCorrectExamples: checkFilePath.autoCorrectExamples ?? [], - violateIfNoMatchesFound: checkFilePath.violateIfNoMatchesFound ?? false - ) - } - } - - 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 deleted file mode 100644 index b2b651f..0000000 --- a/Sources/AnyLintCLI/Tasks/TaskHandler.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -protocol TaskHandler { - func perform() throws -} diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift deleted file mode 100644 index bde9f22..0000000 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -struct VersionTask { /* for extension purposes only */ } - -extension VersionTask: TaskHandler { - func perform() throws { - log.message(Constants.currentVersion, level: .info) - } -} diff --git a/Sources/AnyLintCLI/main.swift b/Sources/AnyLintCLI/main.swift deleted file mode 100644 index 04d1e04..0000000 --- a/Sources/AnyLintCLI/main.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Foundation - -let singleCommand = CLI(singleCommand: SingleCommand()) -singleCommand.goAndExit() diff --git a/Sources/Checkers/Extensions/ArrayExt.swift b/Sources/Checkers/Extensions/ArrayExt.swift index 9acb876..512ee3f 100644 --- a/Sources/Checkers/Extensions/ArrayExt.swift +++ b/Sources/Checkers/Extensions/ArrayExt.swift @@ -1,4 +1,5 @@ import Foundation +import Core extension Array where Element == String { func containsLine(at indexes: [Int], matchingRegex regex: Regex) -> Bool { diff --git a/Sources/Checkers/Extensions/RegexExt.swift b/Sources/Checkers/Extensions/RegexExt.swift index 87fd9ad..9866407 100644 --- a/Sources/Checkers/Extensions/RegexExt.swift +++ b/Sources/Checkers/Extensions/RegexExt.swift @@ -55,7 +55,7 @@ extension Regex: ExpressibleByStringLiteral { } catch { log.message("Failed to convert String literal '\(value)' to type Regex.", level: .error) - log.exit(status: .failure) + log.exit(fail: true) exit(EXIT_FAILURE) // only reachable in unit tests } } @@ -88,7 +88,7 @@ extension Regex: ExpressibleByDictionaryLiteral { } catch { log.message("Failed to convert Dictionary literal '\(elements)' to type Regex.", level: .error) - log.exit(status: .failure) + log.exit(fail: true) exit(EXIT_FAILURE) // only reachable in unit tests } } diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift index f0c28c3..488a5df 100644 --- a/Sources/Checkers/FileContentsChecker.swift +++ b/Sources/Checkers/FileContentsChecker.swift @@ -53,7 +53,7 @@ extension FileContentsChecker: Checker { // let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(checkInfo.id)"#) // // for match in regex.matches(in: fileContents).reversed() { - // let locationInfo = fileContents.locationInfo(of: match.range.lowerBound) + // let fileLocation = fileContents.locationInfo(of: match.range.lowerBound) // // log.message("Found violating match at \(locationInfo) ...", level: .debug) // diff --git a/Sources/Checkers/FilePathsChecker.swift b/Sources/Checkers/FilePathsChecker.swift index 073a5f3..531f817 100644 --- a/Sources/Checkers/FilePathsChecker.swift +++ b/Sources/Checkers/FilePathsChecker.swift @@ -35,7 +35,7 @@ extension FilePathsChecker: Checker { // 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) + // Violation(checkInfo: checkInfo, filePath: nil, fileLocation: nil, appliedAutoCorrection: nil) // ) // } // } diff --git a/Sources/Checkers/Helpers/FileManagerExt.swift b/Sources/Checkers/Helpers/FileManagerExt.swift index fe3a6cc..1e0a8c8 100644 --- a/Sources/Checkers/Helpers/FileManagerExt.swift +++ b/Sources/Checkers/Helpers/FileManagerExt.swift @@ -6,7 +6,7 @@ extension FileManager { 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) + log.exit(fail: true) return // only reachable in unit tests } @@ -22,7 +22,7 @@ extension FileManager { guard fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) - log.exit(status: .failure) + log.exit(fail: true) return // only reachable in unit tests } diff --git a/Sources/Checkers/Helpers/FilesSearch.swift b/Sources/Checkers/Helpers/FilesSearch.swift index 88f7a03..69dfa08 100644 --- a/Sources/Checkers/Helpers/FilesSearch.swift +++ b/Sources/Checkers/Helpers/FilesSearch.swift @@ -1,4 +1,5 @@ import Foundation +import Core /// Helper to search for files and filter using Regexes. public final class FilesSearch { @@ -47,7 +48,7 @@ public final class FilesSearch { // // 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) + // log.exit(fail: true) // return [] // only reachable in unit tests // } // @@ -61,7 +62,7 @@ public final class FilesSearch { // ) // else { // log.message("Couldn't create enumerator for path '\(path)'.", level: .error) - // log.exit(status: .failure) + // log.exit(fail: true) // return [] // only reachable in unit tests // } // @@ -76,7 +77,7 @@ public final class FilesSearch { // let isRegularFilePath = resourceValues.isRegularFile // else { // log.message("Could not read resource values for file at \(fileUrl.path)", level: .error) - // log.exit(status: .failure) + // log.exit(fail: true) // return [] // only reachable in unit tests // } // diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index 9d6334d..bd5835e 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -1,267 +1,275 @@ -//import Foundation -// -///// 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'. -// /// - 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, -// matchingExamples: [String] = [], -// nonMatchingExamples: [String] = [], -// includeFilters: [Regex] = [#".*"#], -// excludeFilters: [Regex] = [], -// autoCorrectReplacement: String? = nil, -// autoCorrectExamples: [AutoCorrection] = [], -// repeatIfAutoCorrected: Bool = false -// ) throws { -// 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 -// } -// -// let filePathsToCheck: [String] = FilesSearch.shared.allFiles( -// within: fileManager.currentDirectoryPath, -// includeFilters: includeFilters, -// excludeFilters: excludeFilters -// ) -// -// let violations = try FileContentsChecker( -// checkInfo: checkInfo, -// regex: regex, -// filePathsToCheck: filePathsToCheck, -// autoCorrectReplacement: autoCorrectReplacement, -// repeatIfAutoCorrected: repeatIfAutoCorrected -// ) -// .performCheck() -// -// Statistics.shared.found(violations: violations, in: checkInfo) -// } -// -// /// 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 { -// validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) -// validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) -// validateParameterCombinations( -// checkInfo: checkInfo, -// autoCorrectReplacement: autoCorrectReplacement, -// autoCorrectExamples: autoCorrectExamples, -// violateIfNoMatchesFound: violateIfNoMatchesFound -// ) -// -// if let autoCorrectReplacement = autoCorrectReplacement { -// validateAutocorrectsAll( -// checkInfo: checkInfo, -// examples: autoCorrectExamples, -// regex: regex, -// autocorrectReplacement: autoCorrectReplacement -// ) -// } -// -// guard !Options.validateOnly else { -// Statistics.shared.executedChecks.append(checkInfo) -// return -// } -// -// let filePathsToCheck: [String] = FilesSearch.shared.allFiles( -// within: fileManager.currentDirectoryPath, -// includeFilters: includeFilters, -// excludeFilters: excludeFilters -// ) -// -// let violations = try FilePathsChecker( -// checkInfo: checkInfo, -// regex: regex, -// filePathsToCheck: filePathsToCheck, -// autoCorrectReplacement: autoCorrectReplacement, -// violateIfNoMatchesFound: violateIfNoMatchesFound -// ) -// .performCheck() -// -// Statistics.shared.found(violations: violations, in: checkInfo) -// } -// -// /// 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) -> [Violation]) { -// guard !Options.validateOnly else { -// Statistics.shared.executedChecks.append(checkInfo) -// return -// } -// -// Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) -// } -// -// /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. -// public static func logSummaryAndExit( -// arguments: [String] = [], -// afterPerformingChecks checksToPerform: () throws -> Void = {} -// ) throws { -// let failOnWarnings = arguments.contains(Constants.strictArgument) -// let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) -// -// if targetIsXcode { -// log = Logger(outputType: .xcode) -// } -// -// log.logDebugLevel = arguments.contains(Constants.debugArgument) -// Options.validateOnly = arguments.contains(Constants.validateArgument) -// -// try checksToPerform() -// -// guard !Options.validateOnly else { -// Statistics.shared.logValidationSummary() -// log.exit(status: .success) -// return // only reachable in unit tests -// } -// -// Statistics.shared.logCheckSummary() -// -// 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, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { -// if matchingExamples.isFilled { -// log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) -// } -// -// for example in matchingExamples { -// if !regex.matches(example) { -// log.message( -// "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", -// level: .error -// ) -// log.exit(status: .failure) -// } -// } -// } -// -// static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { -// if nonMatchingExamples.isFilled { -// log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) -// } -// -// for example in nonMatchingExamples { -// if regex.matches(example) { -// log.message( -// "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", -// level: .error -// ) -// log.exit(status: .failure) -// } -// } -// } -// -// static func validateAutocorrectsAll( -// checkInfo: CheckInfo, -// examples: [AutoCorrection], -// regex: Regex, -// autocorrectReplacement: String -// ) { -// if examples.isFilled { -// log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) -// } -// -// for autocorrect in examples { -// let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) -// if autocorrected != autocorrect.after { -// log.message( -// """ -// Autocorrecting example for \(checkInfo.id) did not result in expected output. -// Before: '\(autocorrect.before.showWhitespacesAndNewlines())' -// After: '\(autocorrected.showWhitespacesAndNewlines())' -// Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' -// """, -// level: .error -// ) -// 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 -// ) -// } -// -// 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 -// } -// } -//} +import Foundation +import Core + +/// 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'. + /// - 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, + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [], + includeFilters: [Regex] = [#".*"#], + excludeFilters: [Regex] = [], + autoCorrectReplacement: String? = nil, + autoCorrectExamples: [AutoCorrection] = [], + repeatIfAutoCorrected: Bool = false + ) throws { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // 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 + // } + // + // let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + // within: fileManager.currentDirectoryPath, + // includeFilters: includeFilters, + // excludeFilters: excludeFilters + // ) + // + // let violations = try FileContentsChecker( + // checkInfo: checkInfo, + // regex: regex, + // filePathsToCheck: filePathsToCheck, + // autoCorrectReplacement: autoCorrectReplacement, + // repeatIfAutoCorrected: repeatIfAutoCorrected + // ) + // .performCheck() + // + // Statistics.shared.found(violations: violations, in: checkInfo) + } + + /// 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 { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + // validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + // validateParameterCombinations( + // checkInfo: checkInfo, + // autoCorrectReplacement: autoCorrectReplacement, + // autoCorrectExamples: autoCorrectExamples, + // violateIfNoMatchesFound: violateIfNoMatchesFound + // ) + // + // if let autoCorrectReplacement = autoCorrectReplacement { + // validateAutocorrectsAll( + // checkInfo: checkInfo, + // examples: autoCorrectExamples, + // regex: regex, + // autocorrectReplacement: autoCorrectReplacement + // ) + // } + // + // guard !Options.validateOnly else { + // Statistics.shared.executedChecks.append(checkInfo) + // return + // } + // + // let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + // within: fileManager.currentDirectoryPath, + // includeFilters: includeFilters, + // excludeFilters: excludeFilters + // ) + // + // let violations = try FilePathsChecker( + // checkInfo: checkInfo, + // regex: regex, + // filePathsToCheck: filePathsToCheck, + // autoCorrectReplacement: autoCorrectReplacement, + // violateIfNoMatchesFound: violateIfNoMatchesFound + // ) + // .performCheck() + // + // Statistics.shared.found(violations: violations, in: checkInfo) + } + + /// 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) -> [Violation]) { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // guard !Options.validateOnly else { + // Statistics.shared.executedChecks.append(checkInfo) + // return + // } + // + // Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) + } + + // /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. + // public static func logSummaryAndExit( + // arguments: [String] = [], + // afterPerformingChecks checksToPerform: () throws -> Void = {} + // ) throws { + // let failOnWarnings = arguments.contains(Constants.strictArgument) + // let targetIsXcode = arguments.contains(Logger.OutputType.xcode.rawValue) + // + // if targetIsXcode { + // log = Logger(outputType: .xcode) + // } + // + // log.logDebugLevel = arguments.contains(Constants.debugArgument) + // Options.validateOnly = arguments.contains(Constants.validateArgument) + // + // try checksToPerform() + // + // guard !Options.validateOnly else { + // Statistics.shared.logValidationSummary() + // log.exit(status: .success) + // return // only reachable in unit tests + // } + // + // Statistics.shared.logCheckSummary() + // + // if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { + // log.exit(fail: true) + // } + // else if failOnWarnings + // && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled + // { + // log.exit(fail: true) + // } + // else { + // log.exit(status: .success) + // } + // } + + static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // if matchingExamples.isFilled { + // log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) + // } + // + // for example in matchingExamples { + // if !regex.matches(example) { + // log.message( + // "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", + // level: .error + // ) + // log.exit(fail: true) + // } + // } + } + + static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // if nonMatchingExamples.isFilled { + // log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) + // } + // + // for example in nonMatchingExamples { + // if regex.matches(example) { + // log.message( + // "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", + // level: .error + // ) + // log.exit(fail: true) + // } + // } + } + + static func validateAutocorrectsAll( + checkInfo: CheckInfo, + examples: [AutoCorrection], + regex: Regex, + autocorrectReplacement: String + ) { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // if examples.isFilled { + // log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) + // } + // + // for autocorrect in examples { + // let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) + // if autocorrected != autocorrect.after { + // log.message( + // """ + // Autocorrecting example for \(checkInfo.id) did not result in expected output. + // Before: '\(autocorrect.before.showWhitespacesAndNewlines())' + // After: '\(autocorrected.showWhitespacesAndNewlines())' + // Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' + // """, + // level: .error + // ) + // log.exit(fail: true) + // } + // } + } + + static func validateParameterCombinations( + checkInfo: CheckInfo, + autoCorrectReplacement: String?, + autoCorrectExamples: [AutoCorrection], + violateIfNoMatchesFound: Bool? + ) { + fatalError() // TODO: [cg_2021-07-08] not yet implemented + // 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(fail: true) + // return // only reachable in unit tests + // } + } +} diff --git a/Sources/Commands/AnyLint.swift b/Sources/Commands/AnyLint.swift index 4c39846..073c095 100644 --- a/Sources/Commands/AnyLint.swift +++ b/Sources/Commands/AnyLint.swift @@ -1,5 +1,5 @@ -import ArgumentParser import Foundation +import ArgumentParser @main struct AnyLint: ParsableCommand { diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift index 95f23c4..b482ce5 100644 --- a/Sources/Commands/InitCommand.swift +++ b/Sources/Commands/InitCommand.swift @@ -1,7 +1,7 @@ +import Foundation import ArgumentParser import Configuration import Core -import Foundation import ShellOut struct InitCommand: ParsableCommand { @@ -18,7 +18,7 @@ struct InitCommand: ParsableCommand { @Option( name: .shortAndLong, - help: "Path to the new config file to initialize it at." + help: "Path to the new config file to initialize it at. If a directory is specified, creates 'anylint.yml' in it." ) var path: String = FileManager.default.currentDirectoryUrl.appendingPathComponent("anylint.yml").path @@ -30,7 +30,7 @@ struct InitCommand: ParsableCommand { guard !FileManager.default.fileExists(atPath: path) else { log.message("Configuration file already exists at path '\(path)'.", level: .error) - log.exit(status: .failure) + log.exit(fail: true) return // only reachable in unit tests } diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index 6f66fd2..d9cb1f7 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -1,7 +1,10 @@ +import Foundation import ArgumentParser +import Checkers +import Configuration import Core -import Foundation import Reporting +import Yams struct LintCommand: ParsableCommand { static var configuration: CommandConfiguration = .init( @@ -47,7 +50,88 @@ struct LintCommand: ParsableCommand { var verbose: Bool = false mutating func run() throws { - // TODO: [cg_2021-06-28] not yet implemented + log = Logger(outputFormat: outputFormat) + + guard FileManager.default.fileExists(atPath: config) else { + log.message( + "No configuration file found at \(config) – consider running `anylint --init` with a template.", + level: .error + ) + log.exit(fail: true) + return // only reachable in unit tests + } + + let configFileUrl = URL(fileURLWithPath: config) + let configFileData = try Data(contentsOf: configFileUrl) + let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) + + do { + log.message("Start linting using config file at \(config) ...", level: .info) + + try checksToPerform() + + Statistics.shared.logCheckSummary() + + if Statistics.shared.violations(severity: .error, excludeAutocorrected: outputFormat == .xcode).isFilled { + log.exit(fail: true) + } + else if + failLevel == .warning, + Statistics.shared.violations(severity: .warning, excludeAutocorrected: outputFormat == .xcode).isFilled + { + log.exit(fail: true) + } + else { + log.exit(status: .success) + } + + + + + + + + + + try Lint.logSummaryAndExit(arguments: arguments) { + for checkFileContent in lintConfig.checkFileContents { + try Lint.checkFileContents( + checkInfo: .init(id: checkFileContent.hint, hint: checkFileContent.hint), + regex: .init(checkFileContent.regex), + matchingExamples: checkFileContent.matchingExamples ?? [], + nonMatchingExamples: checkFileContent.nonMatchingExamples ?? [], + includeFilters: checkFileContent.includeFilters ?? [Regex(".*")], + excludeFilters: checkFileContent.excludeFilters ?? [], + autoCorrectReplacement: checkFileContent.autoCorrectReplacement, + autoCorrectExamples: checkFileContent.autoCorrectExamples ?? [], + repeatIfAutoCorrected: checkFileContent.repeatIfAutoCorrected ?? false + ) + } + + for checkFilePath in lintConfig.checkFilePaths { + try Lint.checkFilePaths( + checkInfo: .init(id: checkFilePath.id, hint: checkFilePath.hint), + regex: .init(checkFilePath.regex), + matchingExamples: checkFilePath.matchingExamples ?? [], + nonMatchingExamples: checkFilePath.nonMatchingExamples ?? [], + includeFilters: checkFilePath.includeFilters ?? [Regex(".*")], + excludeFilters: checkFilePath.excludeFilters ?? [], + autoCorrectReplacement: checkFilePath.autoCorrectReplacement, + autoCorrectExamples: checkFilePath.autoCorrectExamples ?? [], + violateIfNoMatchesFound: checkFilePath.violateIfNoMatchesFound ?? false + ) + } + } + + log.message("Linting successful using config file at \(config). Congrats! 🎉", level: .success) + } + catch is RunError { + if log.outputType != .xcode { + log.message("Linting failed using config file at \(config).", level: .error) + } + + throw LintError.configFileFailed + } } } diff --git a/Sources/Configuration/LintConfiguration.swift b/Sources/Configuration/LintConfiguration.swift new file mode 100644 index 0000000..7d37da2 --- /dev/null +++ b/Sources/Configuration/LintConfiguration.swift @@ -0,0 +1,149 @@ +import Foundation +import Core + +/// The configuration file type. +public struct LintConfiguration: Codable { + enum CodingKeys: String, CodingKey { + case fileContents = "CheckFileContents" + case filePaths = "CheckFilePaths" + case customScripts = "CustomScripts" + } + + /// The list of `FileContents` checks. + public let fileContents: [FileContentsConfiguration] + + /// The list of `FilePaths` checks. + public let filePaths: [FilePathsConfiguration] + + /// The list of `CustomScripts` checks. + public let customScripts: [CustomScriptsConfiguration] +} + +/// The `FileContents` check configuration type. +public struct FileContentsConfiguration: Codable { + /// A unique identifier for the check to show on violations. Required. + public let id: String + + /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. Required. + public let hint: String + + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. + public var severity: Severity = .error + + /// The regular expression to use to find violations. Required. + public let regex: String + + /// A list of strings that are expected to match the provided `regex`. Optional. + /// + /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public let matchingExamples: [String]? + + /// A list of strings that are expected to **not** to match the provided `regex`. Optional. + /// + /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public let nonMatchingExamples: [String]? + + /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. + /// + /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. + public let includeFilters: [Regex]? + + /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. + /// + /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' + public let excludeFilters: [Regex]? + + /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. + /// + /// See `NSRegularExpression` documentation for examples and more details (e.g. in the 'Template Matching Format' section): + /// https://developer.apple.com/documentation/foundation/nsregularexpression + public let autoCorrectReplacement: String? + + /// A dictionary consisting of the keys `before` and `after` to specify how you would expect a given string to be changed. Optional. + /// + /// Use this to validate that the provided `regex` and the `autoCorrectReplacement` together act as expected. + /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. + /// + /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. + public let autoCorrectExamples: [AutoCorrection]? + + /// If set to `true`, a check will be re-run if there was at least one auto-correction applied on the last run. Optional. + /// + /// This can be useful for auto-correcting issues that can scale or repeat. + /// For example, to ensure long numbers are separated by an underscore, you could write the regex `(\d+)(\d{3})` + /// and specify the replacement `$1_$2$3`. By default, the number `123456789` would be transformed to `123456_789`. + /// With this option set to `true`, the check would be re-executed after the first run (because there was a correction) and the result would be `123_456_789`. + public let repeatIfAutoCorrected: Bool? +} + +/// The `FilePaths` check configuration type. +public struct FilePathsConfiguration: Codable { + /// A unique identifier for the check to show on violations. + public let id: String + + /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. + public let hint: String + + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. + public var severity: Severity = .error + + /// The regular expression to use to find violations. Required. + public let regex: String + + /// A list of strings that are expected to match the provided `regex`. Optional. + /// + /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public let matchingExamples: [String]? + + /// A list of strings that are expected to **not** to match the provided `regex`. Optional. + /// + /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public let nonMatchingExamples: [String]? + + /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. + /// + /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. + public let includeFilters: [Regex]? + + /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. + /// + /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' + public let excludeFilters: [Regex]? + + /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. + /// + /// See `NSRegularExpression` documentation for examples and more details (e.g. in the 'Template Matching Format' section): + /// https://developer.apple.com/documentation/foundation/nsregularexpression + /// + /// Use this to automatically move violating files from their current paht to the expected path. + public let autoCorrectReplacement: String? + + /// A dictionary consisting of the keys `before` and `after` to specify how you would expect a given path to be changed. Optional. + /// + /// Use this to validate that the provided `regex` and the `autoCorrectReplacement` together act as expected. + /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. + /// + /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. + public let autoCorrectExamples: [AutoCorrection]? + + /// If set to `true`, a violation will be reported if **no** matches are found. By default (or if set to `false`), a check violates on every matching file path. + public let violateIfNoMatchesFound: Bool? +} + +/// The `CustomScripts` check configuration type. +public struct CustomScriptsConfiguration: Codable { + /// The name of the custom script. + public let name: String + + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. + public var severity: Severity = .error + + /// The custom command line command to execute. + /// If the output conforms to the ``LintResults`` structure formatted as JSON, then the results will be merged. + /// Otherwise AnyLint will violate for any non-zero exit code with the last printed line. + public let command: String +} diff --git a/Sources/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift index 5baccd9..d529341 100644 --- a/Sources/Core/AutoCorrection.swift +++ b/Sources/Core/AutoCorrection.swift @@ -60,7 +60,7 @@ public struct AutoCorrection: Codable { // 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) +// log.exit(fail: true) // exit(EXIT_FAILURE) // only reachable in unit tests // } // diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/CheckInfo.swift index a8e6425..05801a3 100644 --- a/Sources/Core/CheckInfo.swift +++ b/Sources/Core/CheckInfo.swift @@ -23,12 +23,12 @@ public struct CheckInfo { } } -//extension CheckInfo: Hashable { -// public func hash(into hasher: inout Hasher) { -// hasher.combine(id) -// } -//} -// +extension CheckInfo: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + //extension CheckInfo: CustomStringConvertible { // public var description: String { // "check '\(id)'" @@ -57,7 +57,7 @@ public struct CheckInfo { // "Specified severity '\(severityString)' for check '\(id)' unknown. Use one of [error, warning, info].", // level: .error // ) -// log.exit(status: .failure) +// log.exit(fail: true) // exit(EXIT_FAILURE) // only reachable in unit tests // } // @@ -75,7 +75,7 @@ public struct CheckInfo { // "Could not convert String literal '\(value)' to type CheckInfo. Please check the structure to be: (@): ", // level: .error // ) -// log.exit(status: .failure) +// log.exit(fail: true) // exit(EXIT_FAILURE) // only reachable in unit tests // } // diff --git a/Sources/Core/Extensions/StringExt.swift b/Sources/Core/Extensions/StringExt.swift index 564d261..9b6ca68 100644 --- a/Sources/Core/Extensions/StringExt.swift +++ b/Sources/Core/Extensions/StringExt.swift @@ -1,17 +1,14 @@ import Foundation extension String { - /// Info about the exact location of a character in a given file. - public typealias LocationInfo = (line: Int, charInLine: Int) - /// Returns the location info for a given line index. - public func locationInfo(of index: String.Index) -> LocationInfo { + public func fileLocation(of index: String.Index) -> FileLocation { let prefix = self[startIndex.. String { guard let pathUrl = URL(string: self) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) - log.exit(status: .failure) + log.exit(fail: true) return "" // only reachable in unit tests } diff --git a/Sources/Core/FileLocation.swift b/Sources/Core/FileLocation.swift new file mode 100644 index 0000000..84af7c8 --- /dev/null +++ b/Sources/Core/FileLocation.swift @@ -0,0 +1,19 @@ +import Foundation + +/// Info about the exact location of a character in a given file. +public struct FileLocation: Codable { + /// The row or line number of the location. + public let row: Int + + /// The column or character index within a line of the location. + public let column: Int + + /// Initializes a file location object. + public init( + row: Int, + column: Int + ) { + self.row = row + self.column = column + } +} diff --git a/Sources/Core/Logger.swift b/Sources/Core/Logger.swift index 72eb62d..1e653d6 100644 --- a/Sources/Core/Logger.swift +++ b/Sources/Core/Logger.swift @@ -2,7 +2,7 @@ import Foundation import Rainbow /// Shortcut to access the `Logger` within this project. -public var log = Logger(outputType: .console) +public var log = Logger(outputFormat: .commandLine) /// Helper to log output to console or elsewhere. public final class Logger { @@ -20,9 +20,6 @@ public final class Logger { /// Print information that probably is problematic. case error - /// Print detailed information for debugging purposes. - case debug - var color: Color { switch self { case .success: @@ -36,55 +33,18 @@ public final class Logger { 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 + /// The output format of the logger. + public let outputFormat: OutputFormat - /// 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. + /// Initializes a new Logger object with a given output format. public init( - outputType: OutputType + outputFormat: OutputFormat ) { - self.outputType = outputType + self.outputFormat = outputFormat } /// Communicates a message to the chosen output target with proper formatting based on level & source. @@ -93,33 +53,24 @@ public final class Logger { /// - 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: + switch outputFormat { + case .commandLine, .json: 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 - } + /// Exits the current program with the given fail state. + public func exit(fail: Bool) { + let statusCode = fail ? EXIT_FAILURE : EXIT_SUCCESS + + #if os(Linux) + Glibc.exit(statusCode) + #else + Darwin.exit(statusCode) + #endif } private func consoleMessage(_ message: String, level: PrintLevel) { @@ -135,9 +86,6 @@ public final class Logger { case .error: print(formattedCurrentTime(), "❌", message.red) - - case .debug: - print(formattedCurrentTime(), "💬", message) } } @@ -148,12 +96,13 @@ public final class Logger { /// - 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) { + var locationPrefix = "" + if let location = location { - print("\(location) \(level.rawValue): AnyLint: \(message)") - } - else { - print("\(level.rawValue): AnyLint: \(message)") + locationPrefix = location + " " } + + print("\(locationPrefix)\(level.rawValue): AnyLint: \(message)") } private func formattedCurrentTime() -> String { @@ -165,7 +114,7 @@ public final class Logger { } extension Severity { - var logLevel: Logger.PrintLevel { + public var logLevel: Logger.PrintLevel { switch self { case .info: return .info diff --git a/Sources/Reporting/OutputFormat.swift b/Sources/Core/OutputFormat.swift similarity index 100% rename from Sources/Reporting/OutputFormat.swift rename to Sources/Core/OutputFormat.swift diff --git a/Sources/Checkers/Helpers/Regex.swift b/Sources/Core/Regex.swift similarity index 100% rename from Sources/Checkers/Helpers/Regex.swift rename to Sources/Core/Regex.swift diff --git a/Sources/Core/Severity.swift b/Sources/Core/Severity.swift index 41ac362..2c83ea5 100644 --- a/Sources/Core/Severity.swift +++ b/Sources/Core/Severity.swift @@ -2,7 +2,7 @@ import Foundation import AppKit /// Defines the severity of a lint check. -public enum Severity: String, CaseIterable { +public enum Severity: String, CaseIterable, Codable { /// Use for checks that are mostly informational and not necessarily problematic. case info @@ -11,7 +11,7 @@ public enum Severity: String, CaseIterable { /// Use for checks that probably are problematic. case error - // + // static func from(string: String) -> Severity? { // switch string { // case "info", "i": @@ -28,15 +28,15 @@ public enum Severity: String, CaseIterable { // } // } } -// -//extension Severity: Comparable { -// public static func < (lhs: Severity, rhs: Severity) -> Bool { -// switch (lhs, rhs) { -// case (.info, .warning), (.warning, .error), (.info, .error): -// return true -// -// default: -// return false -// } -// } -//} + +extension Severity: Comparable { + public static func < (lhs: Severity, rhs: Severity) -> Bool { + switch (lhs, rhs) { + case (.info, .warning), (.warning, .error), (.info, .error): + return true + + default: + return false + } + } +} diff --git a/Sources/Core/TestHelper.swift b/Sources/Core/TestHelper.swift deleted file mode 100644 index 3a48686..0000000 --- a/Sources/Core/TestHelper.swift +++ /dev/null @@ -1,22 +0,0 @@ -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 shared `TestHelper` object. - public static let shared = TestHelper() - - /// Use only in Unit Tests. - public var consoleOutputs: [ConsoleOutput] = [] - - /// Use only in Unit Tests. - public var exitStatus: Logger.ExitStatus? - - /// Deletes all data collected until now. - public func reset() { - consoleOutputs = [] - exitStatus = nil - } -} diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index e95f9a6..54a9608 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -1,10 +1,7 @@ import Foundation /// A violation found in a check. -public struct Violation { - /// The info about the chack that caused this violation. - public let checkInfo: CheckInfo - +public struct Violation: Codable { /// The file path the violation is related to. public let filePath: String? @@ -12,30 +9,28 @@ public struct Violation { 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? + public let fileLocation: FileLocation? /// 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, + fileLocation: FileLocation? = nil, appliedAutoCorrection: AutoCorrection? = nil ) { - self.checkInfo = checkInfo self.filePath = filePath self.matchedString = matchedString - self.locationInfo = locationInfo + self.fileLocation = fileLocation 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):" + guard let fileLocation = fileLocation else { return filePath.path(type: pathType) } + return "\(filePath.path(type: pathType)):\(fileLocation.row):\(fileLocation.column):" } } diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift new file mode 100644 index 0000000..a0762a5 --- /dev/null +++ b/Sources/Reporting/LintResults.swift @@ -0,0 +1,139 @@ +import Foundation +import Core +import OrderedCollections + +/// The linting output type. Can be merged from multiple +public typealias LintResults = OrderedDictionary> + +extension LintResults { + var allExecutedChecks: [CheckInfo] { + values.reduce(into: []) { $0.append(contentsOf: $1.keys) } + } + + var allFoundViolations: [Violation] { + values.reduce(into: []) { $0.append(contentsOf: $1.values.flatMap { $0 }) } + } + + /// Merges the given lint results into this one. + public mutating func mergeResults(_ other: LintResults) { + merge(other) { currentDict, newDict in + currentDict.merging(newDict) { currentViolations, newViolations in + currentViolations + newViolations + } + } + } + + /// Logs the summary of the violations in the specified output format. + public func report(outputFormat: OutputFormat) { + let executedChecks = allExecutedChecks + + if executedChecks.isEmpty { + log.message("No checks found to perform.", level: .warning) + } + else if values.contains(where: { $0.values.isFilled }) { + switch outputFormat { + case .commandLine: + reportToConsole() + + case .xcode: + reportToXcode() + + case .json: + reportToFile(at: "anylint-results.json") + } + } + else { + log.message( + "Performed \(executedChecks.count) check(s) without any violations.", + level: .success + ) + } + } + + func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { + guard let violations = self[severity]?.values.flatMap({ $0 }) else { return [] } + guard excludeAutocorrected else { return violations } + return violations.filter { $0.appliedAutoCorrection == nil } + } + + private func reportToConsole() { + // TODO: [cg_2021-07-06] not yet implemented + // 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.. " + 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 reportToXcode() { + for severity in keys.sorted().reversed() { + guard let checkResultsAtSeverity = self[severity] else { continue } + + for (checkInfo, violations) in checkResultsAtSeverity { + for violation in violations where violation.appliedAutoCorrection == nil { + log.xcodeMessage( + "[\(checkInfo.id)] \(checkInfo.hint)", + level: severity.logLevel, + location: violation.locationMessage(pathType: .absolute) + ) + } + } + + } + } + + private func reportToFile(at path: String) { + + } +} From 38e1dee715c2a68214f61a8b65019311c1dfc56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 9 Jul 2021 18:03:24 +0200 Subject: [PATCH 08/37] Re-enact most out-commented code with new approach --- Sources/Checkers/Extensions/RegexExt.swift | 2 - Sources/Checkers/FileContentsChecker.swift | 187 +++++----- Sources/Checkers/FilePathsChecker.swift | 76 ++-- Sources/Checkers/Helpers/FileManagerExt.swift | 2 - Sources/Checkers/Helpers/FilesSearch.swift | 172 +++++----- Sources/Checkers/Lint.swift | 324 +++++++----------- Sources/Commands/InitCommand.swift | 1 - Sources/Commands/LintCommand.swift | 124 ++++--- Sources/Configuration/LintConfiguration.swift | 48 ++- Sources/Core/CheckInfo.swift | 58 ---- Sources/Core/Extensions/StringExt.swift | 1 - Sources/Core/Logger.swift | 2 +- Sources/Core/Severity.swift | 16 - Sources/Reporting/LintResults.swift | 20 +- 14 files changed, 429 insertions(+), 604 deletions(-) diff --git a/Sources/Checkers/Extensions/RegexExt.swift b/Sources/Checkers/Extensions/RegexExt.swift index 9866407..41598c5 100644 --- a/Sources/Checkers/Extensions/RegexExt.swift +++ b/Sources/Checkers/Extensions/RegexExt.swift @@ -56,7 +56,6 @@ extension Regex: ExpressibleByStringLiteral { catch { log.message("Failed to convert String literal '\(value)' to type Regex.", level: .error) log.exit(fail: true) - exit(EXIT_FAILURE) // only reachable in unit tests } } } @@ -89,7 +88,6 @@ extension Regex: ExpressibleByDictionaryLiteral { catch { log.message("Failed to convert Dictionary literal '\(elements)' to type Regex.", level: .error) log.exit(fail: true) - exit(EXIT_FAILURE) // only reachable in unit tests } } } diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift index 488a5df..eb01d31 100644 --- a/Sources/Checkers/FileContentsChecker.swift +++ b/Sources/Checkers/FileContentsChecker.swift @@ -27,109 +27,88 @@ public struct FileContentsChecker { extension FileContentsChecker: Checker { public func performCheck() throws -> [Violation] { - fatalError() // TODO: [cg_2021-07-02] not yet implemented - // 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 fileLocation = fileContents.locationInfo(of: match.range.lowerBound) - // - // 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 - // ) - // } - // - // 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, - // filePathsToCheck: filePathsToReCheck, - // autoCorrectReplacement: autoCorrectReplacement, - // repeatIfAutoCorrected: repeatIfAutoCorrected - // ) - // .performCheck() - // violations.append(contentsOf: violationsOnRechecks) - // } - // - // return violations + var violations: [Violation] = [] + + for filePath in filePathsToCheck.reversed() { + if let fileData = FileManager.default.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]|[, ]\#(id)[,\s])"#) + guard !skipInFileRegex.matches(fileContents) else { continue } + + let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(id)"#) + + for match in regex.matches(in: fileContents).reversed() { + let fileLocation = fileContents.fileLocation(of: match.range.lowerBound) + + // skip found match if contains `AnyLint.skipHere: ` in same line or one line before + guard + !linesInFile.containsLine(at: [fileLocation.row - 2, fileLocation.row - 1], matchingRegex: skipHereRegex) + else { 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) + } + + violations.append( + Violation( + filePath: filePath, + matchedString: match.string, + fileLocation: fileLocation, + appliedAutoCorrection: autoCorrection + ) + ) + } + + if newFileContents != fileContents { + 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 + ) + } + } + + violations = violations.reversed() + + if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { + // 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( + id: id, + hint: hint, + severity: severity, + regex: regex, + filePathsToCheck: filePathsToReCheck, + autoCorrectReplacement: autoCorrectReplacement, + repeatIfAutoCorrected: repeatIfAutoCorrected + ) + .performCheck() + violations.append(contentsOf: violationsOnRechecks) + } + + return violations } } diff --git a/Sources/Checkers/FilePathsChecker.swift b/Sources/Checkers/FilePathsChecker.swift index 531f817..8b988d1 100644 --- a/Sources/Checkers/FilePathsChecker.swift +++ b/Sources/Checkers/FilePathsChecker.swift @@ -27,49 +27,37 @@ public struct FilePathsChecker { extension FilePathsChecker: Checker { public func performCheck() throws -> [Violation] { - fatalError() // TODO: [cg_2021-07-02] not yet implemented - // 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, fileLocation: 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) - // } - // - // 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 + var violations: [Violation] = [] + + if violateIfNoMatchesFound { + let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count + if matchingFilePathsCount <= 0 { + violations.append( + Violation(filePath: nil, fileLocation: nil, appliedAutoCorrection: nil) + ) + } + } + else { + for filePath in filePathsToCheck where regex.matches(filePath) { + let appliedAutoCorrection: AutoCorrection? = try { + guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } + + let newFilePath = regex.replaceAllCaptures(in: filePath, with: autoCorrectReplacement) + try FileManager.default.moveFileSafely(from: filePath, to: newFilePath) + + return AutoCorrection(before: filePath, after: newFilePath) + }() + + violations.append( + Violation( + filePath: filePath, + fileLocation: nil, + appliedAutoCorrection: appliedAutoCorrection + ) + ) + } + } + + return violations } } diff --git a/Sources/Checkers/Helpers/FileManagerExt.swift b/Sources/Checkers/Helpers/FileManagerExt.swift index 1e0a8c8..1fdf065 100644 --- a/Sources/Checkers/Helpers/FileManagerExt.swift +++ b/Sources/Checkers/Helpers/FileManagerExt.swift @@ -7,7 +7,6 @@ extension FileManager { guard fileExists(atPath: sourcePath) else { log.message("No file found at \(sourcePath) to move.", level: .error) log.exit(fail: true) - return // only reachable in unit tests } guard !fileExists(atPath: targetPath) || sourcePath.lowercased() == targetPath.lowercased() else { @@ -23,7 +22,6 @@ extension FileManager { guard fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) log.exit(fail: true) - return // only reachable in unit tests } if sourcePath.lowercased() == targetPath.lowercased() { diff --git a/Sources/Checkers/Helpers/FilesSearch.swift b/Sources/Checkers/Helpers/FilesSearch.swift index 69dfa08..54e193a 100644 --- a/Sources/Checkers/Helpers/FilesSearch.swift +++ b/Sources/Checkers/Helpers/FilesSearch.swift @@ -22,101 +22,89 @@ public final class FilesSearch { } /// Returns all file paths within given `path` matching the given `include` and `exclude` filters. - public func allFiles( // swiftlint:disable:this function_body_length + public func allFiles( within path: String, includeFilters: [Regex], excludeFilters: [Regex] = [] ) -> [String] { - fatalError() // TODO: [cg_2021-07-02] not yet implemented - // 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(fail: true) - // 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(fail: true) - // 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(fail: true) - // return [] // only reachable in unit tests - // } - // - // // skip if any exclude filter applies - // if excludeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { - // if !isRegularFilePath { - // enumerator.skipDescendants() - // } - // - // continue - // } - // - // // skip hidden files and directories - // #if os(Linux) - // if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { - // if !isRegularFilePath { - // enumerator.skipDescendants() - // } - // - // 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 + let searchOptions = SearchOptions( + pathToSearch: path, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + if let cachedFilePaths: [String] = cachedFilePaths[searchOptions] { + return cachedFilePaths + } + + guard let url = URL(string: path, relativeTo: FileManager.default.currentDirectoryUrl) else { + log.message("Could not convert path '\(path)' to type URL.", level: .error) + log.exit(fail: true) + } + + let propKeys = [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey] + guard + let enumerator = FileManager.default.enumerator( + at: url, + includingPropertiesForKeys: propKeys, + options: [], + errorHandler: nil + ) + else { + log.message("Couldn't create enumerator for path '\(path)'.", level: .error) + log.exit(fail: true) + } + + 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(fail: true) + } + + // skip if any exclude filter applies + if excludeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { + if !isRegularFilePath { + enumerator.skipDescendants() + } + + continue + } + + // skip hidden files and directories + #if os(Linux) + if isHiddenFilePath || fileUrl.path.contains("/.") || fileUrl.path.starts(with: ".") { + if !isRegularFilePath { + enumerator.skipDescendants() + } + + 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 } } diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index bd5835e..b065305 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -1,5 +1,6 @@ import Foundation import Core +import OrderedCollections /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { @@ -25,48 +26,44 @@ public enum Lint { autoCorrectReplacement: String? = nil, autoCorrectExamples: [AutoCorrection] = [], repeatIfAutoCorrected: Bool = false - ) throws { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // 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 - // } - // - // let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - // within: fileManager.currentDirectoryPath, - // includeFilters: includeFilters, - // excludeFilters: excludeFilters - // ) - // - // let violations = try FileContentsChecker( - // checkInfo: checkInfo, - // regex: regex, - // filePathsToCheck: filePathsToCheck, - // autoCorrectReplacement: autoCorrectReplacement, - // repeatIfAutoCorrected: repeatIfAutoCorrected - // ) - // .performCheck() - // - // Statistics.shared.found(violations: violations, in: checkInfo) + ) throws -> [Violation] { + 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 + ) + } + + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + let violations = try FileContentsChecker( + id: checkInfo.id, + hint: checkInfo.hint, + severity: checkInfo.severity, + regex: regex, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, + repeatIfAutoCorrected: repeatIfAutoCorrected + ) + .performCheck() + + return violations } /// Checks the names of files. @@ -91,134 +88,74 @@ public enum Lint { autoCorrectReplacement: String? = nil, autoCorrectExamples: [AutoCorrection] = [], violateIfNoMatchesFound: Bool = false - ) throws { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - // validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - // validateParameterCombinations( - // checkInfo: checkInfo, - // autoCorrectReplacement: autoCorrectReplacement, - // autoCorrectExamples: autoCorrectExamples, - // violateIfNoMatchesFound: violateIfNoMatchesFound - // ) - // - // if let autoCorrectReplacement = autoCorrectReplacement { - // validateAutocorrectsAll( - // checkInfo: checkInfo, - // examples: autoCorrectExamples, - // regex: regex, - // autocorrectReplacement: autoCorrectReplacement - // ) - // } - // - // guard !Options.validateOnly else { - // Statistics.shared.executedChecks.append(checkInfo) - // return - // } - // - // let filePathsToCheck: [String] = FilesSearch.shared.allFiles( - // within: fileManager.currentDirectoryPath, - // includeFilters: includeFilters, - // excludeFilters: excludeFilters - // ) - // - // let violations = try FilePathsChecker( - // checkInfo: checkInfo, - // regex: regex, - // filePathsToCheck: filePathsToCheck, - // autoCorrectReplacement: autoCorrectReplacement, - // violateIfNoMatchesFound: violateIfNoMatchesFound - // ) - // .performCheck() - // - // Statistics.shared.found(violations: violations, in: checkInfo) + ) throws -> [Violation] { + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + validateParameterCombinations( + checkInfo: checkInfo, + autoCorrectReplacement: autoCorrectReplacement, + autoCorrectExamples: autoCorrectExamples, + violateIfNoMatchesFound: violateIfNoMatchesFound + ) + + if let autoCorrectReplacement = autoCorrectReplacement { + validateAutocorrectsAll( + checkInfo: checkInfo, + examples: autoCorrectExamples, + regex: regex, + autocorrectReplacement: autoCorrectReplacement + ) + } + + let filePathsToCheck: [String] = FilesSearch.shared.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) + + let violations = try FilePathsChecker( + id: checkInfo.id, + hint: checkInfo.hint, + severity: checkInfo.severity, + regex: regex, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, + violateIfNoMatchesFound: violateIfNoMatchesFound + ) + .performCheck() + + return violations } - /// Run custom logic as checks. + /// Run custom scripts 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) -> [Violation]) { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // guard !Options.validateOnly else { - // Statistics.shared.executedChecks.append(checkInfo) - // return - // } - // - // Statistics.shared.found(violations: customClosure(checkInfo), in: checkInfo) + /// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. 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 -> [Violation] { + fatalError() // TODO: [cg_2021-07-09] not yet implemented } - // /// 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) - // - // if targetIsXcode { - // log = Logger(outputType: .xcode) - // } - // - // log.logDebugLevel = arguments.contains(Constants.debugArgument) - // Options.validateOnly = arguments.contains(Constants.validateArgument) - // - // try checksToPerform() - // - // guard !Options.validateOnly else { - // Statistics.shared.logValidationSummary() - // log.exit(status: .success) - // return // only reachable in unit tests - // } - // - // Statistics.shared.logCheckSummary() - // - // if Statistics.shared.violations(severity: .error, excludeAutocorrected: targetIsXcode).isFilled { - // log.exit(fail: true) - // } - // else if failOnWarnings - // && Statistics.shared.violations(severity: .warning, excludeAutocorrected: targetIsXcode).isFilled - // { - // log.exit(fail: true) - // } - // else { - // log.exit(status: .success) - // } - // } - static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // if matchingExamples.isFilled { - // log.message("Validating 'matchingExamples' for \(checkInfo) ...", level: .debug) - // } - // - // for example in matchingExamples { - // if !regex.matches(example) { - // log.message( - // "Couldn't find a match for regex \(regex) in check '\(checkInfo.id)' within matching example:\n\(example)", - // level: .error - // ) - // log.exit(fail: true) - // } - // } + 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(fail: true) + } + } } static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // if nonMatchingExamples.isFilled { - // log.message("Validating 'nonMatchingExamples' for \(checkInfo) ...", level: .debug) - // } - // - // for example in nonMatchingExamples { - // if regex.matches(example) { - // log.message( - // "Unexpectedly found a match for regex \(regex) in check '\(checkInfo.id)' within non-matching example:\n\(example)", - // level: .error - // ) - // log.exit(fail: true) - // } - // } + 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(fail: true) + } + } } static func validateAutocorrectsAll( @@ -227,26 +164,21 @@ public enum Lint { regex: Regex, autocorrectReplacement: String ) { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // if examples.isFilled { - // log.message("Validating 'autoCorrectExamples' for \(checkInfo) ...", level: .debug) - // } - // - // for autocorrect in examples { - // let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) - // if autocorrected != autocorrect.after { - // log.message( - // """ - // Autocorrecting example for \(checkInfo.id) did not result in expected output. - // Before: '\(autocorrect.before.showWhitespacesAndNewlines())' - // After: '\(autocorrected.showWhitespacesAndNewlines())' - // Expected: '\(autocorrect.after.showWhitespacesAndNewlines())' - // """, - // level: .error - // ) - // log.exit(fail: true) - // } - // } + 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(fail: true) + } + } } static func validateParameterCombinations( @@ -255,21 +187,19 @@ public enum Lint { autoCorrectExamples: [AutoCorrection], violateIfNoMatchesFound: Bool? ) { - fatalError() // TODO: [cg_2021-07-08] not yet implemented - // 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(fail: true) - // return // only reachable in unit tests - // } + 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(fail: true) + } } } diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift index b482ce5..0cbd049 100644 --- a/Sources/Commands/InitCommand.swift +++ b/Sources/Commands/InitCommand.swift @@ -31,7 +31,6 @@ struct InitCommand: ParsableCommand { guard !FileManager.default.fileExists(atPath: path) else { log.message("Configuration file already exists at path '\(path)'.", level: .error) log.exit(fail: true) - return // only reachable in unit tests } log.message("Making sure config file directory exists ...", level: .info) diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index d9cb1f7..d6ddd1f 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -43,12 +43,6 @@ struct LintCommand: ParsableCommand { ) var outputFormat: OutputFormat = .commandLine - @Flag( - name: .shortAndLong, - help: "Enables more verbose output for more details." - ) - var verbose: Bool = false - mutating func run() throws { log = Logger(outputFormat: outputFormat) @@ -58,82 +52,82 @@ struct LintCommand: ParsableCommand { level: .error ) log.exit(fail: true) - return // only reachable in unit tests } let configFileUrl = URL(fileURLWithPath: config) let configFileData = try Data(contentsOf: configFileUrl) let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) - do { - log.message("Start linting using config file at \(config) ...", level: .info) - - try checksToPerform() - - Statistics.shared.logCheckSummary() - - if Statistics.shared.violations(severity: .error, excludeAutocorrected: outputFormat == .xcode).isFilled { - log.exit(fail: true) - } - else if - failLevel == .warning, - Statistics.shared.violations(severity: .warning, excludeAutocorrected: outputFormat == .xcode).isFilled - { - log.exit(fail: true) - } - else { - log.exit(status: .success) - } - - - - + log.message("Start linting using config file at \(config) ...", level: .info) + var lintResults: LintResults = [.info: [:], .warning: [:], .error: [:]] + + // run `FileContents` checks + for fileContentsConfig in lintConfig.fileContents { + let violations = try Lint.checkFileContents( + checkInfo: fileContentsConfig.checkInfo, + regex: fileContentsConfig.regex, + matchingExamples: fileContentsConfig.matchingExamples, + nonMatchingExamples: fileContentsConfig.nonMatchingExamples, + includeFilters: fileContentsConfig.includeFilters, + excludeFilters: fileContentsConfig.excludeFilters, + autoCorrectReplacement: fileContentsConfig.autoCorrectReplacement, + autoCorrectExamples: fileContentsConfig.autoCorrectExamples, + repeatIfAutoCorrected: fileContentsConfig.repeatIfAutoCorrected + ) + lintResults.appendViolations(violations, forCheck: fileContentsConfig.checkInfo) + } + // run `FilePaths` checks + for filePathsConfig in lintConfig.filePaths { + let violations = try Lint.checkFilePaths( + checkInfo: filePathsConfig.checkInfo, + regex: filePathsConfig.regex, + matchingExamples: filePathsConfig.matchingExamples, + nonMatchingExamples: filePathsConfig.nonMatchingExamples, + includeFilters: filePathsConfig.includeFilters, + excludeFilters: filePathsConfig.excludeFilters, + autoCorrectReplacement: filePathsConfig.autoCorrectReplacement, + autoCorrectExamples: filePathsConfig.autoCorrectExamples, + violateIfNoMatchesFound: filePathsConfig.violateIfNoMatchesFound + ) + lintResults.appendViolations(violations, forCheck: filePathsConfig.checkInfo) + } + // run `CustomScripts` checks + for customScriptConfig in lintConfig.customScripts { + let violations = try Lint.runCustomScript( + checkInfo: customScriptConfig.checkInfo, + command: customScriptConfig.command + ) - try Lint.logSummaryAndExit(arguments: arguments) { - for checkFileContent in lintConfig.checkFileContents { - try Lint.checkFileContents( - checkInfo: .init(id: checkFileContent.hint, hint: checkFileContent.hint), - regex: .init(checkFileContent.regex), - matchingExamples: checkFileContent.matchingExamples ?? [], - nonMatchingExamples: checkFileContent.nonMatchingExamples ?? [], - includeFilters: checkFileContent.includeFilters ?? [Regex(".*")], - excludeFilters: checkFileContent.excludeFilters ?? [], - autoCorrectReplacement: checkFileContent.autoCorrectReplacement, - autoCorrectExamples: checkFileContent.autoCorrectExamples ?? [], - repeatIfAutoCorrected: checkFileContent.repeatIfAutoCorrected ?? false - ) - } + lintResults.appendViolations(violations, forCheck: customScriptConfig.checkInfo) + } - for checkFilePath in lintConfig.checkFilePaths { - try Lint.checkFilePaths( - checkInfo: .init(id: checkFilePath.id, hint: checkFilePath.hint), - regex: .init(checkFilePath.regex), - matchingExamples: checkFilePath.matchingExamples ?? [], - nonMatchingExamples: checkFilePath.nonMatchingExamples ?? [], - includeFilters: checkFilePath.includeFilters ?? [Regex(".*")], - excludeFilters: checkFilePath.excludeFilters ?? [], - autoCorrectReplacement: checkFilePath.autoCorrectReplacement, - autoCorrectExamples: checkFilePath.autoCorrectExamples ?? [], - violateIfNoMatchesFound: checkFilePath.violateIfNoMatchesFound ?? false - ) - } - } + // report violations & exit with right status code + lintResults.report(outputFormat: outputFormat) - log.message("Linting successful using config file at \(config). Congrats! 🎉", level: .success) + if lintResults.violations(severity: .error, excludeAutocorrected: outputFormat == .xcode).isFilled { + log.exit(fail: true) } - catch is RunError { - if log.outputType != .xcode { - log.message("Linting failed using config file at \(config).", level: .error) - } - - throw LintError.configFileFailed + else if failLevel == .warning, + lintResults.violations(severity: .warning, excludeAutocorrected: outputFormat == .xcode).isFilled + { + log.exit(fail: true) + } + else { + log.message("Linting successful using config file at \(config). Congrats! 🎉", level: .success) + log.exit(fail: false) } } } extension Severity: ExpressibleByArgument {} extension OutputFormat: ExpressibleByArgument {} + +extension CheckConfiguration { + var checkInfo: CheckInfo { + .init(id: id, hint: hint, severity: severity) + } +} diff --git a/Sources/Configuration/LintConfiguration.swift b/Sources/Configuration/LintConfiguration.swift index 7d37da2..6489646 100644 --- a/Sources/Configuration/LintConfiguration.swift +++ b/Sources/Configuration/LintConfiguration.swift @@ -19,8 +19,15 @@ public struct LintConfiguration: Codable { public let customScripts: [CustomScriptsConfiguration] } +/// Defines fields each check configuration needs to have. +public protocol CheckConfiguration { + var id: String { get } + var hint: String { get } + var severity: Severity { get } +} + /// The `FileContents` check configuration type. -public struct FileContentsConfiguration: Codable { +public struct FileContentsConfiguration: CheckConfiguration, Codable { /// A unique identifier for the check to show on violations. Required. public let id: String @@ -31,29 +38,29 @@ public struct FileContentsConfiguration: Codable { public var severity: Severity = .error /// The regular expression to use to find violations. Required. - public let regex: String + public let regex: Regex /// A list of strings that are expected to match the provided `regex`. Optional. /// /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public let matchingExamples: [String]? + public var matchingExamples: [String] = [] /// A list of strings that are expected to **not** to match the provided `regex`. Optional. /// /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public let nonMatchingExamples: [String]? + public var nonMatchingExamples: [String] = [] /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. /// /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. - public let includeFilters: [Regex]? + public var includeFilters: [Regex] = [try! Regex(".*")] /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. /// /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' - public let excludeFilters: [Regex]? + public var excludeFilters: [Regex] = [] /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. /// @@ -67,7 +74,7 @@ public struct FileContentsConfiguration: Codable { /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. /// /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. - public let autoCorrectExamples: [AutoCorrection]? + public var autoCorrectExamples: [AutoCorrection] = [] /// If set to `true`, a check will be re-run if there was at least one auto-correction applied on the last run. Optional. /// @@ -75,11 +82,11 @@ public struct FileContentsConfiguration: Codable { /// For example, to ensure long numbers are separated by an underscore, you could write the regex `(\d+)(\d{3})` /// and specify the replacement `$1_$2$3`. By default, the number `123456789` would be transformed to `123456_789`. /// With this option set to `true`, the check would be re-executed after the first run (because there was a correction) and the result would be `123_456_789`. - public let repeatIfAutoCorrected: Bool? + public var repeatIfAutoCorrected: Bool = false } /// The `FilePaths` check configuration type. -public struct FilePathsConfiguration: Codable { +public struct FilePathsConfiguration: CheckConfiguration, Codable { /// A unique identifier for the check to show on violations. public let id: String @@ -90,29 +97,29 @@ public struct FilePathsConfiguration: Codable { public var severity: Severity = .error /// The regular expression to use to find violations. Required. - public let regex: String + public let regex: Regex /// A list of strings that are expected to match the provided `regex`. Optional. /// /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public let matchingExamples: [String]? + public var matchingExamples: [String] = [] /// A list of strings that are expected to **not** to match the provided `regex`. Optional. /// /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public let nonMatchingExamples: [String]? + public var nonMatchingExamples: [String] = [] /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. /// /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. - public let includeFilters: [Regex]? + public var includeFilters: [Regex] = [try! Regex(".*")] /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. /// /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' - public let excludeFilters: [Regex]? + public var excludeFilters: [Regex] = [] /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. /// @@ -128,16 +135,19 @@ public struct FilePathsConfiguration: Codable { /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. /// /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. - public let autoCorrectExamples: [AutoCorrection]? + public var autoCorrectExamples: [AutoCorrection] = [] /// If set to `true`, a violation will be reported if **no** matches are found. By default (or if set to `false`), a check violates on every matching file path. - public let violateIfNoMatchesFound: Bool? + public var violateIfNoMatchesFound: Bool = false } /// The `CustomScripts` check configuration type. -public struct CustomScriptsConfiguration: Codable { - /// The name of the custom script. - public let name: String +public struct CustomScriptsConfiguration: CheckConfiguration, Codable { + /// A unique identifier for the check to show on violations. + public let id: String + + /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. + public let hint: String /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. public var severity: Severity = .error diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/CheckInfo.swift index 05801a3..e7f9610 100644 --- a/Sources/Core/CheckInfo.swift +++ b/Sources/Core/CheckInfo.swift @@ -28,61 +28,3 @@ extension CheckInfo: Hashable { hasher.combine(id) } } - -//extension CheckInfo: CustomStringConvertible { -// public var description: String { -// "check '\(id)'" -// } -//} -// -//extension CheckInfo: ExpressibleByStringLiteral { -// 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]! -// -// 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(fail: true) -// exit(EXIT_FAILURE) // only reachable in unit tests -// } -// -// 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(fail: true) -// exit(EXIT_FAILURE) // only reachable in unit tests -// } -// -// let id = defaultSeverityMatch.captures[0]! -// let hint = defaultSeverityMatch.captures[2]! -// -// self = CheckInfo(id: id, hint: hint) -// } -// } -//} diff --git a/Sources/Core/Extensions/StringExt.swift b/Sources/Core/Extensions/StringExt.swift index 9b6ca68..d0e56d9 100644 --- a/Sources/Core/Extensions/StringExt.swift +++ b/Sources/Core/Extensions/StringExt.swift @@ -72,7 +72,6 @@ extension String { guard let pathUrl = URL(string: self) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) log.exit(fail: true) - return "" // only reachable in unit tests } return pathUrl.appendingPathComponent(pathComponent).absoluteString diff --git a/Sources/Core/Logger.swift b/Sources/Core/Logger.swift index 1e653d6..c039607 100644 --- a/Sources/Core/Logger.swift +++ b/Sources/Core/Logger.swift @@ -63,7 +63,7 @@ public final class Logger { } /// Exits the current program with the given fail state. - public func exit(fail: Bool) { + public func exit(fail: Bool) -> Never { let statusCode = fail ? EXIT_FAILURE : EXIT_SUCCESS #if os(Linux) diff --git a/Sources/Core/Severity.swift b/Sources/Core/Severity.swift index 2c83ea5..cca08aa 100644 --- a/Sources/Core/Severity.swift +++ b/Sources/Core/Severity.swift @@ -11,22 +11,6 @@ public enum Severity: String, CaseIterable, Codable { /// Use for checks that probably are problematic. case error - - // static func from(string: String) -> Severity? { - // switch string { - // case "info", "i": - // return .info - // - // case "warning", "w": - // return .warning - // - // case "error", "e": - // return .error - // - // default: - // return nil - // } - // } } extension Severity: Comparable { diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index a0762a5..4096036 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -23,6 +23,16 @@ extension LintResults { } } + /// Appends the violations for the provided check to the results. + public mutating func appendViolations(_ violations: [Violation], forCheck checkInfo: CheckInfo) { + assert( + keys.contains(checkInfo.severity), + "Trying to add violations for severity \(checkInfo.severity) to LintResults without having initialized the severity key." + ) + + self[checkInfo.severity]![checkInfo] = violations + } + /// Logs the summary of the violations in the specified output format. public func report(outputFormat: OutputFormat) { let executedChecks = allExecutedChecks @@ -50,7 +60,13 @@ extension LintResults { } } - func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { + /// Used to get validations for a specific severity level. + /// + /// - Parameters: + /// - severity: The severity 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 severity level. + public func violations(severity: Severity, excludeAutocorrected: Bool) -> [Violation] { guard let violations = self[severity]?.values.flatMap({ $0 }) else { return [] } guard excludeAutocorrected else { return violations } return violations.filter { $0.appliedAutoCorrection == nil } @@ -134,6 +150,6 @@ extension LintResults { } private func reportToFile(at path: String) { - + // TODO: [cg_2021-07-09] not yet implemented } } From 0aa719985919715ca01116299cc5d1c37f947d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 31 Jul 2021 15:37:56 +0200 Subject: [PATCH 09/37] Move tests to new split structure & comment out --- Package.resolved | 8 +- Sources/Checkers/Checker.swift | 1 + Sources/Commands/AnyLint.swift | 1 + Sources/Commands/InitCommand.swift | 3 + Sources/Commands/LintCommand.swift | 4 + .../Commands/OptionsStringConvertible.swift | 2 + .../Configuration/CheckConfiguration.swift | 9 + .../CustomScriptsConfiguration.swift | 19 ++ .../FileContentsConfiguration.swift | 61 +++++ .../FilePathsConfiguration.swift | 58 +++++ Sources/Configuration/LintConfiguration.swift | 139 ---------- Sources/Configuration/Template.swift | 2 +- Tests/AnyLintCLITests/AnyLintCLITests.swift | 7 - Tests/AnyLintTests/AutoCorrectionTests.swift | 37 --- Tests/AnyLintTests/CheckInfoTests.swift | 33 --- .../Checkers/FileContentsCheckerTests.swift | 246 ------------------ .../Checkers/FilePathsCheckerTests.swift | 76 ------ .../Extensions/XCTestCaseExt.swift | 34 --- Tests/AnyLintTests/FilesSearchTests.swift | 65 ----- Tests/AnyLintTests/LintTests.swift | 117 --------- Tests/AnyLintTests/LoggerTests.swift | 30 --- Tests/AnyLintTests/RegexExtTests.swift | 17 -- Tests/AnyLintTests/StatisticsTests.swift | 122 --------- Tests/AnyLintTests/ViolationTests.swift | 26 -- .../Extensions/ArrayExtTests.swift | 3 +- .../Extensions/RegexExtTests.swift | 1 + .../FileContentsCheckerTests.swift | 246 ++++++++++++++++++ .../CheckersTests/FilePathsCheckerTests.swift | 76 ++++++ Tests/CheckersTests/FilesSearchTests.swift | 65 +++++ Tests/CheckersTests/LintTests.swift | 117 +++++++++ Tests/CoreTests/AutoCorrectionTests.swift | 37 +++ Tests/CoreTests/RegexExtTests.swift | 17 ++ Tests/CoreTests/ViolationTests.swift | 20 ++ Tests/ReportingTests/StatisticsTests.swift | 116 +++++++++ 34 files changed, 860 insertions(+), 955 deletions(-) create mode 100644 Sources/Configuration/CheckConfiguration.swift create mode 100644 Sources/Configuration/CustomScriptsConfiguration.swift create mode 100644 Sources/Configuration/FileContentsConfiguration.swift create mode 100644 Sources/Configuration/FilePathsConfiguration.swift delete mode 100644 Tests/AnyLintCLITests/AnyLintCLITests.swift delete mode 100644 Tests/AnyLintTests/AutoCorrectionTests.swift delete mode 100644 Tests/AnyLintTests/CheckInfoTests.swift delete mode 100644 Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift delete mode 100644 Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift delete mode 100644 Tests/AnyLintTests/Extensions/XCTestCaseExt.swift delete mode 100644 Tests/AnyLintTests/FilesSearchTests.swift delete mode 100644 Tests/AnyLintTests/LintTests.swift delete mode 100644 Tests/AnyLintTests/LoggerTests.swift delete mode 100644 Tests/AnyLintTests/RegexExtTests.swift delete mode 100644 Tests/AnyLintTests/StatisticsTests.swift delete mode 100644 Tests/AnyLintTests/ViolationTests.swift rename Tests/{AnyLintTests => CheckersTests}/Extensions/ArrayExtTests.swift (95%) rename Tests/{AnyLintTests => CheckersTests}/Extensions/RegexExtTests.swift (99%) create mode 100644 Tests/CheckersTests/FileContentsCheckerTests.swift create mode 100644 Tests/CheckersTests/FilePathsCheckerTests.swift create mode 100644 Tests/CheckersTests/FilesSearchTests.swift create mode 100644 Tests/CheckersTests/LintTests.swift create mode 100644 Tests/CoreTests/AutoCorrectionTests.swift create mode 100644 Tests/CoreTests/RegexExtTests.swift create mode 100644 Tests/CoreTests/ViolationTests.swift create mode 100644 Tests/ReportingTests/StatisticsTests.swift diff --git a/Package.resolved b/Package.resolved index 859b046..1d041d0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser.git", "state": { "branch": null, - "revision": "986d191f94cec88f6350056da59c2e59e83d1229", - "version": "0.4.3" + "revision": "83b23d940471b313427da226196661856f6ba3e0", + "version": "0.4.4" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "d45e63421d3dff834949ac69d3c37691e994bd69", - "version": "0.0.3" + "revision": "0959ba76a1d4a98fd11163aa83fd49c25b93bfae", + "version": "0.0.5" } }, { diff --git a/Sources/Checkers/Checker.swift b/Sources/Checkers/Checker.swift index 5997fe5..351c0c4 100644 --- a/Sources/Checkers/Checker.swift +++ b/Sources/Checkers/Checker.swift @@ -3,5 +3,6 @@ import Core /// Defines how a checker algorithm behaves to produce violations results. public protocol Checker { + /// Executes the checks and returns violations (if any). func performCheck() throws -> [Violation] } diff --git a/Sources/Commands/AnyLint.swift b/Sources/Commands/AnyLint.swift index 073c095..7f2556f 100644 --- a/Sources/Commands/AnyLint.swift +++ b/Sources/Commands/AnyLint.swift @@ -2,6 +2,7 @@ import Foundation import ArgumentParser @main +/// The entry point of the toot, defines the `anylint` primary command. Sets up any sub commands like `init` or `lint`. struct AnyLint: ParsableCommand { static var configuration: CommandConfiguration = .init( commandName: "anylint", diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift index 0cbd049..c5a8753 100644 --- a/Sources/Commands/InitCommand.swift +++ b/Sources/Commands/InitCommand.swift @@ -4,18 +4,21 @@ import Configuration import Core import ShellOut +/// The `init` subcommand helping to get started with AnyLint by setting up a configuration file from a template. struct InitCommand: ParsableCommand { static var configuration: CommandConfiguration = .init( commandName: "init", abstract: "Initializes a new AnyLint configuration file (at specified path & using the specified template)." ) + /// The template option to create the initial config file from. @Option( name: .shortAndLong, help: "The template to create the initial config file from. One of: \(Template.optionsDescription)." ) var template: Template = .blank + /// Path option to the new config file to initialize it at. @Option( name: .shortAndLong, help: "Path to the new config file to initialize it at. If a directory is specified, creates 'anylint.yml' in it." diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index d6ddd1f..9298364 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -12,6 +12,7 @@ struct LintCommand: ParsableCommand { abstract: "Runs the configured checks & reports the results in the specified format." ) + /// The path(s) option to run the checks from. @Option( name: .shortAndLong, parsing: .upToNextOption, @@ -19,12 +20,14 @@ struct LintCommand: ParsableCommand { ) var paths: [String] = [FileManager.default.currentDirectoryUrl.path] + /// Path option to the config file to execute. @Option( name: .shortAndLong, help: .init("Path to the config file to execute.", valueName: "path") ) var config: String = FileManager.default.currentDirectoryUrl.appendingPathComponent("anylint.yml").path + /// The minimum severity level option to fail on if any checks produce violations. @Option( name: .shortAndLong, help: .init( @@ -34,6 +37,7 @@ struct LintCommand: ParsableCommand { ) var failLevel: Severity = .error + /// The expected format option of the output. @Option( name: .shortAndLong, help: .init( diff --git a/Sources/Commands/OptionsStringConvertible.swift b/Sources/Commands/OptionsStringConvertible.swift index 028bcbf..562520a 100644 --- a/Sources/Commands/OptionsStringConvertible.swift +++ b/Sources/Commands/OptionsStringConvertible.swift @@ -3,7 +3,9 @@ import Core import Configuration import Reporting +/// A protocol to output a set of configuration options when asking for help. protocol OptionsStringConvertible { + /// A human readable string representation of the possible options for help text. static var optionsDescription: String { get } } diff --git a/Sources/Configuration/CheckConfiguration.swift b/Sources/Configuration/CheckConfiguration.swift new file mode 100644 index 0000000..fd76e30 --- /dev/null +++ b/Sources/Configuration/CheckConfiguration.swift @@ -0,0 +1,9 @@ +import Foundation +import Core + +/// Defines fields each check configuration needs to have. +public protocol CheckConfiguration { + var id: String { get } + var hint: String { get } + var severity: Severity { get } +} diff --git a/Sources/Configuration/CustomScriptsConfiguration.swift b/Sources/Configuration/CustomScriptsConfiguration.swift new file mode 100644 index 0000000..5ca8f8e --- /dev/null +++ b/Sources/Configuration/CustomScriptsConfiguration.swift @@ -0,0 +1,19 @@ +import Foundation +import Core + +/// The `CustomScripts` check configuration type. +public struct CustomScriptsConfiguration: CheckConfiguration, Codable { + /// A unique identifier for the check to show on violations. + public let id: String + + /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. + public let hint: String + + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. + public var severity: Severity = .error + + /// The custom command line command to execute. + /// If the output conforms to the ``LintResults`` structure formatted as JSON, then the results will be merged. + /// Otherwise AnyLint will violate for any non-zero exit code with the last printed line. + public let command: String +} diff --git a/Sources/Configuration/FileContentsConfiguration.swift b/Sources/Configuration/FileContentsConfiguration.swift new file mode 100644 index 0000000..f80e4dd --- /dev/null +++ b/Sources/Configuration/FileContentsConfiguration.swift @@ -0,0 +1,61 @@ +import Foundation +import Core + +/// The `FileContents` check configuration type. +public struct FileContentsConfiguration: CheckConfiguration, Codable { + /// A unique identifier for the check to show on violations. Required. + public let id: String + + /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. Required. + public let hint: String + + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. + public var severity: Severity = .error + + /// The regular expression to use to find violations. Required. + public let regex: Regex + + /// A list of strings that are expected to match the provided `regex`. Optional. + /// + /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public var matchingExamples: [String] = [] + + /// A list of strings that are expected to **not** to match the provided `regex`. Optional. + /// + /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public var nonMatchingExamples: [String] = [] + + /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. + /// + /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. + public var includeFilters: [Regex] = [try! Regex(".*")] + + /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. + /// + /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' + public var excludeFilters: [Regex] = [] + + /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. + /// + /// See `NSRegularExpression` documentation for examples and more details (e.g. in the 'Template Matching Format' section): + /// https://developer.apple.com/documentation/foundation/nsregularexpression + public let autoCorrectReplacement: String? + + /// A dictionary consisting of the keys `before` and `after` to specify how you would expect a given string to be changed. Optional. + /// + /// Use this to validate that the provided `regex` and the `autoCorrectReplacement` together act as expected. + /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. + /// + /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. + public var autoCorrectExamples: [AutoCorrection] = [] + + /// If set to `true`, a check will be re-run if there was at least one auto-correction applied on the last run. Optional. + /// + /// This can be useful for auto-correcting issues that can scale or repeat. + /// For example, to ensure long numbers are separated by an underscore, you could write the regex `(\d+)(\d{3})` + /// and specify the replacement `$1_$2$3`. By default, the number `123456789` would be transformed to `123456_789`. + /// With this option set to `true`, the check would be re-executed after the first run (because there was a correction) and the result would be `123_456_789`. + public var repeatIfAutoCorrected: Bool = false +} diff --git a/Sources/Configuration/FilePathsConfiguration.swift b/Sources/Configuration/FilePathsConfiguration.swift new file mode 100644 index 0000000..ebb8570 --- /dev/null +++ b/Sources/Configuration/FilePathsConfiguration.swift @@ -0,0 +1,58 @@ +import Foundation +import Core + +/// The `FilePaths` check configuration type. +public struct FilePathsConfiguration: CheckConfiguration, Codable { + /// A unique identifier for the check to show on violations. + public let id: String + + /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. + public let hint: String + + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. + public var severity: Severity = .error + + /// The regular expression to use to find violations. Required. + public let regex: Regex + + /// A list of strings that are expected to match the provided `regex`. Optional. + /// + /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public var matchingExamples: [String] = [] + + /// A list of strings that are expected to **not** to match the provided `regex`. Optional. + /// + /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. + /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. + public var nonMatchingExamples: [String] = [] + + /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. + /// + /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. + public var includeFilters: [Regex] = [try! Regex(".*")] + + /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. + /// + /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' + public var excludeFilters: [Regex] = [] + + /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. + /// + /// See `NSRegularExpression` documentation for examples and more details (e.g. in the 'Template Matching Format' section): + /// https://developer.apple.com/documentation/foundation/nsregularexpression + /// + /// Use this to automatically move violating files from their current paht to the expected path. + public let autoCorrectReplacement: String? + + /// A dictionary consisting of the keys `before` and `after` to specify how you would expect a given path to be changed. Optional. + /// + /// Use this to validate that the provided `regex` and the `autoCorrectReplacement` together act as expected. + /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. + /// + /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. + public var autoCorrectExamples: [AutoCorrection] = [] + + /// If set to `true`, a violation will be reported if **no** matches are found. By default (or if set to `false`), a check violates on every matching file path. + public var violateIfNoMatchesFound: Bool = false +} diff --git a/Sources/Configuration/LintConfiguration.swift b/Sources/Configuration/LintConfiguration.swift index 6489646..4dcef25 100644 --- a/Sources/Configuration/LintConfiguration.swift +++ b/Sources/Configuration/LintConfiguration.swift @@ -18,142 +18,3 @@ public struct LintConfiguration: Codable { /// The list of `CustomScripts` checks. public let customScripts: [CustomScriptsConfiguration] } - -/// Defines fields each check configuration needs to have. -public protocol CheckConfiguration { - var id: String { get } - var hint: String { get } - var severity: Severity { get } -} - -/// The `FileContents` check configuration type. -public struct FileContentsConfiguration: CheckConfiguration, Codable { - /// A unique identifier for the check to show on violations. Required. - public let id: String - - /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. Required. - public let hint: String - - /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. - public var severity: Severity = .error - - /// The regular expression to use to find violations. Required. - public let regex: Regex - - /// A list of strings that are expected to match the provided `regex`. Optional. - /// - /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. - /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var matchingExamples: [String] = [] - - /// A list of strings that are expected to **not** to match the provided `regex`. Optional. - /// - /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. - /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var nonMatchingExamples: [String] = [] - - /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. - /// - /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. - public var includeFilters: [Regex] = [try! Regex(".*")] - - /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. - /// - /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' - public var excludeFilters: [Regex] = [] - - /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. - /// - /// See `NSRegularExpression` documentation for examples and more details (e.g. in the 'Template Matching Format' section): - /// https://developer.apple.com/documentation/foundation/nsregularexpression - public let autoCorrectReplacement: String? - - /// A dictionary consisting of the keys `before` and `after` to specify how you would expect a given string to be changed. Optional. - /// - /// Use this to validate that the provided `regex` and the `autoCorrectReplacement` together act as expected. - /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. - /// - /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. - public var autoCorrectExamples: [AutoCorrection] = [] - - /// If set to `true`, a check will be re-run if there was at least one auto-correction applied on the last run. Optional. - /// - /// This can be useful for auto-correcting issues that can scale or repeat. - /// For example, to ensure long numbers are separated by an underscore, you could write the regex `(\d+)(\d{3})` - /// and specify the replacement `$1_$2$3`. By default, the number `123456789` would be transformed to `123456_789`. - /// With this option set to `true`, the check would be re-executed after the first run (because there was a correction) and the result would be `123_456_789`. - public var repeatIfAutoCorrected: Bool = false -} - -/// The `FilePaths` check configuration type. -public struct FilePathsConfiguration: CheckConfiguration, Codable { - /// A unique identifier for the check to show on violations. - public let id: String - - /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. - public let hint: String - - /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. - public var severity: Severity = .error - - /// The regular expression to use to find violations. Required. - public let regex: Regex - - /// A list of strings that are expected to match the provided `regex`. Optional. - /// - /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. - /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var matchingExamples: [String] = [] - - /// A list of strings that are expected to **not** to match the provided `regex`. Optional. - /// - /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. - /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var nonMatchingExamples: [String] = [] - - /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. - /// - /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. - public var includeFilters: [Regex] = [try! Regex(".*")] - - /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. - /// - /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' - public var excludeFilters: [Regex] = [] - - /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. - /// - /// See `NSRegularExpression` documentation for examples and more details (e.g. in the 'Template Matching Format' section): - /// https://developer.apple.com/documentation/foundation/nsregularexpression - /// - /// Use this to automatically move violating files from their current paht to the expected path. - public let autoCorrectReplacement: String? - - /// A dictionary consisting of the keys `before` and `after` to specify how you would expect a given path to be changed. Optional. - /// - /// Use this to validate that the provided `regex` and the `autoCorrectReplacement` together act as expected. - /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. - /// - /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. - public var autoCorrectExamples: [AutoCorrection] = [] - - /// If set to `true`, a violation will be reported if **no** matches are found. By default (or if set to `false`), a check violates on every matching file path. - public var violateIfNoMatchesFound: Bool = false -} - -/// The `CustomScripts` check configuration type. -public struct CustomScriptsConfiguration: CheckConfiguration, Codable { - /// A unique identifier for the check to show on violations. - public let id: String - - /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. - public let hint: String - - /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. - public var severity: Severity = .error - - /// The custom command line command to execute. - /// If the output conforms to the ``LintResults`` structure formatted as JSON, then the results will be merged. - /// Otherwise AnyLint will violate for any non-zero exit code with the last printed line. - public let command: String -} diff --git a/Sources/Configuration/Template.swift b/Sources/Configuration/Template.swift index c71e815..95ff9ad 100644 --- a/Sources/Configuration/Template.swift +++ b/Sources/Configuration/Template.swift @@ -1,6 +1,6 @@ import Foundation -/// The template for setting up configuration initially. +/// The possible templates for setting up configuration initially. public enum Template: String, CaseIterable { /// The blank template with all existing checks and one 'Hello world' kind of example per check. case blank diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift deleted file mode 100644 index 46746f2..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 deleted file mode 100644 index dd97d23..0000000 --- a/Tests/AnyLintTests/AutoCorrectionTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -@testable import AnyLint -import XCTest - -final class AutoCorrectionTests: XCTestCase { - 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", - ] - ) - - 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 deleted file mode 100644 index 6d30f31..0000000 --- a/Tests/AnyLintTests/CheckInfoTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -@testable import AnyLint -import XCTest - -final class CheckInfoTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testInitWithStringLiteral() { - XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) - - let checkInfo1: CheckInfo = "test1@error: hint1" - XCTAssertEqual(checkInfo1.id, "test1") - XCTAssertEqual(checkInfo1.hint, "hint1") - XCTAssertEqual(checkInfo1.severity, .error) - - let checkInfo2: CheckInfo = "test2@warning: hint2" - XCTAssertEqual(checkInfo2.id, "test2") - XCTAssertEqual(checkInfo2.hint, "hint2") - XCTAssertEqual(checkInfo2.severity, .warning) - - let checkInfo3: CheckInfo = "test3@info: hint3" - XCTAssertEqual(checkInfo3.id, "test3") - XCTAssertEqual(checkInfo3.hint, "hint3") - XCTAssertEqual(checkInfo3.severity, .info) - - let checkInfo4: CheckInfo = "test4: hint4" - XCTAssertEqual(checkInfo4.id, "test4") - XCTAssertEqual(checkInfo4.hint, "hint4") - XCTAssertEqual(checkInfo4.severity, .error) - } -} diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift deleted file mode 100644 index 478584b..0000000 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ /dev/null @@ -1,246 +0,0 @@ -@testable import AnyLint -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+"#, - 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+"#, - 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+"#, - 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+)"#, - 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 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 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/XCTestCaseExt.swift b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift deleted file mode 100644 index 7bb0835..0000000 --- a/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift +++ /dev/null @@ -1,34 +0,0 @@ -@testable import AnyLint -import Foundation -import XCTest - -extension XCTestCase { - typealias TemporaryFile = (subpath: String, contents: String) - - var tempDir: String { "AnyLintTempTests" } - - func withTemporaryFiles(_ temporaryFiles: [TemporaryFile], testCode: ([String]) throws -> Void) { - var filePathsToCheck: [String] = [] - - for tempFile in temporaryFiles { - let tempFileUrl = FileManager.default.currentDirectoryUrl.appendingPathComponent(tempDir) - .appendingPathComponent(tempFile.subpath) - let tempFileParentDirUrl = tempFileUrl.deletingLastPathComponent() - 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? FileManager.default.removeItem(atPath: tempDir) - } -} diff --git a/Tests/AnyLintTests/FilesSearchTests.swift b/Tests/AnyLintTests/FilesSearchTests.swift deleted file mode 100644 index 7a1d539..0000000 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ /dev/null @@ -1,65 +0,0 @@ -@testable import AnyLint -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"] - ) - 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)" })) - - 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 deleted file mode 100644 index 2a17f6b..0000000 --- a/Tests/AnyLintTests/LintTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -@testable import AnyLint -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) - } -} diff --git a/Tests/AnyLintTests/LoggerTests.swift b/Tests/AnyLintTests/LoggerTests.swift deleted file mode 100644 index 5fdc282..0000000 --- a/Tests/AnyLintTests/LoggerTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -import XCTest - -final class LoggerTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testMessage() { - XCTAssert(TestHelper.shared.consoleOutputs.isEmpty) - - 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") - - 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") - - 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") - } -} diff --git a/Tests/AnyLintTests/RegexExtTests.swift b/Tests/AnyLintTests/RegexExtTests.swift deleted file mode 100644 index b3b9397..0000000 --- a/Tests/AnyLintTests/RegexExtTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -@testable import AnyLint -import XCTest - -final class RegexExtTests: XCTestCase { - 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.*)"#) - } -} diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift deleted file mode 100644 index 5bd311c..0000000 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ /dev/null @@ -1,122 +0,0 @@ -@testable import AnyLint -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() - 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() - - XCTAssertEqual( - TestHelper.shared.consoleOutputs.map { $0.level }, - [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] - ) - - let expectedOutputs = [ - "\("[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 deleted file mode 100644 index 059f486..0000000 --- a/Tests/AnyLintTests/ViolationTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -@testable import AnyLint -import XCTest - -final class ViolationTests: XCTestCase { - 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)) - - let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") - XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") - - let locationInfoViolation = Violation( - checkInfo: checkInfo, - filePath: "Temp/Souces/World.swift", - locationInfo: String.LocationInfo(line: 5, charInLine: 15) - ) - - XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") - } -} diff --git a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift b/Tests/CheckersTests/Extensions/ArrayExtTests.swift similarity index 95% rename from Tests/AnyLintTests/Extensions/ArrayExtTests.swift rename to Tests/CheckersTests/Extensions/ArrayExtTests.swift index 2594899..afc7a09 100644 --- a/Tests/AnyLintTests/Extensions/ArrayExtTests.swift +++ b/Tests/CheckersTests/Extensions/ArrayExtTests.swift @@ -1,4 +1,5 @@ -@testable import AnyLint +@testable import Checkers +import Core import XCTest final class ArrayExtTests: XCTestCase { diff --git a/Tests/AnyLintTests/Extensions/RegexExtTests.swift b/Tests/CheckersTests/Extensions/RegexExtTests.swift similarity index 99% rename from Tests/AnyLintTests/Extensions/RegexExtTests.swift rename to Tests/CheckersTests/Extensions/RegexExtTests.swift index f6253e7..6f88580 100644 --- a/Tests/AnyLintTests/Extensions/RegexExtTests.swift +++ b/Tests/CheckersTests/Extensions/RegexExtTests.swift @@ -1,3 +1,4 @@ +import Core import XCTest final class RegexExtTests: XCTestCase { diff --git a/Tests/CheckersTests/FileContentsCheckerTests.swift b/Tests/CheckersTests/FileContentsCheckerTests.swift new file mode 100644 index 0000000..64aeb27 --- /dev/null +++ b/Tests/CheckersTests/FileContentsCheckerTests.swift @@ -0,0 +1,246 @@ +@testable import Checkers +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+"#, + // 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+"#, + // 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+"#, + // 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+)"#, + // 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 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 noWorldCheck() -> CheckInfo { + // CheckInfo(id: "no_world", hint: "Do not include the global world, be more specific instead.", severity: .error) + // } +} diff --git a/Tests/CheckersTests/FilesSearchTests.swift b/Tests/CheckersTests/FilesSearchTests.swift new file mode 100644 index 0000000..c32ff0a --- /dev/null +++ b/Tests/CheckersTests/FilesSearchTests.swift @@ -0,0 +1,65 @@ +@testable import Checkers +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"] + // ) + // 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)" })) + // + // 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/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift new file mode 100644 index 0000000..c372de2 --- /dev/null +++ b/Tests/CheckersTests/LintTests.swift @@ -0,0 +1,117 @@ +@testable import Checkers +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) + // } +} diff --git a/Tests/CoreTests/AutoCorrectionTests.swift b/Tests/CoreTests/AutoCorrectionTests.swift new file mode 100644 index 0000000..2a53135 --- /dev/null +++ b/Tests/CoreTests/AutoCorrectionTests.swift @@ -0,0 +1,37 @@ +@testable import Core +import XCTest + +final class AutoCorrectionTests: XCTestCase { + // 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", + // ] + // ) + // + // 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/CoreTests/RegexExtTests.swift b/Tests/CoreTests/RegexExtTests.swift new file mode 100644 index 0000000..dd67c27 --- /dev/null +++ b/Tests/CoreTests/RegexExtTests.swift @@ -0,0 +1,17 @@ +@testable import Core +import XCTest + +final class RegexExtTests: XCTestCase { + // 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.*)"#) + // } +} diff --git a/Tests/CoreTests/ViolationTests.swift b/Tests/CoreTests/ViolationTests.swift new file mode 100644 index 0000000..4b31cf6 --- /dev/null +++ b/Tests/CoreTests/ViolationTests.swift @@ -0,0 +1,20 @@ +@testable import Core +import XCTest + +final class ViolationTests: XCTestCase { + // func testLocationMessage() { + // let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) + // XCTAssertNil(Violation(checkInfo: checkInfo).locationMessage(pathType: .relative)) + // + // let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") + // XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") + // + // let locationInfoViolation = Violation( + // checkInfo: checkInfo, + // filePath: "Temp/Souces/World.swift", + // locationInfo: String.LocationInfo(line: 5, charInLine: 15) + // ) + // + // XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") + // } +} diff --git a/Tests/ReportingTests/StatisticsTests.swift b/Tests/ReportingTests/StatisticsTests.swift new file mode 100644 index 0000000..128d321 --- /dev/null +++ b/Tests/ReportingTests/StatisticsTests.swift @@ -0,0 +1,116 @@ +@testable import Core +import XCTest + +final class StatisticsTests: XCTestCase { + // 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() + // 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() + // + // XCTAssertEqual( + // TestHelper.shared.consoleOutputs.map { $0.level }, + // [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] + // ) + // + // let expectedOutputs = [ + // "\("[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) + // } + // } +} From 447f8bd30e36b879ddbd7731ac49a0836dcde398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 31 Jul 2021 15:44:36 +0200 Subject: [PATCH 10/37] Update .gitignore file for Swift packages --- .gitignore | 92 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 81b332f..a016df8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,87 @@ +# macOS + +# General .DS_Store -/.build -/Packages -/*.xcodeproj +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +# Swift + +# Xcode +## User settings xcuserdata/ -/AnyLintTempTests -.codacy-coverage -*.lcov -codacy-coverage.json + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +Packages/ +Package.pins +Package.resolved +*.xcodeproj + +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm +.build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output From 52d6a06fdeb1e170353043d557481d86cb21988a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 31 Jul 2021 15:50:54 +0200 Subject: [PATCH 11/37] Add tests stubs for all targets This ensures Xcode supports showing the file-specific coverage in the editor. --- Tests/CheckersTests/FileContentsCheckerTests.swift | 6 ++++-- Tests/CheckersTests/FilePathsCheckerTests.swift | 4 ++++ Tests/CheckersTests/FilesSearchTests.swift | 6 ++++-- Tests/CheckersTests/LintTests.swift | 4 ++++ Tests/CommandsTests/AnyLintTests.swift | 8 ++++++++ Tests/ConfigurationTests/CheckConfigurationTests.swift | 8 ++++++++ Tests/CoreTests/AutoCorrectionTests.swift | 4 ++++ Tests/CoreTests/RegexExtTests.swift | 4 ++++ Tests/CoreTests/ViolationTests.swift | 4 ++++ Tests/ReportingTests/StatisticsTests.swift | 6 +++++- 10 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 Tests/CommandsTests/AnyLintTests.swift create mode 100644 Tests/ConfigurationTests/CheckConfigurationTests.swift diff --git a/Tests/CheckersTests/FileContentsCheckerTests.swift b/Tests/CheckersTests/FileContentsCheckerTests.swift index 64aeb27..7d32307 100644 --- a/Tests/CheckersTests/FileContentsCheckerTests.swift +++ b/Tests/CheckersTests/FileContentsCheckerTests.swift @@ -1,9 +1,11 @@ @testable import Checkers import XCTest -// swiftlint:disable function_body_length - final class FileContentsCheckerTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // override func setUp() { // log = Logger(outputType: .test) // TestHelper.shared.reset() diff --git a/Tests/CheckersTests/FilePathsCheckerTests.swift b/Tests/CheckersTests/FilePathsCheckerTests.swift index 5bbe264..771ccdb 100644 --- a/Tests/CheckersTests/FilePathsCheckerTests.swift +++ b/Tests/CheckersTests/FilePathsCheckerTests.swift @@ -2,6 +2,10 @@ import XCTest final class FilePathsCheckerTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // override func setUp() { // log = Logger(outputType: .test) // TestHelper.shared.reset() diff --git a/Tests/CheckersTests/FilesSearchTests.swift b/Tests/CheckersTests/FilesSearchTests.swift index c32ff0a..3c93c6e 100644 --- a/Tests/CheckersTests/FilesSearchTests.swift +++ b/Tests/CheckersTests/FilesSearchTests.swift @@ -1,9 +1,11 @@ @testable import Checkers import XCTest -// swiftlint:disable force_try - final class FilesSearchTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // override func setUp() { // log = Logger(outputType: .test) // TestHelper.shared.reset() diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index c372de2..bb9bc1b 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -2,6 +2,10 @@ import XCTest final class LintTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // override func setUp() { // log = Logger(outputType: .test) // TestHelper.shared.reset() diff --git a/Tests/CommandsTests/AnyLintTests.swift b/Tests/CommandsTests/AnyLintTests.swift new file mode 100644 index 0000000..891ca68 --- /dev/null +++ b/Tests/CommandsTests/AnyLintTests.swift @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +final class AnyLintTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } +} diff --git a/Tests/ConfigurationTests/CheckConfigurationTests.swift b/Tests/ConfigurationTests/CheckConfigurationTests.swift new file mode 100644 index 0000000..a7a6cdf --- /dev/null +++ b/Tests/ConfigurationTests/CheckConfigurationTests.swift @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +final class CheckConfigurationTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } +} diff --git a/Tests/CoreTests/AutoCorrectionTests.swift b/Tests/CoreTests/AutoCorrectionTests.swift index 2a53135..94c4559 100644 --- a/Tests/CoreTests/AutoCorrectionTests.swift +++ b/Tests/CoreTests/AutoCorrectionTests.swift @@ -2,6 +2,10 @@ import XCTest final class AutoCorrectionTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // func testInitWithDictionaryLiteral() { // let autoCorrection: AutoCorrection = ["before": "Lisence", "after": "License"] // XCTAssertEqual(autoCorrection.before, "Lisence") diff --git a/Tests/CoreTests/RegexExtTests.swift b/Tests/CoreTests/RegexExtTests.swift index dd67c27..f0a145e 100644 --- a/Tests/CoreTests/RegexExtTests.swift +++ b/Tests/CoreTests/RegexExtTests.swift @@ -2,6 +2,10 @@ import XCTest final class RegexExtTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // func testInitWithStringLiteral() { // let regex: Regex = #"(?capture[_\-\.]group)\s+\n(.*)"# // XCTAssertEqual(regex.pattern, #"(?capture[_\-\.]group)\s+\n(.*)"#) diff --git a/Tests/CoreTests/ViolationTests.swift b/Tests/CoreTests/ViolationTests.swift index 4b31cf6..d4683c9 100644 --- a/Tests/CoreTests/ViolationTests.swift +++ b/Tests/CoreTests/ViolationTests.swift @@ -2,6 +2,10 @@ import XCTest final class ViolationTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // 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)) diff --git a/Tests/ReportingTests/StatisticsTests.swift b/Tests/ReportingTests/StatisticsTests.swift index 128d321..2879137 100644 --- a/Tests/ReportingTests/StatisticsTests.swift +++ b/Tests/ReportingTests/StatisticsTests.swift @@ -2,6 +2,10 @@ import XCTest final class StatisticsTests: XCTestCase { + func testSample() { + XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + } + // func testFoundViolationsInCheck() { // XCTAssert(Statistics.shared.executedChecks.isEmpty) // XCTAssert(Statistics.shared.violationsBySeverity[.info]!.isEmpty) @@ -48,7 +52,7 @@ final class StatisticsTests: XCTestCase { // XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 3) // } // - // func testLogSummary() { // swiftlint:disable:this function_body_length + // func testLogSummary() { // Statistics.shared.logCheckSummary() // XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) // XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) From 56d33bd6e48fae1a32eebf70e3fba9d76def7716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Aug 2021 17:20:25 +0200 Subject: [PATCH 12/37] Reconfigure GitHub Actions & downgrade Swift --- .github/workflows/main.yml | 25 +----------- .github/workflows/pull-request.yml | 45 ++++++++++++++++++++++ CHANGELOG.md | 4 +- LICENSE | 2 +- Package.swift | 4 +- Sources/Checkers/Helpers/FilesSearch.swift | 1 + Sources/Commands/AnyLint.swift | 1 - Sources/Commands/main.swift | 5 +++ Sources/Core/Severity.swift | 1 - lint.swift | 45 +--------------------- 10 files changed, 58 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/pull-request.yml create mode 100644 Sources/Commands/main.swift diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d33c42a..7b1f7f8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,33 +1,10 @@ -name: CI +name: Main 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: Install AnyLint - run: brew tap Flinesoft/AnyLint https://github.com/Flinesoft/AnyLint.git && brew install anylint - - - name: Run AnyLint - run: anylint - test-linux: runs-on: ubuntu-latest diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..19021c0 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,45 @@ +name: Pull Request + +on: + 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: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install AnyLint + run: brew tap Flinesoft/AnyLint https://github.com/Flinesoft/AnyLint.git && brew install anylint + + - name: Run AnyLint + run: anylint + + test-linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Run tests + run: swift test -v --enable-test-discovery + + test-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Run tests + run: swift test -v --enable-code-coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b005ab..56babd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ## [Unreleased] ### Added -- YAML-based configuration file. Supportts `FilePaths` and `FileContents` as well as `CustomScripts` for full feature parity with previous Swift-based configuration file. +- YAML-based configuration file. Supports `FilePaths` and `FileContents` as well as `CustomScripts` for full feature parity with previous Swift-based configuration file. Author: [Cihat Gündüz](https://github.com/Jeehut) ### Changed - Migrated from jakeheis/SwiftCLI to apple/swift-argument-parser for improved reliability & reduced maintenance. @@ -31,7 +31,7 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se ### Removed - Swift-based configuration file support removed in favor of YAML-based configuration. All features supported via the Swift file still supported via YAML file. See README.md for more details. Parameters were not renamed to keep migration simple. Author: [Cihat Gündüz](https://github.com/Jeehut) -- Support for Swift versions below 5.5 was dropped to make use of the latest improvements in concurrency & SwiftPM plugin system. Use version `0.8.2` if you need to stay on lower Swift versions. +- Support for Swift versions below 5.4 was dropped to make use of the latest improvements in Swift & SwiftPM. Use version `0.8.2` if you need to stay on lower Swift versions. Author: [Cihat Gündüz](https://github.com/Jeehut) ### Fixed - Issues with paths due to Swift scripting not being as easy to use now fixed by moving over to YAML-based configuration. For custom scripts, responsibility is moved to the user side by allowing to specify the exact command to run. diff --git a/LICENSE b/LICENSE index b1efe33..0f84aa7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Flinesoft (alias Cihat Gündüz) +Copyright (c) 2020-2021 Flinesoft (alias Cihat Gündüz) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift index dfa877d..8bebfe8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.3 import PackageDescription let package = Package( @@ -44,7 +44,7 @@ let package = Package( .product(name: "OrderedCollections", package: "swift-collections"), ] ), - .executableTarget( + .target( name: "Commands", dependencies: [ "Checkers", diff --git a/Sources/Checkers/Helpers/FilesSearch.swift b/Sources/Checkers/Helpers/FilesSearch.swift index 54e193a..0a47633 100644 --- a/Sources/Checkers/Helpers/FilesSearch.swift +++ b/Sources/Checkers/Helpers/FilesSearch.swift @@ -33,6 +33,7 @@ public final class FilesSearch { excludeFilters: excludeFilters ) + // AnyLint.skipHere: IfAsGuard if let cachedFilePaths: [String] = cachedFilePaths[searchOptions] { return cachedFilePaths } diff --git a/Sources/Commands/AnyLint.swift b/Sources/Commands/AnyLint.swift index 7f2556f..18059c1 100644 --- a/Sources/Commands/AnyLint.swift +++ b/Sources/Commands/AnyLint.swift @@ -1,7 +1,6 @@ import Foundation import ArgumentParser -@main /// The entry point of the toot, defines the `anylint` primary command. Sets up any sub commands like `init` or `lint`. struct AnyLint: ParsableCommand { static var configuration: CommandConfiguration = .init( diff --git a/Sources/Commands/main.swift b/Sources/Commands/main.swift new file mode 100644 index 0000000..dba66a6 --- /dev/null +++ b/Sources/Commands/main.swift @@ -0,0 +1,5 @@ +import ArgumentParser + +// TODO: [cg_2021-08-15] remove in favor of `@main` in AnyLint struct once GitHub has macos-11 available: +// https://github.com/actions/virtual-environments/issues/2486 +AnyLint.main() diff --git a/Sources/Core/Severity.swift b/Sources/Core/Severity.swift index cca08aa..a1c07ad 100644 --- a/Sources/Core/Severity.swift +++ b/Sources/Core/Severity.swift @@ -1,5 +1,4 @@ import Foundation -import AppKit /// Defines the severity of a lint check. public enum Severity: String, CaseIterable, Codable { diff --git a/lint.swift b/lint.swift index 4265c8a..cb0499a 100755 --- a/lint.swift +++ b/lint.swift @@ -1,5 +1,5 @@ #!/usr/local/bin/swift-sh -import AnyLint // . +import AnyLint // @Flinesoft import Utility import ShellOut // @JohnSundell @@ -440,49 +440,6 @@ try Lint.logSummaryAndExit(arguments: CommandLine.arguments) { ] ) - // 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 violations - } - // MARK: Logger try Lint.checkFileContents( checkInfo: "Logger: Don't use `print` – use `log.message` instead.", From 76dd519779e5b85b01f9e953f47a1ebeb8f324b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Aug 2021 23:05:17 +0200 Subject: [PATCH 13/37] Add tests for `Template` file in `Configuration` target --- Package.swift | 3 + Sources/Commands/InitCommand.swift | 2 +- Sources/Configuration/LintConfiguration.swift | 4 +- Sources/Configuration/Template.swift | 75 ++++--------------- Sources/Configuration/Templates/Blank.yml | 40 ++++++++++ .../Configuration/Templates/OpenSource.yml | 51 +++++++++++++ Tests/ConfigurationTests/TemplateTests.swift | 30 ++++++++ 7 files changed, 141 insertions(+), 64 deletions(-) create mode 100644 Sources/Configuration/Templates/Blank.yml create mode 100644 Sources/Configuration/Templates/OpenSource.yml create mode 100644 Tests/ConfigurationTests/TemplateTests.swift diff --git a/Package.swift b/Package.swift index 8bebfe8..da0e8a7 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,9 @@ let package = Package( dependencies: [ "Core", .product(name: "Yams", package: "Yams"), + ], + resources: [ + .copy("Templates"), ] ), .target( diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift index c5a8753..a64a0a7 100644 --- a/Sources/Commands/InitCommand.swift +++ b/Sources/Commands/InitCommand.swift @@ -42,7 +42,7 @@ struct InitCommand: ParsableCommand { log.message("Creating config file using template '\(template.rawValue)' ...", level: .info) FileManager.default.createFile( atPath: path, - contents: template.fileContents.data(using: .utf8), + contents: template.fileContents, attributes: nil ) diff --git a/Sources/Configuration/LintConfiguration.swift b/Sources/Configuration/LintConfiguration.swift index 4dcef25..4c6d8ba 100644 --- a/Sources/Configuration/LintConfiguration.swift +++ b/Sources/Configuration/LintConfiguration.swift @@ -4,8 +4,8 @@ import Core /// The configuration file type. public struct LintConfiguration: Codable { enum CodingKeys: String, CodingKey { - case fileContents = "CheckFileContents" - case filePaths = "CheckFilePaths" + case fileContents = "FileContents" + case filePaths = "FilePaths" case customScripts = "CustomScripts" } diff --git a/Sources/Configuration/Template.swift b/Sources/Configuration/Template.swift index 95ff9ad..b14e6ca 100644 --- a/Sources/Configuration/Template.swift +++ b/Sources/Configuration/Template.swift @@ -1,3 +1,4 @@ +import Core import Foundation /// The possible templates for setting up configuration initially. @@ -9,66 +10,18 @@ public enum Template: String, CaseIterable { case openSource /// Returns the file contents for the chosen template. - public var fileContents: String { - switch self { - case .blank: - return #""" - FileContents: - - id: Readme - hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' - regex: '^README\.md$' - violateIfNoMatchesFound: true - matchingExamples: ['README.md'] - nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] - - - id: ReadmeTopLevelTitle - hint: 'The README.md file should only contain a single top level title.' - regex: '(^|\n)#[^#](.*\n)*\n#[^#]' - includeFilter: ['^README\.md$'] - matchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - nonMatchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle - - - id: ReadmeTypoLicense - hint: 'ReadmeTypoLicense: Misspelled word `license`.' - regex: '([\s#]L|l)isence([\s\.,:;])' - matchingExamples: [' lisence:', '## Lisence\n'] - nonMatchingExamples: [' license:', '## License\n'] - includeFilters: ['^README\.md$'] - autoCorrectReplacement: '$1icense$2' - autoCorrectExamples: - - { before: ' lisence:', after: ' license:' } - - { before: '## Lisence\n', after: '## License\n' } - - FilePaths: - - id: 'ReadmePath' - hint: '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' } - - """# - - case .openSource: - fatalError() // TODO: [cg_2021-07-02] not yet implemented - } + public var fileContents: Data { + // NOTE: force unwrapping and force try safe together with `testFileContentsNotFailing` test & CI + let templateFileUrl = Bundle.module.url( + forResource: rawValue.firstUppercased, + withExtension: "yml", + subdirectory: "Templates" + )! + return try! Data(contentsOf: templateFileUrl) } } + +extension String { + /// Returns a variation with the first character uppercased. + fileprivate var firstUppercased: String { prefix(1).uppercased() + dropFirst() } +} diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml new file mode 100644 index 0000000..8cae376 --- /dev/null +++ b/Sources/Configuration/Templates/Blank.yml @@ -0,0 +1,40 @@ +FileContents: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + + - id: ReadmeTopLevelTitle + hint: 'The README.md file should only contain a single top level title.' + regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + includeFilter: ['^README\.md$'] + matchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + nonMatchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + +FilePaths: + - id: 'ReadmePath' + hint: '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' } diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml new file mode 100644 index 0000000..1616754 --- /dev/null +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -0,0 +1,51 @@ +FileContents: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + + - id: ReadmeTopLevelTitle + hint: 'The README.md file should only contain a single top level title.' + regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + includeFilter: ['^README\.md$'] + matchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum + + # Other Title + ## Other Subtitle + nonMatchingExamples: + - | + # Title + ## Subtitle + Lorem ipsum #1 and # 2. + + ## Other Subtitle + ### Other Subsubtitle + + - id: ReadmeTypoLicense + hint: 'ReadmeTypoLicense: Misspelled word `license`.' + regex: '([\s#]L|l)isence([\s\.,:;])' + matchingExamples: [' lisence:', '## Lisence\n'] + nonMatchingExamples: [' license:', '## License\n'] + includeFilters: ['^README\.md$'] + autoCorrectReplacement: '$1icense$2' + autoCorrectExamples: + - { before: ' lisence:', after: ' license:' } + - { before: '## Lisence\n', after: '## License\n' } + +FilePaths: + - id: 'ReadmePath' + hint: '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' } diff --git a/Tests/ConfigurationTests/TemplateTests.swift b/Tests/ConfigurationTests/TemplateTests.swift new file mode 100644 index 0000000..a61ea1a --- /dev/null +++ b/Tests/ConfigurationTests/TemplateTests.swift @@ -0,0 +1,30 @@ +import Foundation +import XCTest +import Yams +@testable import Configuration + +final class TemplateTests: XCTestCase { + func testFileContentsNotFailing() { + for template in Template.allCases { + XCTAssertFalse(template.fileContents.isEmpty) + } + } + + func testBlankIsValidYAMLConfig() throws { + let configFileData = Template.blank.fileContents + let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) + + XCTAssertFalse(lintConfig.filePaths.isEmpty) + XCTAssertFalse(lintConfig.fileContents.isEmpty) + XCTAssert(lintConfig.customScripts.isEmpty) + } + + func testOpenSourceIsValidYAMLConfig() throws { + let configFileData = Template.openSource.fileContents + let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) + + XCTAssertFalse(lintConfig.filePaths.isEmpty) + XCTAssertFalse(lintConfig.fileContents.isEmpty) + XCTAssert(lintConfig.customScripts.isEmpty) + } +} From f8372516a486713fa4ba55b8de0faa98e34d0ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Aug 2021 23:18:46 +0200 Subject: [PATCH 14/37] Update template contents & test expectations --- Sources/Configuration/Templates/Blank.yml | 56 +++++++------------ .../Configuration/Templates/OpenSource.yml | 3 + Tests/ConfigurationTests/TemplateTests.swift | 6 +- 3 files changed, 25 insertions(+), 40 deletions(-) diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index 8cae376..c4848de 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -1,40 +1,22 @@ FileContents: - - id: Readme - hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' - regex: '^README\.md$' - violateIfNoMatchesFound: true - matchingExamples: ['README.md'] - nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] - - - id: ReadmeTopLevelTitle - hint: 'The README.md file should only contain a single top level title.' - regex: '(^|\n)#[^#](.*\n)*\n#[^#]' - includeFilter: ['^README\.md$'] - matchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - nonMatchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle +# - id: Readme +# hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' +# regex: '^README\.md$' +# violateIfNoMatchesFound: true +# matchingExamples: ['README.md'] +# nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] FilePaths: - - id: 'ReadmePath' - hint: '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' } +# - id: 'ReadmePath' +# hint: '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' } + +CustomScripts: +# TODO: add sample entry diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 1616754..54a2c7a 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -49,3 +49,6 @@ FilePaths: - { before: 'api/readme.md', after: 'api/README.md' } - { before: 'ReadMe.md', after: 'README.md' } - { before: 'README.markdown', after: 'README.md' } + +CustomScripts: +# TODO: add useful custom scripts, e.g. in Ruby/Python which are installed on most developer systems diff --git a/Tests/ConfigurationTests/TemplateTests.swift b/Tests/ConfigurationTests/TemplateTests.swift index a61ea1a..dc09392 100644 --- a/Tests/ConfigurationTests/TemplateTests.swift +++ b/Tests/ConfigurationTests/TemplateTests.swift @@ -14,8 +14,8 @@ final class TemplateTests: XCTestCase { let configFileData = Template.blank.fileContents let lintConfig: LintConfiguration = try YAMLDecoder().decode(from: configFileData) - XCTAssertFalse(lintConfig.filePaths.isEmpty) - XCTAssertFalse(lintConfig.fileContents.isEmpty) + XCTAssert(lintConfig.filePaths.isEmpty) + XCTAssert(lintConfig.fileContents.isEmpty) XCTAssert(lintConfig.customScripts.isEmpty) } @@ -25,6 +25,6 @@ final class TemplateTests: XCTestCase { XCTAssertFalse(lintConfig.filePaths.isEmpty) XCTAssertFalse(lintConfig.fileContents.isEmpty) - XCTAssert(lintConfig.customScripts.isEmpty) + XCTAssertFalse(lintConfig.customScripts.isEmpty) } } From e2754a67799d3eb4d1d810fea6ca01a535fd8661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Aug 2021 23:32:42 +0200 Subject: [PATCH 15/37] Fix empty YAML array issue & document next TODO --- Sources/Configuration/FilePathsConfiguration.swift | 4 ++++ Sources/Configuration/Templates/Blank.yml | 6 +++--- Sources/Configuration/Templates/OpenSource.yml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/Configuration/FilePathsConfiguration.swift b/Sources/Configuration/FilePathsConfiguration.swift index ebb8570..0cb40a1 100644 --- a/Sources/Configuration/FilePathsConfiguration.swift +++ b/Sources/Configuration/FilePathsConfiguration.swift @@ -9,6 +9,10 @@ public struct FilePathsConfiguration: CheckConfiguration, Codable { /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. public let hint: String + // TODO: [cg_2021-08-15] continue here by fixing default decoding value issue, see also: + // https://swiftbysundell.com/tips/default-decoding-values/ + // https://github.com/marksands/BetterCodable#defaultcodable + /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. public var severity: Severity = .error diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index c4848de..126941f 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -1,4 +1,4 @@ -FileContents: +FileContents: [] # - id: Readme # hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' # regex: '^README\.md$' @@ -6,7 +6,7 @@ FileContents: # matchingExamples: ['README.md'] # nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] -FilePaths: +FilePaths: [] # - id: 'ReadmePath' # hint: 'The README file should be named exactly `README.md`.' # regex: '^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$' @@ -18,5 +18,5 @@ FilePaths: # - { before: 'ReadMe.md', after: 'README.md' } # - { before: 'README.markdown', after: 'README.md' } -CustomScripts: +CustomScripts: [] # TODO: add sample entry diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 54a2c7a..89ca5f5 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -50,5 +50,5 @@ FilePaths: - { before: 'ReadMe.md', after: 'README.md' } - { before: 'README.markdown', after: 'README.md' } -CustomScripts: +CustomScripts: [] # TODO: add useful custom scripts, e.g. in Ruby/Python which are installed on most developer systems From 3f461597cff48da78795b9835433a275d414046e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 26 Aug 2021 16:59:28 +0200 Subject: [PATCH 16/37] Fix default value issue & add sample custom script --- Package.resolved | 13 ++++- Package.swift | 5 ++ .../Core+DefaultCodableStrategy.swift | 16 +++++++ .../CustomScriptsConfiguration.swift | 4 +- .../FileContentsConfiguration.swift | 22 ++++++--- .../FilePathsConfiguration.swift | 26 +++++----- Sources/Configuration/LintConfiguration.swift | 10 ++-- .../Configuration/Templates/OpenSource.yml | 13 +++-- Sources/Core/CheckInfo.swift | 48 +++++++++++++++++++ 9 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 Sources/Configuration/Core+DefaultCodableStrategy.swift diff --git a/Package.resolved b/Package.resolved index 1d041d0..3de8d31 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "BetterCodable", + "repositoryURL": "https://github.com/marksands/BetterCodable.git", + "state": { + "branch": null, + "revision": "61153170668db7a46a20a87e35e70f80b24d4eb5", + "version": "0.4.0" + } + }, { "package": "Rainbow", "repositoryURL": "https://github.com/onevcat/Rainbow.git", @@ -33,8 +42,8 @@ "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "0959ba76a1d4a98fd11163aa83fd49c25b93bfae", - "version": "0.0.5" + "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", + "version": "0.0.7" } }, { diff --git a/Package.swift b/Package.swift index da0e8a7..b385f98 100644 --- a/Package.swift +++ b/Package.swift @@ -3,10 +3,14 @@ import PackageDescription let package = Package( name: "AnyLint", + platforms: [.macOS(.v10_12)], products: [ .executable(name: "anylint", targets: ["Commands"]), ], dependencies: [ + // Better Codable through Property Wrappers + .package(url: "https://github.com/marksands/BetterCodable.git", from: "0.4.0"), + // Delightful console output for Swift developers. .package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.0"), @@ -33,6 +37,7 @@ let package = Package( .target( name: "Configuration", dependencies: [ + .product(name: "BetterCodable", package: "BetterCodable"), "Core", .product(name: "Yams", package: "Yams"), ], diff --git a/Sources/Configuration/Core+DefaultCodableStrategy.swift b/Sources/Configuration/Core+DefaultCodableStrategy.swift new file mode 100644 index 0000000..65c96c8 --- /dev/null +++ b/Sources/Configuration/Core+DefaultCodableStrategy.swift @@ -0,0 +1,16 @@ +import Foundation +import Core +import BetterCodable + +extension Severity { + /// Use to set the default value of `Severity` instances to `error` in rules when users don't provide an explicit value. + public enum DefaultToError: DefaultCodableStrategy { + public static var defaultValue: Severity { .error } + } +} + +extension Regex { + public enum DefaultToMatchAllArray: DefaultCodableStrategy { + public static var defaultValue: [Regex] { [try! Regex(".*")] } + } +} diff --git a/Sources/Configuration/CustomScriptsConfiguration.swift b/Sources/Configuration/CustomScriptsConfiguration.swift index 5ca8f8e..cf5c86d 100644 --- a/Sources/Configuration/CustomScriptsConfiguration.swift +++ b/Sources/Configuration/CustomScriptsConfiguration.swift @@ -1,5 +1,6 @@ import Foundation import Core +import BetterCodable /// The `CustomScripts` check configuration type. public struct CustomScriptsConfiguration: CheckConfiguration, Codable { @@ -10,7 +11,8 @@ public struct CustomScriptsConfiguration: CheckConfiguration, Codable { public let hint: String /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. - public var severity: Severity = .error + @DefaultCodable + public var severity: Severity /// The custom command line command to execute. /// If the output conforms to the ``LintResults`` structure formatted as JSON, then the results will be merged. diff --git a/Sources/Configuration/FileContentsConfiguration.swift b/Sources/Configuration/FileContentsConfiguration.swift index f80e4dd..1ce8339 100644 --- a/Sources/Configuration/FileContentsConfiguration.swift +++ b/Sources/Configuration/FileContentsConfiguration.swift @@ -1,5 +1,6 @@ import Foundation import Core +import BetterCodable /// The `FileContents` check configuration type. public struct FileContentsConfiguration: CheckConfiguration, Codable { @@ -10,7 +11,8 @@ public struct FileContentsConfiguration: CheckConfiguration, Codable { public let hint: String /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. - public var severity: Severity = .error + @DefaultCodable + public var severity: Severity /// The regular expression to use to find violations. Required. public let regex: Regex @@ -19,23 +21,27 @@ public struct FileContentsConfiguration: CheckConfiguration, Codable { /// /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var matchingExamples: [String] = [] + @DefaultEmptyArray + public var matchingExamples: [String] /// A list of strings that are expected to **not** to match the provided `regex`. Optional. /// /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var nonMatchingExamples: [String] = [] + @DefaultEmptyArray + public var nonMatchingExamples: [String] /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. /// /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. - public var includeFilters: [Regex] = [try! Regex(".*")] + @DefaultCodable + public var includeFilters: [Regex] /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. /// /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' - public var excludeFilters: [Regex] = [] + @DefaultEmptyArray + public var excludeFilters: [Regex] /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. /// @@ -49,7 +55,8 @@ public struct FileContentsConfiguration: CheckConfiguration, Codable { /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. /// /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. - public var autoCorrectExamples: [AutoCorrection] = [] + @DefaultEmptyArray + public var autoCorrectExamples: [AutoCorrection] /// If set to `true`, a check will be re-run if there was at least one auto-correction applied on the last run. Optional. /// @@ -57,5 +64,6 @@ public struct FileContentsConfiguration: CheckConfiguration, Codable { /// For example, to ensure long numbers are separated by an underscore, you could write the regex `(\d+)(\d{3})` /// and specify the replacement `$1_$2$3`. By default, the number `123456789` would be transformed to `123456_789`. /// With this option set to `true`, the check would be re-executed after the first run (because there was a correction) and the result would be `123_456_789`. - public var repeatIfAutoCorrected: Bool = false + @DefaultFalse + public var repeatIfAutoCorrected: Bool } diff --git a/Sources/Configuration/FilePathsConfiguration.swift b/Sources/Configuration/FilePathsConfiguration.swift index 0cb40a1..dda3b25 100644 --- a/Sources/Configuration/FilePathsConfiguration.swift +++ b/Sources/Configuration/FilePathsConfiguration.swift @@ -1,5 +1,6 @@ import Foundation import Core +import BetterCodable /// The `FilePaths` check configuration type. public struct FilePathsConfiguration: CheckConfiguration, Codable { @@ -9,12 +10,9 @@ public struct FilePathsConfiguration: CheckConfiguration, Codable { /// A hint that should be shown on violations of this check. Should explain what's wrong and guide on fixing the issue. public let hint: String - // TODO: [cg_2021-08-15] continue here by fixing default decoding value issue, see also: - // https://swiftbysundell.com/tips/default-decoding-values/ - // https://github.com/marksands/BetterCodable#defaultcodable - /// The severity level of this check. One of `.info`, `.warning` or `.error`. Defaults to `.error`. - public var severity: Severity = .error + @DefaultCodable + public var severity: Severity /// The regular expression to use to find violations. Required. public let regex: Regex @@ -23,23 +21,27 @@ public struct FilePathsConfiguration: CheckConfiguration, Codable { /// /// If any of the provided examples doesn't match, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var matchingExamples: [String] = [] + @DefaultEmptyArray + public var matchingExamples: [String] /// A list of strings that are expected to **not** to match the provided `regex`. Optional. /// /// If any of the provided examples matches, linting will fail early to ensure the provided `regex` works as expected. The check itself will not be run. /// This can be considered a 'unit test' for the regex. It's recommended to provide at least one matching example & one non-matching example. - public var nonMatchingExamples: [String] = [] + @DefaultEmptyArray + public var nonMatchingExamples: [String] /// A list of path-matching regexes to restrict this check to files in the matching paths only ("allow-listing"). Optional. /// /// When combined with `excludeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins'. - public var includeFilters: [Regex] = [try! Regex(".*")] + @DefaultCodable + public var includeFilters: [Regex] /// A list of path-matching regexes to skip this check on for files with matching paths ("deny-listing"). Optional. /// /// When combined with `includeFilters`, the exclude paths take precedence over the include paths – in other words: 'exclude always wins' - public var excludeFilters: [Regex] = [] + @DefaultEmptyArray + public var excludeFilters: [Regex] /// A regex replacement template with `$1`-kind of references to capture groups in the regex. Optional. /// @@ -55,8 +57,10 @@ public struct FilePathsConfiguration: CheckConfiguration, Codable { /// If any of the provided `before` doesn't get transformed to the `after`, linting will fail early and the check itself will not be run. /// /// This can be considered a 'unit test' for the auto-correction. It's recommended to provide at least one pair if you specify use `autoCorrectReplacement`. - public var autoCorrectExamples: [AutoCorrection] = [] + @DefaultEmptyArray + public var autoCorrectExamples: [AutoCorrection] /// If set to `true`, a violation will be reported if **no** matches are found. By default (or if set to `false`), a check violates on every matching file path. - public var violateIfNoMatchesFound: Bool = false + @DefaultFalse + public var violateIfNoMatchesFound: Bool } diff --git a/Sources/Configuration/LintConfiguration.swift b/Sources/Configuration/LintConfiguration.swift index 4c6d8ba..9f3ae50 100644 --- a/Sources/Configuration/LintConfiguration.swift +++ b/Sources/Configuration/LintConfiguration.swift @@ -1,5 +1,6 @@ import Foundation import Core +import BetterCodable /// The configuration file type. public struct LintConfiguration: Codable { @@ -10,11 +11,14 @@ public struct LintConfiguration: Codable { } /// The list of `FileContents` checks. - public let fileContents: [FileContentsConfiguration] + @DefaultEmptyArray + public var fileContents: [FileContentsConfiguration] /// The list of `FilePaths` checks. - public let filePaths: [FilePathsConfiguration] + @DefaultEmptyArray + public var filePaths: [FilePathsConfiguration] /// The list of `CustomScripts` checks. - public let customScripts: [CustomScriptsConfiguration] + @DefaultEmptyArray + public var customScripts: [CustomScriptsConfiguration] } diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 89ca5f5..1e9068d 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -39,7 +39,7 @@ FileContents: - { before: '## Lisence\n', after: '## License\n' } FilePaths: - - id: 'ReadmePath' + - id: ReadmePath hint: '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'] @@ -50,5 +50,12 @@ FilePaths: - { before: 'ReadMe.md', after: 'README.md' } - { before: 'README.markdown', after: 'README.md' } -CustomScripts: [] -# TODO: add useful custom scripts, e.g. in Ruby/Python which are installed on most developer systems +CustomScripts: + - id: LintConfig + hint: 'Lint the AnyLint config file to conform to YAML best practices.' + command: |- + if which yamllint > /dev/null; then + yamllint anylint.yml + else + echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint" + fi diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/CheckInfo.swift index e7f9610..bd3805f 100644 --- a/Sources/Core/CheckInfo.swift +++ b/Sources/Core/CheckInfo.swift @@ -28,3 +28,51 @@ extension CheckInfo: Hashable { hasher.combine(id) } } + +extension CheckInfo: Codable { + public init( + from decoder: Decoder + ) throws { + let container = try decoder.singleValueContainer() + let rawString = try container.decode(String.self) + + let customSeverityRegex = try Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#) + + if let match = customSeverityRegex.firstMatch(in: rawString) { + let id = match.captures[0]! + let severityString = match.captures[1]! + let hint = match.captures[2]! + + guard let severity = Severity(rawValue: severityString) else { + log.message( + "Specified severity '\(severityString)' for check '\(id)' unknown. Use one of [error, warning, info].", + level: .error + ) + log.exit(fail: true) + } + + self = .init(id: id, hint: hint, severity: severity) + } + else { + let defaultSeverityRegex = try Regex(#"^([^@:]+): ?(.*$)"#) + + guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawString) else { + log.message( + "Could not convert String literal '\(rawString)' to type CheckInfo. Please check the structure to be: (@): ", + level: .error + ) + log.exit(fail: true) + } + + let id = defaultSeverityMatch.captures[0]! + let hint = defaultSeverityMatch.captures[1]! + + self = .init(id: id, hint: hint) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("\(id)@\(severity.rawValue): \(hint)") + } +} From 25f4089e7cad8b994273f16687ab246de027e79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 31 Aug 2021 22:45:15 +0200 Subject: [PATCH 17/37] Re-add adjusted tests for Core library + refactor --- Sources/Checkers/Extensions/RegexExt.swift | 83 +------ Sources/Checkers/Lint.swift | 4 +- Sources/Core/AutoCorrection.swift | 143 ++++++------ Sources/Core/Regex.swift | 2 +- .../Extensions/ArrayExtTests.swift | 4 +- .../Extensions/RegexExtTests.swift | 42 +--- Tests/CoreTests/AutoCorrectionTests.swift | 64 +++--- Tests/CoreTests/RegexExtTests.swift | 21 -- Tests/CoreTests/RegexTests.swift | 203 ++++++++++++++++++ Tests/CoreTests/ViolationTests.swift | 30 ++- 10 files changed, 321 insertions(+), 275 deletions(-) delete mode 100644 Tests/CoreTests/RegexExtTests.swift create mode 100644 Tests/CoreTests/RegexTests.swift diff --git a/Sources/Checkers/Extensions/RegexExt.swift b/Sources/Checkers/Extensions/RegexExt.swift index 41598c5..b913435 100644 --- a/Sources/Checkers/Extensions/RegexExt.swift +++ b/Sources/Checkers/Extensions/RegexExt.swift @@ -1,8 +1,8 @@ import Foundation import Core -extension Regex: ExpressibleByStringLiteral { - /// Constants to reference across the project. +extension Regex { + /// Constants to reference across the project related to Regexes. enum Constants { /// The separator indicating that next come regex options. static let regexOptionsSeparator: String = #"\"# @@ -12,83 +12,6 @@ extension Regex: ExpressibleByStringLiteral { /// Hint that the case dot matches newline option should be active on a Regex. static let dotMatchesNewlinesRegexOption: String = "m" - - // /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. - // static let newlinesRequiredForDiffing: Int = 3 - } - - 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(fail: true) - } - } -} - -extension Regex: ExpressibleByDictionaryLiteral { - public init( - dictionaryLiteral elements: (String, String)... - ) { - var patternElements = elements - var options: Options = Regex.defaultOptions - - if let regexOptionsValue = elements.last(where: { $0.0 == Constants.regexOptionsSeparator })?.1 { - patternElements.removeAll { $0.0 == Constants.regexOptionsSeparator } - - if regexOptionsValue.contains(Constants.caseInsensitiveRegexOption) { - options.insert(.ignoreCase) - } - - if regexOptionsValue.contains(Constants.dotMatchesNewlinesRegexOption) { - options.insert(.dotMatchesLineSeparators) - } - } - - do { - let pattern: String = patternElements.reduce(into: "") { result, element in - result.append("(?<\(element.0)>\(element.1))") - } - self = try Regex(pattern, options: options) - } - catch { - log.message("Failed to convert Dictionary literal '\(elements)' to type Regex.", level: .error) - log.exit(fail: true) - } } } @@ -100,7 +23,7 @@ extension Regex { /// 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 captureGroupNameRegex = try! Regex(#"\(\?\<([a-zA-Z0-9_-]+)\>[^\)]+\)"#) let captureGroupNames: [String] = captureGroupNameRegex.matches(in: pattern).map { $0.captures[0]! } return captureGroupNames.enumerated() .reduce(replacementString) { result, enumeratedGroupName in diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index b065305..28bf807 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -21,7 +21,7 @@ public enum Lint { regex: Regex, matchingExamples: [String] = [], nonMatchingExamples: [String] = [], - includeFilters: [Regex] = [#".*"#], + includeFilters: [Regex] = [try! Regex(#".*"#)], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, autoCorrectExamples: [AutoCorrection] = [], @@ -83,7 +83,7 @@ public enum Lint { regex: Regex, matchingExamples: [String] = [], nonMatchingExamples: [String] = [], - includeFilters: [Regex] = [#".*"#], + includeFilters: [Regex] = [try! Regex(#".*"#)], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, autoCorrectExamples: [AutoCorrection] = [], diff --git a/Sources/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift index d529341..272d248 100644 --- a/Sources/Core/AutoCorrection.swift +++ b/Sources/Core/AutoCorrection.swift @@ -2,44 +2,50 @@ import Foundation /// Information about an autocorrection. public struct AutoCorrection: Codable { + private enum Constants { + /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. + static let newlinesRequiredForDiffing: Int = 3 + } + /// The matching text before applying the autocorrection. public let before: String /// The matching text after applying the autocorrection. public let after: String - // var appliedMessageLines: [String] { - // if useDiffOutput, #available(OSX 10.15, *) { - // var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] - // - // let beforeLines = before.components(separatedBy: .newlines) - // let afterLines = after.components(separatedBy: .newlines) - // - // for difference in afterLines.difference(from: beforeLines).sorted() { - // switch difference { - // case let .insert(offset, element, _): - // lines.append("+ [L\(offset + 1)] \(element)".green) - // - // case let .remove(offset, element, _): - // lines.append("- [L\(offset + 1)] \(element)".red) - // } - // } - // - // return lines - // } - // else { - // return [ - // "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 - // } + // TODO: [cg_2021-08-31] consider migrating over to https://github.com/pointfreeco/swift-custom-dump#diff + var appliedMessageLines: [String] { + if useDiffOutput, #available(OSX 10.15, *) { + var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] + + let beforeLines = before.components(separatedBy: .newlines) + let afterLines = after.components(separatedBy: .newlines) + + for difference in afterLines.difference(from: beforeLines).sorted() { + switch difference { + case let .insert(offset, element, _): + lines.append("+ [L\(offset + 1)] \(element)".green) + + case let .remove(offset, element, _): + lines.append("- [L\(offset + 1)] \(element)".red) + } + } + + return lines + } + else { + return [ + "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( @@ -51,48 +57,31 @@ public struct AutoCorrection: Codable { } } -//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(fail: true) -// 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 -// } -// } -//} +// 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 + } + } +} diff --git a/Sources/Core/Regex.swift b/Sources/Core/Regex.swift index 04e663e..749a15e 100644 --- a/Sources/Core/Regex.swift +++ b/Sources/Core/Regex.swift @@ -314,7 +314,7 @@ extension Regex { // MARK: - CustomStringConvertible /// Returns a string describing the match. public var description: String { - "Match<\"\(string)\">" + #"Match<"\#(string)">"# } } } diff --git a/Tests/CheckersTests/Extensions/ArrayExtTests.swift b/Tests/CheckersTests/Extensions/ArrayExtTests.swift index afc7a09..bb44cb4 100644 --- a/Tests/CheckersTests/Extensions/ArrayExtTests.swift +++ b/Tests/CheckersTests/Extensions/ArrayExtTests.swift @@ -3,8 +3,8 @@ import Core import XCTest final class ArrayExtTests: XCTestCase { - func testContainsLineAtIndexesMatchingRegex() { - let regex: Regex = #"foo:bar"# + func testContainsLineAtIndexesMatchingRegex() throws { + let regex: Regex = try .init(#"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)) diff --git a/Tests/CheckersTests/Extensions/RegexExtTests.swift b/Tests/CheckersTests/Extensions/RegexExtTests.swift index 6f88580..36a1c0d 100644 --- a/Tests/CheckersTests/Extensions/RegexExtTests.swift +++ b/Tests/CheckersTests/Extensions/RegexExtTests.swift @@ -2,46 +2,8 @@ import Core import XCTest final class RegexExtTests: XCTestCase { - func testStringLiteralInit() { - let regex: Regex = #".*"# - XCTAssertEqual(regex.description, #"/.*/"#) - } - - func testStringLiteralInitWithOptions() { - let regexI: Regex = #".*\i"# - XCTAssertEqual(regexI.description, #"/.*/i"#) - - let regexM: Regex = #".*\m"# - XCTAssertEqual(regexM.description, #"/.*/m"#) - - let regexIM: Regex = #".*\im"# - XCTAssertEqual(regexIM.description, #"/.*/im"#) - - let regexMI: Regex = #".*\mi"# - XCTAssertEqual(regexMI.description, #"/.*/im"#) - } - - func testDictionaryLiteralInit() { - let regex: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#] - XCTAssertEqual(regex.description, #"/(?[a-z]+)(?\d+\.?\d*)/"#) - } - - func testDictionaryLiteralInitWithOptions() { - let regexI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "i"] - XCTAssertEqual(regexI.description, #"/(?[a-z]+)(?\d+\.?\d*)/i"#) - - let regexM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "m"] - XCTAssertEqual(regexM.description, #"/(?[a-z]+)(?\d+\.?\d*)/m"#) - - let regexMI: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "mi"] - XCTAssertEqual(regexMI.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) - - let regexIM: Regex = ["chars": #"[a-z]+"#, "num": #"\d+\.?\d*"#, #"\"#: "im"] - XCTAssertEqual(regexIM.description, #"/(?[a-z]+)(?\d+\.?\d*)/im"#) - } - - func testReplacingMatchesInInputWithTemplate() { - let regexTrailing: Regex = #"(?<=\n)([-–] .*[^ ])( {0,1}| {3,})\n"# + func testReplacingMatchesInInputWithTemplate() throws { + let regexTrailing: Regex = try .init(#"(?<=\n)([-–] .*[^ ])( {0,1}| {3,})\n"#) let text: String = "\n- Sample Text.\n" XCTAssertEqual( diff --git a/Tests/CoreTests/AutoCorrectionTests.swift b/Tests/CoreTests/AutoCorrectionTests.swift index 94c4559..8672142 100644 --- a/Tests/CoreTests/AutoCorrectionTests.swift +++ b/Tests/CoreTests/AutoCorrectionTests.swift @@ -2,40 +2,36 @@ import XCTest final class AutoCorrectionTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + func testInitWithDictionaryLiteral() { + let autoCorrection: AutoCorrection = .init(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", - // ] - // ) - // - // 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", - // ] - // ) - // } + func testAppliedMessageLines() { + let singleLineAutoCorrection: AutoCorrection = .init(before: "Lisence", after: "License") + XCTAssertEqual( + singleLineAutoCorrection.appliedMessageLines, + [ + "Autocorrection applied, the diff is: (+ added, - removed)", + "- Lisence", + "+ License", + ] + ) + + let multiLineAutoCorrection: AutoCorrection = .init( + 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/CoreTests/RegexExtTests.swift b/Tests/CoreTests/RegexExtTests.swift deleted file mode 100644 index f0a145e..0000000 --- a/Tests/CoreTests/RegexExtTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -@testable import Core -import XCTest - -final class RegexExtTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented - } - - // 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.*)"#) - // } -} diff --git a/Tests/CoreTests/RegexTests.swift b/Tests/CoreTests/RegexTests.swift new file mode 100644 index 0000000..4480c46 --- /dev/null +++ b/Tests/CoreTests/RegexTests.swift @@ -0,0 +1,203 @@ +import Foundation +import XCTest +@testable import Core + +class RegexTests: XCTestCase { + // MARK: - Initialization + func testValidInitialization() { + XCTAssertNoThrow({ try Regex("abc") }) // swiftlint:disable:this trailing_closure + } + + func testInvalidInitialization() { + do { + _ = try Regex("*") + XCTFail("Regex initialization unexpectedly didn't fail") + } + catch {} + } + + // MARK: - Options + func testOptions() { + let regexOptions1: Regex.Options = [ + .ignoreCase, .ignoreMetacharacters, .anchorsMatchLines, .dotMatchesLineSeparators, + ] + let nsRegexOptions1: NSRegularExpression.Options = [ + .caseInsensitive, .ignoreMetacharacters, .anchorsMatchLines, .dotMatchesLineSeparators, + ] + + let regexOptions2: Regex.Options = [.ignoreMetacharacters] + let nsRegexOptions2: NSRegularExpression.Options = [.ignoreMetacharacters] + + let regexOptions3: Regex.Options = [] + let nsRegexOptions3: NSRegularExpression.Options = [] + + XCTAssertEqual(regexOptions1.toNSRegularExpressionOptions, nsRegexOptions1) + XCTAssertEqual(regexOptions2.toNSRegularExpressionOptions, nsRegexOptions2) + XCTAssertEqual(regexOptions3.toNSRegularExpressionOptions, nsRegexOptions3) + } + + // MARK: - Matching + func testMatchesBool() { + let regex = try? Regex("[1-9]+") + XCTAssertTrue(regex!.matches("5")) + } + + func testFirstMatch() { + let regex = try? Regex("[1-9]?+") + XCTAssertEqual(regex?.firstMatch(in: "5 3 7")?.string, "5") + } + + func testMatches() { + let regex = try? Regex("[1-9]+") + XCTAssertEqual(regex?.matches(in: "5 432 11").map { $0.string }, ["5", "432", "11"]) + + let key = "bi" + let complexRegex = try? Regex(#"<\#(key)>([^<>]+)"#) + XCTAssertEqual( + complexRegex? + .matches( + in: + "Add all your tasks in here. We will guide you with the right questions to get them organized." + ) + .map { $0.string }, + ["tasks", "organized"] + ) + } + + func testReplacingMatches() { + let regex = try? Regex("([1-9]+)") + + let stringAfterReplace1 = regex?.replacingMatches(in: "5 3 7", with: "2") + let stringAfterReplace2 = regex?.replacingMatches(in: "5 3 7", with: "$1") + let stringAfterReplace3 = regex?.replacingMatches(in: "5 3 7", with: "1$1,") + let stringAfterReplace4 = regex?.replacingMatches(in: "5 3 7", with: "2", count: 5) + let stringAfterReplace5 = regex?.replacingMatches(in: "5 3 7", with: "2", count: 2) + + XCTAssertEqual(stringAfterReplace1, "2 2 2") + XCTAssertEqual(stringAfterReplace2, "5 3 7") + XCTAssertEqual(stringAfterReplace3, "15, 13, 17,") + XCTAssertEqual(stringAfterReplace4, "2 2 2") + XCTAssertEqual(stringAfterReplace5, "2 2 7") + } + + func testReplacingMatchesWithSpecialCharacters() { + let testString = "\nSimuliere, wie gut ein \\nE-Fahrzeug zu dir passt\n" + let newValue = "Simuliere, wie gut ein \\nE-Fahrzeug zu dir passt2" + let expectedResult = + "\nSimuliere, wie gut ein \\nE-Fahrzeug zu dir passt2\n" + + let regex = try? Regex("(]* name=\"nav_menu_sim_info\"[^>]*>)(.*)()") + let stringAfterReplace1 = regex? + .replacingMatches(in: testString, with: "$1\(NSRegularExpression.escapedTemplate(for: newValue))$3") + + XCTAssertEqual(stringAfterReplace1, expectedResult) + } + + // MARK: - Match + func testMatchString() { + let regex = try? Regex("[1-9]+") + let firstMatchString = regex?.firstMatch(in: "abc5def")?.string + XCTAssertEqual(firstMatchString, "5") + } + + func testMatchRange() { + let regex = try? Regex("[1-9]+") + let text = "abc5def" + let firstMatchRange = regex?.firstMatch(in: text)?.range + XCTAssertEqual(firstMatchRange?.lowerBound.utf16Offset(in: text), 3) + XCTAssertEqual(firstMatchRange?.upperBound.utf16Offset(in: text), 4) + } + + func testMatchCaptures() { + let regex = try? Regex("([1-9])(Needed)(Optional)?") + let match1 = regex?.firstMatch(in: "2Needed") + let match2 = regex?.firstMatch(in: "5NeededOptional") + + enum CapturingError: Error { + case indexTooHigh + case noMatch + } + + func captures(at index: Int, forMatch match: Regex.Match?) throws -> String? { + guard let captures = match?.captures else { throw CapturingError.noMatch } + guard captures.count > index else { throw CapturingError.indexTooHigh } + return captures[index] + } + + do { + let match1Capture0 = try captures(at: 0, forMatch: match1) + let match1Capture1 = try captures(at: 1, forMatch: match1) + let match1Capture2 = try captures(at: 2, forMatch: match1) + + let match2Capture0 = try captures(at: 0, forMatch: match2) + let match2Capture1 = try captures(at: 1, forMatch: match2) + let match2Capture2 = try captures(at: 2, forMatch: match2) + + XCTAssertEqual(match1Capture0, "2") + XCTAssertEqual(match1Capture1, "Needed") + XCTAssertNil(match1Capture2) + + XCTAssertEqual(match2Capture0, "5") + XCTAssertEqual(match2Capture1, "Needed") + XCTAssertEqual(match2Capture2, "Optional") + } + catch let error as CapturingError { + switch error { + case .indexTooHigh: + XCTFail("Capturing group index is too high.") + + case .noMatch: + XCTFail("The match is nil.") + } + } + catch { + XCTFail("An unexpected error occured.") + } + } + + func testMatchStringApplyingTemplate() { + let regex = try? Regex("([1-9])(Needed)") + let match = regex?.firstMatch(in: "1Needed") + XCTAssertEqual(match?.string(applyingTemplate: "Test$1ThatIs$2"), "Test1ThatIsNeeded") + } + + // MARK: - Equatable + func testEquatable() { + do { + let regex1 = try Regex("abc") + let regex2 = try Regex("abc") + let regex3 = try Regex("cba") + let regex4 = try Regex("abc", options: [.ignoreCase]) + let regex5 = regex1 + + XCTAssertEqual(regex1, regex2) + XCTAssertNotEqual(regex1, regex3) + XCTAssertNotEqual(regex1, regex4) + XCTAssertEqual(regex1, regex5) + + XCTAssertNotEqual(regex2, regex3) + XCTAssertNotEqual(regex2, regex4) + XCTAssertEqual(regex2, regex5) + + XCTAssertNotEqual(regex3, regex4) + XCTAssertNotEqual(regex3, regex5) + + XCTAssertNotEqual(regex4, regex5) + } + catch { + XCTFail("Sample Regex creation failed.") + } + } + + // MARK: - CustomStringConvertible + func testRegexCustomStringConvertible() { + let regex = try? Regex("foo") + XCTAssertEqual(regex?.description, #"/foo/"#) + } + + func testMatchCustomStringConvertible() { + let regex = try? Regex("bar") + let match = regex?.firstMatch(in: "bar")! + XCTAssertEqual(match?.description, #"Match<"bar">"#) + } +} diff --git a/Tests/CoreTests/ViolationTests.swift b/Tests/CoreTests/ViolationTests.swift index d4683c9..8551313 100644 --- a/Tests/CoreTests/ViolationTests.swift +++ b/Tests/CoreTests/ViolationTests.swift @@ -2,23 +2,17 @@ import XCTest final class ViolationTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented - } + func testLocationMessage() { + XCTAssertNil(Violation().locationMessage(pathType: .relative)) + + let fileViolation = Violation(filePath: "Temp/Souces/Hello.swift") + XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") - // func testLocationMessage() { - // let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) - // XCTAssertNil(Violation(checkInfo: checkInfo).locationMessage(pathType: .relative)) - // - // let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") - // XCTAssertEqual(fileViolation.locationMessage(pathType: .relative), "Temp/Souces/Hello.swift") - // - // let locationInfoViolation = Violation( - // checkInfo: checkInfo, - // filePath: "Temp/Souces/World.swift", - // locationInfo: String.LocationInfo(line: 5, charInLine: 15) - // ) - // - // XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") - // } + let locationInfoViolation = Violation( + filePath: "Temp/Souces/World.swift", + fileLocation: .init(row: 5, column: 15) + ) + + XCTAssertEqual(locationInfoViolation.locationMessage(pathType: .relative), "Temp/Souces/World.swift:5:15:") + } } From 7b5ecd06adf12d978e3c086d3c6bc987590ca637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Wed, 1 Sep 2021 10:17:47 +0200 Subject: [PATCH 18/37] Complete tests in Core library --- Tests/CoreTests/CheckInfoTests.swift | 26 ++++++++++++++++++++++++++ Tests/CoreTests/SeverityTests.swift | 16 ++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 Tests/CoreTests/CheckInfoTests.swift create mode 100644 Tests/CoreTests/SeverityTests.swift diff --git a/Tests/CoreTests/CheckInfoTests.swift b/Tests/CoreTests/CheckInfoTests.swift new file mode 100644 index 0000000..e6e709d --- /dev/null +++ b/Tests/CoreTests/CheckInfoTests.swift @@ -0,0 +1,26 @@ +@testable import Core +import XCTest + +final class CheckInfoTests: 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) + + XCTAssertEqual(CheckInfo(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 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) + } +} diff --git a/Tests/CoreTests/SeverityTests.swift b/Tests/CoreTests/SeverityTests.swift new file mode 100644 index 0000000..a001bd6 --- /dev/null +++ b/Tests/CoreTests/SeverityTests.swift @@ -0,0 +1,16 @@ +@testable import Core +import XCTest + +final class SeverityTests: XCTestCase { + func testComparable() throws { + XCTAssert(Severity.error > Severity.warning) + XCTAssert(Severity.warning > Severity.info) + XCTAssert(Severity.error > Severity.info) + XCTAssert(Severity.warning < Severity.error) + XCTAssert(Severity.info < Severity.warning) + XCTAssert(Severity.info < Severity.error) + XCTAssert(Severity.error == Severity.error) + XCTAssert(Severity.warning == Severity.warning) + XCTAssert(Severity.info == Severity.info) + } +} From 907893005962818f41493e29f818669c053469fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 3 Sep 2021 17:29:04 +0200 Subject: [PATCH 19/37] Implement out-commented code in LintResults type --- Sources/Core/AutoCorrection.swift | 3 +- .../Reporting/Extensions/JSONEncoderExt.swift | 9 ++ Sources/Reporting/LintResults.swift | 150 +++++++++++------- 3 files changed, 103 insertions(+), 59 deletions(-) create mode 100644 Sources/Reporting/Extensions/JSONEncoderExt.swift diff --git a/Sources/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift index 272d248..7f791ea 100644 --- a/Sources/Core/AutoCorrection.swift +++ b/Sources/Core/AutoCorrection.swift @@ -14,7 +14,8 @@ public struct AutoCorrection: Codable { public let after: String // TODO: [cg_2021-08-31] consider migrating over to https://github.com/pointfreeco/swift-custom-dump#diff - var appliedMessageLines: [String] { + /// A summary of the applied autocorrections as human readable output. + public var appliedMessageLines: [String] { if useDiffOutput, #available(OSX 10.15, *) { var lines: [String] = ["Autocorrection applied, the diff is: (+ added, - removed)"] diff --git a/Sources/Reporting/Extensions/JSONEncoderExt.swift b/Sources/Reporting/Extensions/JSONEncoderExt.swift new file mode 100644 index 0000000..a253fc4 --- /dev/null +++ b/Sources/Reporting/Extensions/JSONEncoderExt.swift @@ -0,0 +1,9 @@ +import Foundation + +extension JSONEncoder { + static var iso: Self { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return encoder + } +} diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index 4096036..cfa341a 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -14,6 +14,17 @@ extension LintResults { values.reduce(into: []) { $0.append(contentsOf: $1.values.flatMap { $0 }) } } + /// 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.isEmpty { + return severity + } + } + + return nil + } + /// Merges the given lint results into this one. public mutating func mergeResults(_ other: LintResults) { merge(other) { currentDict, newDict in @@ -72,64 +83,78 @@ extension LintResults { return violations.filter { $0.appliedAutoCorrection == nil } } + /// Used to get validations for a specific check. + /// + /// - Parameters: + /// - check: The `CheckInfo` 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 [] } + guard excludeAutocorrected else { return violations } + return violations.filter { $0.appliedAutoCorrection == nil } + } + private func reportToConsole() { - // TODO: [cg_2021-07-06] not yet implemented - // 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.. " + 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 - // ) + for check in allExecutedChecks { + let checkViolations = violations(check: check, excludeAutocorrected: false) + + if 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.. " + 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 = "\(violations(severity: .error, excludeAutocorrected: false).count) error(s)" + let warnings = "\(violations(severity: .warning, excludeAutocorrected: false).count) warning(s)" + + log.message( + "Performed \(allExecutedChecks.count) check(s) and found \(errors) & \(warnings).", + // TODO: [cg_2021-09-03] forward option "exclude autocorrected" to use here rather than using `false` + level: maxViolationSeverity(excludeAutocorrected: false)?.logLevel ?? .info + ) } private func reportToXcode() { @@ -150,6 +175,15 @@ extension LintResults { } private func reportToFile(at path: String) { - // TODO: [cg_2021-07-09] not yet implemented + let resultFileUrl = URL(fileURLWithPath: "anylint-results.json") + + do { + let resultsData = try JSONEncoder.iso.encode(self) + try resultsData.write(to: resultFileUrl) + } + catch { + log.message("Failed to report results to file at \(resultFileUrl.path).", level: .error) + log.exit(fail: true) + } } } From 8973d6627eef683fc136ae5f48fa5e30ffd50838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 4 Sep 2021 13:37:17 +0200 Subject: [PATCH 20/37] Add more LintResults tests + refactor FileLocation --- Sources/Checkers/FileContentsChecker.swift | 11 +- Sources/Checkers/FilePathsChecker.swift | 5 +- Sources/Commands/LintCommand.swift | 4 +- Sources/Core/ConsoleLogger.swift | 33 +++++ Sources/Core/Extensions/StringExt.swift | 6 +- Sources/Core/FileLocation.swift | 19 --- Sources/Core/Location.swift | 37 +++++ Sources/Core/Loggable.swift | 26 ++++ Sources/Core/Logger.swift | 129 +---------------- Sources/Core/PrintLevel.swift | 33 +++++ Sources/Core/Regex.swift | 13 +- Sources/Core/Severity.swift | 15 ++ Sources/Core/Violation.swift | 16 +-- Sources/Core/XcodeLogger.swift | 20 +++ .../Reporting/Extensions/JSONEncoderExt.swift | 2 +- Sources/Reporting/LintResults.swift | 13 +- Tests/CoreTests/ViolationTests.swift | 15 +- Tests/ReportingTests/LintResultsTests.swift | 132 ++++++++++++++++++ 18 files changed, 336 insertions(+), 193 deletions(-) create mode 100644 Sources/Core/ConsoleLogger.swift delete mode 100644 Sources/Core/FileLocation.swift create mode 100644 Sources/Core/Location.swift create mode 100644 Sources/Core/Loggable.swift create mode 100644 Sources/Core/PrintLevel.swift create mode 100644 Sources/Core/XcodeLogger.swift create mode 100644 Tests/ReportingTests/LintResultsTests.swift diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift index eb01d31..de8ba3d 100644 --- a/Sources/Checkers/FileContentsChecker.swift +++ b/Sources/Checkers/FileContentsChecker.swift @@ -43,11 +43,11 @@ extension FileContentsChecker: Checker { let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(id)"#) for match in regex.matches(in: fileContents).reversed() { - let fileLocation = fileContents.fileLocation(of: match.range.lowerBound) + let fileLocation = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath) // skip found match if contains `AnyLint.skipHere: ` in same line or one line before guard - !linesInFile.containsLine(at: [fileLocation.row - 2, fileLocation.row - 1], matchingRegex: skipHereRegex) + !linesInFile.containsLine(at: [fileLocation.row! - 2, fileLocation.row! - 1], matchingRegex: skipHereRegex) else { continue } let autoCorrection: AutoCorrection? = { @@ -69,7 +69,6 @@ extension FileContentsChecker: Checker { violations.append( Violation( - filePath: filePath, matchedString: match.string, fileLocation: fileLocation, appliedAutoCorrection: autoCorrection @@ -93,8 +92,10 @@ extension FileContentsChecker: Checker { if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { // only paths where auto-corrections were applied need to be re-checked - let filePathsToReCheck = Array(Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.filePath! })) - .sorted() + let filePathsToReCheck = Array( + Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.fileLocation!.filePath }) + ) + .sorted() let violationsOnRechecks = try FileContentsChecker( id: id, diff --git a/Sources/Checkers/FilePathsChecker.swift b/Sources/Checkers/FilePathsChecker.swift index 8b988d1..9c83e36 100644 --- a/Sources/Checkers/FilePathsChecker.swift +++ b/Sources/Checkers/FilePathsChecker.swift @@ -33,7 +33,7 @@ extension FilePathsChecker: Checker { let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count if matchingFilePathsCount <= 0 { violations.append( - Violation(filePath: nil, fileLocation: nil, appliedAutoCorrection: nil) + Violation(fileLocation: nil, appliedAutoCorrection: nil) ) } } @@ -50,8 +50,7 @@ extension FilePathsChecker: Checker { violations.append( Violation( - filePath: filePath, - fileLocation: nil, + fileLocation: .init(filePath: filePath), appliedAutoCorrection: appliedAutoCorrection ) ) diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index 9298364..eb60415 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -48,7 +48,9 @@ struct LintCommand: ParsableCommand { var outputFormat: OutputFormat = .commandLine mutating func run() throws { - log = Logger(outputFormat: outputFormat) + if outputFormat == .xcode { + log = Logger.xcode + } guard FileManager.default.fileExists(atPath: config) else { log.message( diff --git a/Sources/Core/ConsoleLogger.swift b/Sources/Core/ConsoleLogger.swift new file mode 100644 index 0000000..ea0ecde --- /dev/null +++ b/Sources/Core/ConsoleLogger.swift @@ -0,0 +1,33 @@ +import Foundation + +/// Helper to log output to console. +public final class ConsoleLogger: Loggable { + /// Communicates a message to console 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. + /// - location: The file, line and char in line location string. + public func message(_ message: String, level: PrintLevel, fileLocation: Location?) { + switch level { + case .success: + print(formattedCurrentTime(), "✅", message.green) + + case .info: + print(formattedCurrentTime(), "ℹ️ ", message.lightBlue) + + case .warning: + print(formattedCurrentTime(), "⚠️ ", message.yellow) + + case .error: + print(formattedCurrentTime(), "❌", message.red) + } + } + + private func formattedCurrentTime() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss.SSS" + let dateTimeString = dateFormatter.string(from: Date()) + return "\(dateTimeString):" + } +} diff --git a/Sources/Core/Extensions/StringExt.swift b/Sources/Core/Extensions/StringExt.swift index d0e56d9..f9938ac 100644 --- a/Sources/Core/Extensions/StringExt.swift +++ b/Sources/Core/Extensions/StringExt.swift @@ -2,13 +2,13 @@ import Foundation extension String { /// Returns the location info for a given line index. - public func fileLocation(of index: String.Index) -> FileLocation { + public func fileLocation(of index: String.Index, filePath: String) -> Location { let prefix = self[startIndex.. String { + if let row = row { + if let column = column { + return "\(filePath.path(type: pathType)):\(row):\(column):" + } + + return "\(filePath.path(type: pathType)):\(row):" + } + + return "\(filePath.path(type: pathType))" + } +} diff --git a/Sources/Core/Loggable.swift b/Sources/Core/Loggable.swift new file mode 100644 index 0000000..2e94009 --- /dev/null +++ b/Sources/Core/Loggable.swift @@ -0,0 +1,26 @@ +import Foundation + +/// Shortcut to access the `Logger` within this project. +public var log: Loggable = ConsoleLogger() + +public protocol Loggable { + func message(_ message: String, level: PrintLevel, fileLocation: Location?) +} + +extension Loggable { + /// Exits the current program with the given fail state. + public func exit(fail: Bool) -> Never { + let statusCode = fail ? EXIT_FAILURE : EXIT_SUCCESS + + #if os(Linux) + Glibc.exit(statusCode) + #else + Darwin.exit(statusCode) + #endif + } + + /// Convenience overload of `message(:level:fileLocation:)` with `fileLocation` set to `nil`. + public func message(_ message: String, level: PrintLevel) { + self.message(message, level: level, fileLocation: nil) + } +} diff --git a/Sources/Core/Logger.swift b/Sources/Core/Logger.swift index c039607..105cb1a 100644 --- a/Sources/Core/Logger.swift +++ b/Sources/Core/Logger.swift @@ -1,129 +1,6 @@ import Foundation -import Rainbow -/// Shortcut to access the `Logger` within this project. -public var log = Logger(outputFormat: .commandLine) - -/// 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 - - 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 - } - } - } - - /// The output format of the logger. - public let outputFormat: OutputFormat - - /// Initializes a new Logger object with a given output format. - public init( - outputFormat: OutputFormat - ) { - self.outputFormat = outputFormat - } - - /// 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) { - switch outputFormat { - case .commandLine, .json: - consoleMessage(message, level: level) - - case .xcode: - xcodeMessage(message, level: level) - } - } - - /// Exits the current program with the given fail state. - public func exit(fail: Bool) -> Never { - let statusCode = fail ? EXIT_FAILURE : EXIT_SUCCESS - - #if os(Linux) - Glibc.exit(statusCode) - #else - Darwin.exit(statusCode) - #endif - } - - private func consoleMessage(_ message: String, level: PrintLevel) { - switch level { - case .success: - print(formattedCurrentTime(), "✅", message.green) - - case .info: - print(formattedCurrentTime(), "ℹ️ ", message.lightBlue) - - case .warning: - print(formattedCurrentTime(), "⚠️ ", message.yellow) - - case .error: - print(formattedCurrentTime(), "❌", message.red) - } - } - - /// 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) { - var locationPrefix = "" - - if let location = location { - locationPrefix = location + " " - } - - print("\(locationPrefix)\(level.rawValue): AnyLint: \(message)") - } - - private func formattedCurrentTime() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss.SSS" - let dateTime = dateFormatter.string(from: Date()) - return "\(dateTime):" - } -} - -extension Severity { - public var logLevel: Logger.PrintLevel { - switch self { - case .info: - return .info - - case .warning: - return .warning - - case .error: - return .error - } - } +public enum Logger { + public static let console: ConsoleLogger = .init() + public static let xcode: XcodeLogger = .init() } diff --git a/Sources/Core/PrintLevel.swift b/Sources/Core/PrintLevel.swift new file mode 100644 index 0000000..31d553f --- /dev/null +++ b/Sources/Core/PrintLevel.swift @@ -0,0 +1,33 @@ +import Foundation +import Rainbow + +/// 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 + + 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 + } + } +} diff --git a/Sources/Core/Regex.swift b/Sources/Core/Regex.swift index 749a15e..dc01fa1 100644 --- a/Sources/Core/Regex.swift +++ b/Sources/Core/Regex.swift @@ -58,11 +58,9 @@ public struct Regex { /// /// - returns: An optional `Match` describing the first match, or `nil`. public func firstMatch(in string: String) -> Match? { - let firstMatch = - regularExpression + 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 @@ -73,11 +71,9 @@ public struct Regex { /// /// - returns: An array of `Match` describing every match in `string`. public func matches(in string: String) -> [Match] { - let matches = - regularExpression + regularExpression .matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) .map { Match(result: $0, in: string) } - return matches } // MARK: Replacing @@ -100,6 +96,7 @@ public struct Regex { 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.. String { - let replacement = result.regularExpression! + result.regularExpression! .replacementString( for: result, in: baseString, offset: 0, template: template ) - - return replacement } // MARK: - CustomStringConvertible diff --git a/Sources/Core/Severity.swift b/Sources/Core/Severity.swift index a1c07ad..360b587 100644 --- a/Sources/Core/Severity.swift +++ b/Sources/Core/Severity.swift @@ -23,3 +23,18 @@ extension Severity: Comparable { } } } + +extension Severity { + public var logLevel: PrintLevel { + switch self { + case .info: + return .info + + case .warning: + return .warning + + case .error: + return .error + } + } +} diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index 54a9608..df0104f 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -2,35 +2,23 @@ import Foundation /// A violation found in a check. public struct Violation: Codable { - /// The file path the violation is related to. - public let filePath: String? - /// The matched string that violates the check. public let matchedString: String? /// The info about the exact location of the violation within the file. Will be ignored if no `filePath` specified. - public let fileLocation: FileLocation? + public let fileLocation: Location? /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? /// Initializes a violation object. public init( - filePath: String? = nil, matchedString: String? = nil, - fileLocation: FileLocation? = nil, + fileLocation: Location? = nil, appliedAutoCorrection: AutoCorrection? = nil ) { - self.filePath = filePath self.matchedString = matchedString self.fileLocation = fileLocation 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 fileLocation = fileLocation else { return filePath.path(type: pathType) } - return "\(filePath.path(type: pathType)):\(fileLocation.row):\(fileLocation.column):" - } } diff --git a/Sources/Core/XcodeLogger.swift b/Sources/Core/XcodeLogger.swift new file mode 100644 index 0000000..2c76733 --- /dev/null +++ b/Sources/Core/XcodeLogger.swift @@ -0,0 +1,20 @@ +import Foundation + +/// Helper to log output optimized for Xcode. +public final class XcodeLogger: Loggable { + /// 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 message(_ message: String, level: PrintLevel, fileLocation: Location?) { + var locationPrefix = "" + + if let fileLocation = fileLocation { + locationPrefix = fileLocation.locationMessage(pathType: .absolute) + " " + } + + print("\(locationPrefix)\(level.rawValue): AnyLint: \(message)") + } +} diff --git a/Sources/Reporting/Extensions/JSONEncoderExt.swift b/Sources/Reporting/Extensions/JSONEncoderExt.swift index a253fc4..7a48077 100644 --- a/Sources/Reporting/Extensions/JSONEncoderExt.swift +++ b/Sources/Reporting/Extensions/JSONEncoderExt.swift @@ -1,7 +1,7 @@ import Foundation extension JSONEncoder { - static var iso: Self { + static var iso: JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 return encoder diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index cfa341a..a884a47 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -100,7 +100,7 @@ extension LintResults { let checkViolations = violations(check: check, excludeAutocorrected: false) if checkViolations.isFilled { - let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage(pathType: .relative) != nil } + let violationsWithLocationMessage = checkViolations.filter { $0.fileLocation != nil } if violationsWithLocationMessage.isFilled { log.message( @@ -112,7 +112,10 @@ extension LintResults { 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) + log.message( + prefix + violation.fileLocation!.locationMessage(pathType: .relative), + level: check.severity.logLevel + ) let prefixLengthWhitespaces = (0.. Date: Sun, 5 Sep 2021 13:43:12 +0200 Subject: [PATCH 21/37] Complete LintResults tests to reach high coverage --- Package.resolved | 18 ++ Package.swift | 13 +- .../Reporting/Extensions/JSONDecoderExt.swift | 9 + Sources/Reporting/LintResults.swift | 14 +- Tests/ReportingTests/LintResultsTests.swift | 223 +++++++++++++++--- Tests/ReportingTests/TestLogger.swift | 19 ++ 6 files changed, 254 insertions(+), 42 deletions(-) create mode 100644 Sources/Reporting/Extensions/JSONDecoderExt.swift create mode 100644 Tests/ReportingTests/TestLogger.swift diff --git a/Package.resolved b/Package.resolved index 3de8d31..9777109 100644 --- a/Package.resolved +++ b/Package.resolved @@ -46,6 +46,24 @@ "version": "0.0.7" } }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump.git", + "state": { + "branch": null, + "revision": "8655495733d8c712db8d9078f47d17f3555db79f", + "version": "0.1.2" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", + "version": "0.2.1" + } + }, { "package": "Yams", "repositoryURL": "https://github.com/jpsim/Yams.git", diff --git a/Package.swift b/Package.swift index b385f98..48b7eb2 100644 --- a/Package.swift +++ b/Package.swift @@ -20,6 +20,9 @@ let package = Package( // Commonly used data structures for Swift` .package(url: "https://github.com/apple/swift-collections.git", from: "0.0.3"), + // A collection of tools for debugging, diffing, and testing your application's data structures. + .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "0.1.2"), + // Easily run shell commands from a Swift script or command line tool .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.3.0"), @@ -30,7 +33,7 @@ let package = Package( .target( name: "Core", dependencies: [ - .product(name: "Rainbow", package: "Rainbow") + .product(name: "Rainbow", package: "Rainbow"), ] ), .target(name: "Checkers", dependencies: ["Core"]), @@ -68,7 +71,13 @@ let package = Package( .testTarget(name: "CoreTests", dependencies: ["Core"]), .testTarget(name: "CheckersTests", dependencies: ["Checkers"]), .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), - .testTarget(name: "ReportingTests", dependencies: ["Reporting"]), + .testTarget( + name: "ReportingTests", + dependencies: [ + "Reporting", + .product(name: "CustomDump", package: "swift-custom-dump"), + ] + ), .testTarget(name: "CommandsTests", dependencies: ["Commands"]), ] ) diff --git a/Sources/Reporting/Extensions/JSONDecoderExt.swift b/Sources/Reporting/Extensions/JSONDecoderExt.swift new file mode 100644 index 0000000..c5dbc53 --- /dev/null +++ b/Sources/Reporting/Extensions/JSONDecoderExt.swift @@ -0,0 +1,9 @@ +import Foundation + +extension JSONDecoder { + static var iso: JSONDecoder { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + } +} diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index a884a47..e8fd046 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -17,7 +17,9 @@ extension LintResults { /// 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.isEmpty { + if let severityViolations = self[severity], + severityViolations.values.elements.contains(where: { !$0.isEmpty }) + { return severity } } @@ -78,7 +80,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.flatMap({ $0 }) else { return [] } + guard let violations = self[severity]?.values.elements.flatMap({ $0 }) else { return [] } guard excludeAutocorrected else { return violations } return violations.filter { $0.appliedAutoCorrection == nil } } @@ -95,7 +97,7 @@ extension LintResults { return violations.filter { $0.appliedAutoCorrection == nil } } - private func reportToConsole() { + func reportToConsole() { for check in allExecutedChecks { let checkViolations = violations(check: check, excludeAutocorrected: false) @@ -160,7 +162,7 @@ extension LintResults { ) } - private func reportToXcode() { + func reportToXcode() { for severity in keys.sorted().reversed() { guard let checkResultsAtSeverity = self[severity] else { continue } @@ -177,8 +179,8 @@ extension LintResults { } } - private func reportToFile(at path: String) { - let resultFileUrl = URL(fileURLWithPath: "anylint-results.json") + func reportToFile(at path: String) { + let resultFileUrl = URL(fileURLWithPath: path) do { let resultsData = try JSONEncoder.iso.encode(self) diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index bdcb0be..13dc0b3 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -1,48 +1,56 @@ @testable import Reporting import Core import XCTest +import CustomDump final class LintResultsTests: XCTestCase { private var sampleLintResults: LintResults { [ Severity.error: [ CheckInfo(id: "1", hint: "hint #1", severity: .error): [ - Violation(matchedString: "oink", fileLocation: .init(filePath: "/sample/path1", row: 4, column: 2)), - Violation(matchedString: "boo", fileLocation: .init(filePath: "/sample/path2", row: 40, column: 20)), + Violation(matchedString: "oink1", fileLocation: .init(filePath: "/sample/path1", row: 4, column: 2)), + Violation(matchedString: "boo1", fileLocation: .init(filePath: "/sample/path2", row: 40, column: 20)), Violation( fileLocation: .init(filePath: "/sample/path2"), appliedAutoCorrection: .init(before: "foo", after: "bar") ), - ], + ] + ], + Severity.warning: [ CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ - Violation(matchedString: "oink", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), - Violation(matchedString: "boo", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), + Violation(matchedString: "oink2", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), + Violation(matchedString: "boo2", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), Violation( fileLocation: .init(filePath: "/sample/path4"), appliedAutoCorrection: .init(before: "fool", after: "barl") ), - ], + ] + ], + Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) - ], - ] + ] + ], ] } func testAllExecutedChecks() { let allExecutedChecks = sampleLintResults.allExecutedChecks - XCTAssertEqual(allExecutedChecks.count, 3) - XCTAssertEqual(allExecutedChecks.map(\.id), ["1", "2", "3"]) + XCTAssertNoDifference(allExecutedChecks.count, 3) + XCTAssertNoDifference(allExecutedChecks.map(\.id), ["1", "2", "3"]) } func testAllFoundViolations() { let allFoundViolations = sampleLintResults.allFoundViolations - XCTAssertEqual(allFoundViolations.count, 7) - XCTAssertEqual( + XCTAssertNoDifference(allFoundViolations.count, 7) + XCTAssertNoDifference( allFoundViolations.map(\.fileLocation).map(\.?.filePath).map(\.?.last), ["1", "2", "2", "1", "3", "4", "0"] ) - XCTAssertEqual(allFoundViolations.map(\.matchedString), ["oink", "boo", nil, "oink", "boo", nil, "blubb"]) + XCTAssertNoDifference( + allFoundViolations.map(\.matchedString), + ["oink1", "boo1", nil, "oink2", "boo2", nil, "blubb"] + ) } func testMergeResults() { @@ -67,17 +75,17 @@ final class LintResultsTests: XCTestCase { let allExecutedChecks = lintResults.allExecutedChecks let allFoundViolations = lintResults.allFoundViolations - XCTAssertEqual(allExecutedChecks.count, 6) - XCTAssertEqual(allExecutedChecks.map(\.id), ["1", "2", "3", "1", "2", "4"]) + XCTAssertNoDifference(allExecutedChecks.count, 6) + XCTAssertNoDifference(allExecutedChecks.map(\.id), ["1", "1", "2", "4", "2", "3"]) - XCTAssertEqual(allFoundViolations.count, 10) - XCTAssertEqual( + XCTAssertNoDifference(allFoundViolations.count, 10) + XCTAssertNoDifference( allFoundViolations.map(\.fileLocation).map(\.?.filePath).map(\.?.last), - ["1", "2", "2", "1", "3", "4", "0", "4", "5", "1"] + ["1", "2", "2", "4", "5", "1", "1", "3", "4", "0"] ) - XCTAssertEqual( + XCTAssertNoDifference( allFoundViolations.map(\.matchedString), - ["oink", "boo", nil, "oink", "boo", nil, "blubb", "muuh", nil, "super"] + ["oink1", "boo1", nil, "muuh", nil, "super", "oink2", "boo2", nil, "blubb"] ) } @@ -85,13 +93,13 @@ final class LintResultsTests: XCTestCase { // TODO: [cg_2021-09-01] not yet implemented var lintResults = sampleLintResults - XCTAssertEqual(lintResults.allFoundViolations.count, 7) - XCTAssertEqual(lintResults.allExecutedChecks.count, 3) - XCTAssertEqual( + XCTAssertNoDifference(lintResults.allFoundViolations.count, 7) + XCTAssertNoDifference(lintResults.allExecutedChecks.count, 3) + XCTAssertNoDifference( lintResults.allFoundViolations.map(\.matchedString).map(\.?.first), ["o", "b", nil, "o", "b", nil, "b"] ) - XCTAssertEqual(lintResults.allExecutedChecks.map(\.id), ["1", "2", "3"]) + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1", "2", "3"]) lintResults.appendViolations( [ @@ -105,28 +113,175 @@ final class LintResultsTests: XCTestCase { forCheck: .init(id: "Added", hint: "hint for added") ) - XCTAssertEqual(lintResults.allFoundViolations.count, 10) - XCTAssertEqual(lintResults.allExecutedChecks.count, 4) - XCTAssertEqual( + XCTAssertNoDifference(lintResults.allFoundViolations.count, 10) + XCTAssertNoDifference(lintResults.allExecutedChecks.count, 4) + XCTAssertNoDifference( lintResults.allFoundViolations.map(\.matchedString).map(\.?.first), - ["o", "b", nil, "o", "b", nil, "b", "A", "B", nil] + ["o", "b", nil, "A", "B", nil, "o", "b", nil, "b"] ) - XCTAssertEqual(lintResults.allExecutedChecks.map(\.id), ["1", "2", "3", "Added"]) + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1", "Added", "2", "3"]) } func testReportToConsole() { - // TODO: [cg_2021-09-01] not yet implemented + let testLogger = TestLogger() + log = testLogger + + XCTAssertNoDifference(testLogger.loggedMessages, []) + sampleLintResults.reportToConsole() + + XCTAssertNoDifference( + testLogger.loggedMessages, + [ + "[error] [1] Found 3 violation(s) at:", + "[error] > 1. /sample/path1:4:2:", + "[info] Matching string: (trimmed & reduced whitespaces)", + "[info] > oink1", + "[error] > 2. /sample/path2:40:20:", + "[info] Matching string: (trimmed & reduced whitespaces)", + "[info] > boo1", + "[error] > 3. /sample/path2", + "[info] Autocorrection applied, the diff is: (+ added, - removed)", + "[info] - foo", + "[info] + bar", + "[error] >> Hint: hint #1", + "[warning] [2] Found 3 violation(s) at:", + "[warning] > 1. /sample/path1:5:6:", + "[info] Matching string: (trimmed & reduced whitespaces)", + "[info] > oink2", + "[warning] > 2. /sample/path3:50:60:", + "[info] Matching string: (trimmed & reduced whitespaces)", + "[info] > boo2", + "[warning] > 3. /sample/path4", + "[info] Autocorrection applied, the diff is: (+ added, - removed)", + "[info] - fool", + "[info] + barl", + "[warning] >> Hint: hint #2", + "[info] [3] Found 1 violation(s) at:", + "[info] > 1. /sample/path0:10:20:", + "[info] Matching string: (trimmed & reduced whitespaces)", + "[info] > blubb", + "[info] >> Hint: hint #3", + "[error] Performed 3 check(s) and found 3 error(s) & 3 warning(s).", + ] + ) } func testReportToXcode() { - // TODO: [cg_2021-09-01] not yet implemented + let testLogger = TestLogger() + log = testLogger + + XCTAssertNoDifference(testLogger.loggedMessages, []) + sampleLintResults.reportToXcode() + + XCTAssertNoDifference( + testLogger.loggedMessages, + [ + "[error] /sample/path1:4:2: [1] hint #1", + "[error] /sample/path2:40:20: [1] hint #1", + "[warning] /sample/path1:5:6: [2] hint #2", + "[warning] /sample/path3:50:60: [2] hint #2", + "[info] /sample/path0:10:20: [3] hint #3", + ] + ) } - func testReportToFile() { - // TODO: [cg_2021-09-01] not yet implemented + func testReportToFile() throws { + let resultFileUrl = URL(fileURLWithPath: "anylint-test-results.json") + + if FileManager.default.fileExists(atPath: resultFileUrl.path) { + try FileManager.default.removeItem(at: resultFileUrl) + } + XCTAssertFalse(FileManager.default.fileExists(atPath: resultFileUrl.path)) + + sampleLintResults.reportToFile(at: resultFileUrl.path) + XCTAssert(FileManager.default.fileExists(atPath: resultFileUrl.path)) + + let reportedContents = try Data(contentsOf: resultFileUrl) + let reportedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: reportedContents) + + XCTAssertNoDifference(sampleLintResults.map(\.key), reportedLintResults.map(\.key)) } func testViolations() { - // TODO: [cg_2021-09-01] not yet implemented + let lintResults = sampleLintResults + + XCTAssertNoDifference( + lintResults.violations(severity: .warning, excludeAutocorrected: false).map(\.matchedString), + ["oink2", "boo2", nil] + ) + + XCTAssertNoDifference( + lintResults.violations(severity: .warning, excludeAutocorrected: true).map(\.matchedString), + ["oink2", "boo2"] + ) + + XCTAssertNoDifference( + lintResults + .violations(check: .init(id: "1", hint: "hint #1"), excludeAutocorrected: false).map(\.matchedString), + ["oink1", "boo1", nil] + ) + + XCTAssertNoDifference( + lintResults + .violations(check: .init(id: "1", hint: "hint #1"), excludeAutocorrected: true).map(\.matchedString), + ["oink1", "boo1"] + ) + } + + func testMaxViolationSeverity() { + var lintResults: LintResults = sampleLintResults + XCTAssertEqual(sampleLintResults.maxViolationSeverity(excludeAutocorrected: false), .error) + + lintResults = [ + Severity.error: [ + CheckInfo(id: "1", hint: "hint #1", severity: .error): [] + ], + Severity.warning: [ + CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ + Violation(matchedString: "oink2", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), + Violation(matchedString: "boo2", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), + Violation( + fileLocation: .init(filePath: "/sample/path4"), + appliedAutoCorrection: .init(before: "fool", after: "barl") + ), + ] + ], + Severity.info: [ + CheckInfo(id: "3", hint: "hint #3", severity: .info): [ + Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + ] + ], + ] + XCTAssertEqual(lintResults.maxViolationSeverity(excludeAutocorrected: false), .warning) + + lintResults = [ + Severity.error: [ + CheckInfo(id: "1", hint: "hint #1", severity: .error): [] + ], + Severity.warning: [ + CheckInfo(id: "2", hint: "hint #2", severity: .warning): [] + ], + Severity.info: [ + CheckInfo(id: "3", hint: "hint #3", severity: .info): [ + Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + ] + ], + ] + XCTAssertEqual(lintResults.maxViolationSeverity(excludeAutocorrected: false), .info) + + lintResults = [ + Severity.error: [ + CheckInfo(id: "1", hint: "hint #1", severity: .error): [] + ], + Severity.warning: [ + CheckInfo(id: "2", hint: "hint #2", severity: .warning): [] + ], + Severity.info: [ + CheckInfo(id: "3", hint: "hint #3", severity: .info): [] + ], + ] + XCTAssertEqual(lintResults.maxViolationSeverity(excludeAutocorrected: false), nil) + + XCTAssertEqual(LintResults().maxViolationSeverity(excludeAutocorrected: false), nil) } } diff --git a/Tests/ReportingTests/TestLogger.swift b/Tests/ReportingTests/TestLogger.swift new file mode 100644 index 0000000..9c164f0 --- /dev/null +++ b/Tests/ReportingTests/TestLogger.swift @@ -0,0 +1,19 @@ +import Foundation +import Core + +final class TestLogger: Loggable { + var loggedMessages: [String] = [] + + func message(_ message: String, level: PrintLevel, fileLocation: Location?) { + if let fileLocation = fileLocation { + loggedMessages.append( + "[\(level.rawValue)] \(fileLocation.locationMessage(pathType: .relative)) \(message)" + ) + } + else { + loggedMessages.append( + "[\(level.rawValue)] \(message)" + ) + } + } +} From 4199b7a7410bb227aca728db59b32c4fb0552b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 5 Sep 2021 14:27:26 +0200 Subject: [PATCH 22/37] Refactor files in project + start re-adding tests --- .gitignore | 3 + Package.resolved | 79 --------- Package.swift | 8 +- .../FileManagerExt.swift | 0 .../Checkers/{Helpers => }/FilesSearch.swift | 0 .../Commands/{ => Compatibility}/main.swift | 0 Sources/Configuration/Templates/Blank.yml | 9 +- .../Core/{ => Logging}/ConsoleLogger.swift | 0 Sources/Core/{ => Logging}/Loggable.swift | 0 Sources/Core/{ => Logging}/Logger.swift | 0 Sources/Core/{ => Logging}/PrintLevel.swift | 0 Sources/Core/{ => Logging}/XcodeLogger.swift | 0 Sources/Reporting/Statistics.swift | 154 ------------------ .../TestSupport}/TestLogger.swift | 10 +- Sources/TestSupport/XCTestCaseExt.swift | 26 +++ .../FileContentsCheckerTests.swift | 149 ++++++++--------- Tests/ReportingTests/LintResultsTests.swift | 1 + Tests/ReportingTests/StatisticsTests.swift | 120 -------------- 18 files changed, 118 insertions(+), 441 deletions(-) delete mode 100644 Package.resolved rename Sources/Checkers/{Helpers => Extensions}/FileManagerExt.swift (100%) rename Sources/Checkers/{Helpers => }/FilesSearch.swift (100%) rename Sources/Commands/{ => Compatibility}/main.swift (100%) rename Sources/Core/{ => Logging}/ConsoleLogger.swift (100%) rename Sources/Core/{ => Logging}/Loggable.swift (100%) rename Sources/Core/{ => Logging}/Logger.swift (100%) rename Sources/Core/{ => Logging}/PrintLevel.swift (100%) rename Sources/Core/{ => Logging}/XcodeLogger.swift (100%) delete mode 100644 Sources/Reporting/Statistics.swift rename {Tests/ReportingTests => Sources/TestSupport}/TestLogger.swift (59%) create mode 100644 Sources/TestSupport/XCTestCaseExt.swift delete mode 100644 Tests/ReportingTests/StatisticsTests.swift diff --git a/.gitignore b/.gitignore index a016df8..89bf5c2 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,6 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output + +# project-specific +anylint-test-results.json diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 9777109..0000000 --- a/Package.resolved +++ /dev/null @@ -1,79 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "BetterCodable", - "repositoryURL": "https://github.com/marksands/BetterCodable.git", - "state": { - "branch": null, - "revision": "61153170668db7a46a20a87e35e70f80b24d4eb5", - "version": "0.4.0" - } - }, - { - "package": "Rainbow", - "repositoryURL": "https://github.com/onevcat/Rainbow.git", - "state": { - "branch": null, - "revision": "7c3dad0e918534c6d19dd1048bde734c246d05fe", - "version": "4.0.0" - } - }, - { - "package": "ShellOut", - "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", - "state": { - "branch": null, - "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", - "version": "2.3.0" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "83b23d940471b313427da226196661856f6ba3e0", - "version": "0.4.4" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", - "version": "0.0.7" - } - }, - { - "package": "swift-custom-dump", - "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump.git", - "state": { - "branch": null, - "revision": "8655495733d8c712db8d9078f47d17f3555db79f", - "version": "0.1.2" - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", - "version": "0.2.1" - } - }, - { - "package": "Yams", - "repositoryURL": "https://github.com/jpsim/Yams.git", - "state": { - "branch": null, - "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", - "version": "4.0.6" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 48b7eb2..9bcce97 100644 --- a/Package.swift +++ b/Package.swift @@ -68,14 +68,16 @@ let package = Package( ), // test targets - .testTarget(name: "CoreTests", dependencies: ["Core"]), - .testTarget(name: "CheckersTests", dependencies: ["Checkers"]), + .target(name: "TestSupport", dependencies: ["Core"]), + .testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]), + .testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]), .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), .testTarget( name: "ReportingTests", dependencies: [ - "Reporting", .product(name: "CustomDump", package: "swift-custom-dump"), + "Reporting", + "TestSupport", ] ), .testTarget(name: "CommandsTests", dependencies: ["Commands"]), diff --git a/Sources/Checkers/Helpers/FileManagerExt.swift b/Sources/Checkers/Extensions/FileManagerExt.swift similarity index 100% rename from Sources/Checkers/Helpers/FileManagerExt.swift rename to Sources/Checkers/Extensions/FileManagerExt.swift diff --git a/Sources/Checkers/Helpers/FilesSearch.swift b/Sources/Checkers/FilesSearch.swift similarity index 100% rename from Sources/Checkers/Helpers/FilesSearch.swift rename to Sources/Checkers/FilesSearch.swift diff --git a/Sources/Commands/main.swift b/Sources/Commands/Compatibility/main.swift similarity index 100% rename from Sources/Commands/main.swift rename to Sources/Commands/Compatibility/main.swift diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index 126941f..c36f66f 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -19,4 +19,11 @@ FilePaths: [] # - { before: 'README.markdown', after: 'README.md' } CustomScripts: [] -# TODO: add sample entry +# - id: LintConfig +# hint: 'Lint the AnyLint config file to conform to YAML best practices.' +# command: |- +# if which yamllint > /dev/null; then +# yamllint anylint.yml +# else +# echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint" +# fi diff --git a/Sources/Core/ConsoleLogger.swift b/Sources/Core/Logging/ConsoleLogger.swift similarity index 100% rename from Sources/Core/ConsoleLogger.swift rename to Sources/Core/Logging/ConsoleLogger.swift diff --git a/Sources/Core/Loggable.swift b/Sources/Core/Logging/Loggable.swift similarity index 100% rename from Sources/Core/Loggable.swift rename to Sources/Core/Logging/Loggable.swift diff --git a/Sources/Core/Logger.swift b/Sources/Core/Logging/Logger.swift similarity index 100% rename from Sources/Core/Logger.swift rename to Sources/Core/Logging/Logger.swift diff --git a/Sources/Core/PrintLevel.swift b/Sources/Core/Logging/PrintLevel.swift similarity index 100% rename from Sources/Core/PrintLevel.swift rename to Sources/Core/Logging/PrintLevel.swift diff --git a/Sources/Core/XcodeLogger.swift b/Sources/Core/Logging/XcodeLogger.swift similarity index 100% rename from Sources/Core/XcodeLogger.swift rename to Sources/Core/Logging/XcodeLogger.swift diff --git a/Sources/Reporting/Statistics.swift b/Sources/Reporting/Statistics.swift deleted file mode 100644 index 7a4e5d6..0000000 --- a/Sources/Reporting/Statistics.swift +++ /dev/null @@ -1,154 +0,0 @@ -//import Foundation -// -//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 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) -// } -// -// /// 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() { -// if executedChecks.isEmpty { -// log.message("No checks found to perform.", level: .warning) -// } -// else if violationsBySeverity.values.contains(where: { $0.isFilled }) { -// switch log.outputType { -// case .console, .test: -// logViolationsToConsole() -// -// case .xcode: -// showViolationsInXcode() -// } -// } -// else { -// log.message( -// "Performed \(executedChecks.count) check(s) in \(filesChecked.count) file(s) without any violations.", -// level: .success -// ) -// } -// } -// -// 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.. " + 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/Tests/ReportingTests/TestLogger.swift b/Sources/TestSupport/TestLogger.swift similarity index 59% rename from Tests/ReportingTests/TestLogger.swift rename to Sources/TestSupport/TestLogger.swift index 9c164f0..31cd545 100644 --- a/Tests/ReportingTests/TestLogger.swift +++ b/Sources/TestSupport/TestLogger.swift @@ -1,10 +1,14 @@ import Foundation import Core -final class TestLogger: Loggable { - var loggedMessages: [String] = [] +public final class TestLogger: Loggable { + public var loggedMessages: [String] - func message(_ message: String, level: PrintLevel, fileLocation: Location?) { + public init() { + loggedMessages = [] + } + + public func message(_ message: String, level: PrintLevel, fileLocation: Location?) { if let fileLocation = fileLocation { loggedMessages.append( "[\(level.rawValue)] \(fileLocation.locationMessage(pathType: .relative)) \(message)" diff --git a/Sources/TestSupport/XCTestCaseExt.swift b/Sources/TestSupport/XCTestCaseExt.swift new file mode 100644 index 0000000..892d9af --- /dev/null +++ b/Sources/TestSupport/XCTestCaseExt.swift @@ -0,0 +1,26 @@ +import Foundation +import XCTest + +extension XCTestCase { + public typealias TemporaryFile = (subpath: String, contents: String) + + public var tempDir: String { "AnyLintTempTests" } + + public 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) + } + + try? testCode(filePathsToCheck) + try? FileManager.default.removeItem(atPath: tempDir) + } +} diff --git a/Tests/CheckersTests/FileContentsCheckerTests.swift b/Tests/CheckersTests/FileContentsCheckerTests.swift index 7d32307..9de12f1 100644 --- a/Tests/CheckersTests/FileContentsCheckerTests.swift +++ b/Tests/CheckersTests/FileContentsCheckerTests.swift @@ -1,90 +1,77 @@ @testable import Checkers import XCTest +import Core +import TestSupport final class FileContentsCheckerTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + 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 violations = try FileContentsChecker( + id: "Whitespacing", + hint: "Always add a single whitespace around '='.", + severity: .warning, + regex: Regex(#"(let|var) \w+=\w+"#), + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, + repeatIfAutoCorrected: false + ) + .performCheck() + + XCTAssertEqual(violations.count, 2) + + XCTAssertEqual(violations[0].matchedString, "let x=5") + XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[0].fileLocation?.row, 1) + XCTAssertEqual(violations[0].fileLocation!.column, 1) + + XCTAssertEqual(violations[1].matchedString, "var y=10") + XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].fileLocation?.row, 2) + XCTAssertEqual(violations[1].fileLocation?.column, 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 violations = try FileContentsChecker( + id: "Whitespacing", + hint: "Always add a single whitespace around '='.", + severity: .warning, + regex: Regex(#"(let|var) \w+=\w+"#), + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, + repeatIfAutoCorrected: false + ) + .performCheck() + + XCTAssertEqual(violations.count, 2) + + XCTAssertEqual(violations[0].matchedString, "let x=5") + XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[0].fileLocation?.row, 4) + XCTAssertEqual(violations[0].fileLocation?.column, 1) + + XCTAssertEqual(violations[1].matchedString, "var y=10") + XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[1].fileLocation?.row, 5) + XCTAssertEqual(violations[1].fileLocation?.column, 1) + } } - // 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+"#, - // 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+"#, - // 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"), diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index 13dc0b3..7b31fa1 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -2,6 +2,7 @@ import Core import XCTest import CustomDump +import TestSupport final class LintResultsTests: XCTestCase { private var sampleLintResults: LintResults { diff --git a/Tests/ReportingTests/StatisticsTests.swift b/Tests/ReportingTests/StatisticsTests.swift deleted file mode 100644 index 2879137..0000000 --- a/Tests/ReportingTests/StatisticsTests.swift +++ /dev/null @@ -1,120 +0,0 @@ -@testable import Core -import XCTest - -final class StatisticsTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented - } - - // 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() { - // Statistics.shared.logCheckSummary() - // XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) - // XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) - // XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "No checks found to perform.") - // - // 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() - // - // XCTAssertEqual( - // TestHelper.shared.consoleOutputs.map { $0.level }, - // [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] - // ) - // - // let expectedOutputs = [ - // "\("[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) - // } - // } -} From 5bd9ac77fca185945b350647a86084dc22a8f898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 5 Sep 2021 20:33:28 +0200 Subject: [PATCH 23/37] Re-enable uncommented test code for Checkers --- Sources/TestSupport/TestLogger.swift | 6 + .../FileContentsCheckerTests.swift | 311 +++++++++--------- .../CheckersTests/FilePathsCheckerTests.swift | 129 ++++---- Tests/CheckersTests/FilesSearchTests.swift | 112 +++---- Tests/CheckersTests/LintTests.swift | 242 +++++++------- 5 files changed, 393 insertions(+), 407 deletions(-) diff --git a/Sources/TestSupport/TestLogger.swift b/Sources/TestSupport/TestLogger.swift index 31cd545..1d12b57 100644 --- a/Sources/TestSupport/TestLogger.swift +++ b/Sources/TestSupport/TestLogger.swift @@ -3,6 +3,7 @@ import Core public final class TestLogger: Loggable { public var loggedMessages: [String] + public var exitStatusCode: Int32? public init() { loggedMessages = [] @@ -20,4 +21,9 @@ public final class TestLogger: Loggable { ) } } + + public func exit(fail: Bool) -> Never { + exitStatusCode = fail ? EXIT_FAILURE : EXIT_SUCCESS + fatalError() + } } diff --git a/Tests/CheckersTests/FileContentsCheckerTests.swift b/Tests/CheckersTests/FileContentsCheckerTests.swift index 9de12f1..592cf9d 100644 --- a/Tests/CheckersTests/FileContentsCheckerTests.swift +++ b/Tests/CheckersTests/FileContentsCheckerTests.swift @@ -72,164 +72,155 @@ final class FileContentsCheckerTests: XCTestCase { } } - // 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+"#, - // 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+)"#, - // 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 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 noWorldCheck() -> CheckInfo { - // CheckInfo(id: "no_world", hint: "Do not include the global world, be more specific instead.", severity: .error) - // } + private func sayHelloChecker(filePathsToCheck: [String]) -> FilePathsChecker { + FilePathsChecker( + id: "say_hello", + hint: "Should always say hello.", + severity: .info, + regex: try! Regex(#".*Hello\.swift"#), + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, + violateIfNoMatchesFound: true + ) + } + + private func noWorldChecker(filePathsToCheck: [String]) -> FilePathsChecker { + FilePathsChecker( + id: "no_world", + hint: "Do not include the global world, be more specific instead.", + severity: .error, + regex: try! Regex(#".*World\.swift"#), + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, + violateIfNoMatchesFound: false + ) + } } diff --git a/Tests/CheckersTests/FilesSearchTests.swift b/Tests/CheckersTests/FilesSearchTests.swift index 3c93c6e..4f373e7 100644 --- a/Tests/CheckersTests/FilesSearchTests.swift +++ b/Tests/CheckersTests/FilesSearchTests.swift @@ -1,67 +1,59 @@ @testable import Checkers import XCTest +import Core final class FilesSearchTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + 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: [try Regex("World")] + ) + XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift"]) + } } - // 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"#)] - // ) - // } - // - // // 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)" })) - // } - // } - // } + 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)" })) + + 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/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index bb9bc1b..e99f942 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -1,121 +1,135 @@ @testable import Checkers import XCTest +import Core +import TestSupport final class LintTests: XCTestCase { - func testSample() { - XCTAssertTrue(true) // TODO: [cg_2021-07-31] not yet implemented + var testLogger: TestLogger = .init() + + override func setUp() { + testLogger = TestLogger() + log = testLogger + } + + func testValidateRegexMatchesForEach() { + XCTAssertNil(testLogger.exitStatusCode) + + let regex = try! 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(testLogger.exitStatusCode) + + // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` + // Lint.validate( + // regex: regex, + // matchesForEach: ["foo1bar", "FooBar", "myfoo4barbeque"], + // checkInfo: checkInfo + // ) + // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) + } + + func testValidateRegexDoesNotMatchAny() { + XCTAssertNil(testLogger.exitStatusCode) + + let regex = try! 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(testLogger.exitStatusCode) + + // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` + // Lint.validate( + // regex: regex, + // doesNotMatchAny: ["fooLbar", "foobar", "myfoo40barbeque"], + // checkInfo: checkInfo + // ) + // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) + } + + func testValidateAutocorrectsAllExamplesWithAnonymousGroups() { + XCTAssertNil(testLogger.exitStatusCode) + + 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(testLogger.exitStatusCode) + + // 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"), + // 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(testLogger.exitStatusCode, EXIT_FAILURE) + } + + func testValidateAutocorrectsAllExamplesWithNamedGroups() { + XCTAssertNil(testLogger.exitStatusCode) + + let namedCaptureRegex = 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: namedCaptureRegex, + autocorrectReplacement: "$3.$2.$1" + ) + + XCTAssertNil(testLogger.exitStatusCode) + + // 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"), + // 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(testLogger.exitStatusCode, EXIT_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) - // } + func testRunCustomScript() { + // TODO: [cg_2021-09-05] not yet implemented + } + + func testValidateParameterCombinations() { + // TODO: [cg_2021-09-05] not yet implemented + } + + func testCheckFileContents() { + // TODO: [cg_2021-09-05] not yet implemented + } + + func testCheckFilePaths() { + // TODO: [cg_2021-09-05] not yet implemented + } } From e7cc71138bb1cb57318fd8a8aaad4c672a837a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 7 Sep 2021 14:41:13 +0200 Subject: [PATCH 24/37] Write custom script tests + some refactoring --- Package.swift | 26 ++-- README.md | 2 +- Sources/Checkers/FileContentsChecker.swift | 8 +- Sources/Checkers/FilePathsChecker.swift | 4 +- Sources/Checkers/Lint.swift | 48 +++++++- Sources/Commands/LintCommand.swift | 4 +- Sources/Configuration/Templates/Blank.yml | 2 +- .../Configuration/Templates/OpenSource.yml | 2 +- Sources/Core/AutoCorrection.swift | 2 +- Sources/Core/CheckInfo.swift | 31 +++-- Sources/Core/Location.swift | 2 +- Sources/Core/Logging/ConsoleLogger.swift | 2 +- Sources/Core/Logging/Loggable.swift | 4 +- Sources/Core/Logging/XcodeLogger.swift | 6 +- Sources/Core/Violation.swift | 8 +- .../Reporting/Extensions/JSONDecoderExt.swift | 2 +- .../Reporting/Extensions/JSONEncoderExt.swift | 3 +- Sources/Reporting/LintResults.swift | 12 +- Sources/TestSupport/TestLogger.swift | 6 +- .../FileContentsCheckerTests.swift | 114 +++++++++--------- .../CheckersTests/FilePathsCheckerTests.swift | 8 +- Tests/CheckersTests/LintTests.swift | 69 ++++++++++- Tests/CoreTests/ViolationTests.swift | 10 +- Tests/ReportingTests/LintResultsTests.swift | 87 ++++++++++--- 24 files changed, 316 insertions(+), 146 deletions(-) diff --git a/Package.swift b/Package.swift index 9bcce97..010ef1d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( name: "AnyLint", - platforms: [.macOS(.v10_12)], + platforms: [.macOS(.v10_15)], products: [ .executable(name: "anylint", targets: ["Commands"]), ], @@ -36,7 +36,14 @@ let package = Package( .product(name: "Rainbow", package: "Rainbow"), ] ), - .target(name: "Checkers", dependencies: ["Core"]), + .target( + name: "Checkers", + dependencies: [ + "Core", + "Reporting", + .product(name: "ShellOut", package: "ShellOut"), + ] + ), .target( name: "Configuration", dependencies: [ @@ -68,18 +75,17 @@ let package = Package( ), // test targets - .target(name: "TestSupport", dependencies: ["Core"]), - .testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]), - .testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]), - .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), - .testTarget( - name: "ReportingTests", + .target( + name: "TestSupport", dependencies: [ + "Core", .product(name: "CustomDump", package: "swift-custom-dump"), - "Reporting", - "TestSupport", ] ), + .testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]), + .testTarget(name: "CheckersTests", dependencies: ["Checkers", "TestSupport"]), + .testTarget(name: "ConfigurationTests", dependencies: ["Configuration"]), + .testTarget(name: "ReportingTests", dependencies: ["Reporting", "TestSupport"]), .testTarget(name: "CommandsTests", dependencies: ["Commands"]), ] ) diff --git a/README.md b/README.md index f87af9e..114f898 100644 --- a/README.md +++ b/README.md @@ -529,7 +529,7 @@ multiline2: |+ multiline3: |- This will ignore any trailing newlines and - will end with the lest non-newline character (the following dot in this case -->). + will end with the last non-newline character (the following dot in this case -->). ``` diff --git a/Sources/Checkers/FileContentsChecker.swift b/Sources/Checkers/FileContentsChecker.swift index de8ba3d..98c2f66 100644 --- a/Sources/Checkers/FileContentsChecker.swift +++ b/Sources/Checkers/FileContentsChecker.swift @@ -43,11 +43,11 @@ extension FileContentsChecker: Checker { let skipHereRegex = try Regex(#"AnyLint\.skipHere:[^\n]*[, ]\#(id)"#) for match in regex.matches(in: fileContents).reversed() { - let fileLocation = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath) + let location = fileContents.fileLocation(of: match.range.lowerBound, filePath: filePath) // skip found match if contains `AnyLint.skipHere: ` in same line or one line before guard - !linesInFile.containsLine(at: [fileLocation.row! - 2, fileLocation.row! - 1], matchingRegex: skipHereRegex) + !linesInFile.containsLine(at: [location.row! - 2, location.row! - 1], matchingRegex: skipHereRegex) else { continue } let autoCorrection: AutoCorrection? = { @@ -70,7 +70,7 @@ extension FileContentsChecker: Checker { violations.append( Violation( matchedString: match.string, - fileLocation: fileLocation, + location: location, appliedAutoCorrection: autoCorrection ) ) @@ -93,7 +93,7 @@ extension FileContentsChecker: Checker { if repeatIfAutoCorrected && violations.contains(where: { $0.appliedAutoCorrection != nil }) { // only paths where auto-corrections were applied need to be re-checked let filePathsToReCheck = Array( - Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.fileLocation!.filePath }) + Set(violations.filter { $0.appliedAutoCorrection != nil }.map { $0.location!.filePath }) ) .sorted() diff --git a/Sources/Checkers/FilePathsChecker.swift b/Sources/Checkers/FilePathsChecker.swift index 9c83e36..729d686 100644 --- a/Sources/Checkers/FilePathsChecker.swift +++ b/Sources/Checkers/FilePathsChecker.swift @@ -33,7 +33,7 @@ extension FilePathsChecker: Checker { let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count if matchingFilePathsCount <= 0 { violations.append( - Violation(fileLocation: nil, appliedAutoCorrection: nil) + Violation(location: nil, appliedAutoCorrection: nil) ) } } @@ -50,7 +50,7 @@ extension FilePathsChecker: Checker { violations.append( Violation( - fileLocation: .init(filePath: filePath), + location: .init(filePath: filePath), appliedAutoCorrection: appliedAutoCorrection ) ) diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index 28bf807..612e023 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -1,6 +1,8 @@ import Foundation import Core import OrderedCollections +import ShellOut +import Reporting /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { @@ -129,9 +131,27 @@ public enum Lint { /// Run custom scripts as checks. /// - /// - Returns: If the command produces an output in the ``LintResults`` JSON format, will forward them. 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 -> [Violation] { - fatalError() // TODO: [cg_2021-07-09] not yet implemented + /// - 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") + try command.write(to: tempScriptFileUrl, atomically: true, encoding: .utf8) + + let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) + if let jsonString = output.lintResultsJsonString, + let jsonData = jsonString.data(using: .utf8), + let lintResults: LintResults = try? JSONDecoder.iso.decode(LintResults.self, from: jsonData) + { + return lintResults + } + else if let jsonString = output.violationsArrayJsonString, + let jsonData = jsonString.data(using: .utf8), + let violations: [Violation] = try? JSONDecoder.iso.decode([Violation].self, from: jsonData) + { + return [checkInfo.severity: [checkInfo: violations]] + } + else { + return [checkInfo.severity: [checkInfo: [Violation()]]] + } } static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { @@ -196,10 +216,30 @@ public enum Lint { 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 \(checkInfo.id): `autoCorrectReplacement` and `violateIfNoMatchesFound` can't be used together.", level: .error ) log.exit(fail: true) } } } + +fileprivate extension String { + var lintResultsJsonString: String? { + try! Regex( + #"\{.*(?:\"error\"\s*:|\"warning\"\s*:|\"info\"\s*:).*\}"#, + options: .dotMatchesLineSeparators + ) + .firstMatch(in: self)? + .string + } + + var violationsArrayJsonString: String? { + try! Regex( + #"\[(?:\s*\{.*\}\s*,)*(?:\s*\{.*\}\s*)?\]"#, + options: .dotMatchesLineSeparators + ) + .firstMatch(in: self)? + .string + } +} diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index eb60415..2b063d8 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -103,12 +103,12 @@ struct LintCommand: ParsableCommand { // run `CustomScripts` checks for customScriptConfig in lintConfig.customScripts { - let violations = try Lint.runCustomScript( + let customScriptLintResults = try Lint.runCustomScript( checkInfo: customScriptConfig.checkInfo, command: customScriptConfig.command ) - lintResults.appendViolations(violations, forCheck: customScriptConfig.checkInfo) + lintResults.mergeResults(customScriptLintResults) } // report violations & exit with right status code diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index c36f66f..8dd7b5d 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -25,5 +25,5 @@ CustomScripts: [] # if which yamllint > /dev/null; then # yamllint anylint.yml # else -# echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint" +# echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' # fi diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 1e9068d..78cf79f 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -57,5 +57,5 @@ CustomScripts: if which yamllint > /dev/null; then yamllint anylint.yml else - echo "{ warning: yamllint not installed, see installation instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint" + echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' fi diff --git a/Sources/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift index 7f791ea..426b04b 100644 --- a/Sources/Core/AutoCorrection.swift +++ b/Sources/Core/AutoCorrection.swift @@ -1,7 +1,7 @@ import Foundation /// Information about an autocorrection. -public struct AutoCorrection: Codable { +public struct AutoCorrection: Codable, Equatable { private enum Constants { /// The number of newlines required in both before and after of AutoCorrections required to use diff for outputs. static let newlinesRequiredForDiffing: Int = 3 diff --git a/Sources/Core/CheckInfo.swift b/Sources/Core/CheckInfo.swift index bd3805f..7653e35 100644 --- a/Sources/Core/CheckInfo.swift +++ b/Sources/Core/CheckInfo.swift @@ -35,10 +35,26 @@ extension CheckInfo: Codable { ) throws { let container = try decoder.singleValueContainer() let rawString = try container.decode(String.self) + self.init(rawValue: rawString)! + } - let customSeverityRegex = try Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#) + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} + +extension CheckInfo: RawRepresentable { + public var rawValue: String { + "\(id)@\(severity.rawValue): \(hint)" + } - if let match = customSeverityRegex.firstMatch(in: rawString) { + public init?( + rawValue: String + ) { + let customSeverityRegex = try! Regex(#"^([^@:]+)@([^:]+): ?(.*)$"#) + + if let match = customSeverityRegex.firstMatch(in: rawValue) { let id = match.captures[0]! let severityString = match.captures[1]! let hint = match.captures[2]! @@ -54,11 +70,11 @@ extension CheckInfo: Codable { self = .init(id: id, hint: hint, severity: severity) } else { - let defaultSeverityRegex = try Regex(#"^([^@:]+): ?(.*$)"#) + let defaultSeverityRegex = try! Regex(#"^([^@:]+): ?(.*$)"#) - guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawString) else { + guard let defaultSeverityMatch = defaultSeverityRegex.firstMatch(in: rawValue) else { log.message( - "Could not convert String literal '\(rawString)' to type CheckInfo. Please check the structure to be: (@): ", + "Could not convert String literal '\(rawValue)' to type CheckInfo. Please check the structure to be: (@): ", level: .error ) log.exit(fail: true) @@ -70,9 +86,4 @@ extension CheckInfo: Codable { self = .init(id: id, hint: hint) } } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode("\(id)@\(severity.rawValue): \(hint)") - } } diff --git a/Sources/Core/Location.swift b/Sources/Core/Location.swift index 347dda0..3e95546 100644 --- a/Sources/Core/Location.swift +++ b/Sources/Core/Location.swift @@ -1,7 +1,7 @@ import Foundation /// Info about the exact location of a character in a given file. -public struct Location: Codable { +public struct Location: Codable, Equatable { /// The path to the file. public let filePath: String diff --git a/Sources/Core/Logging/ConsoleLogger.swift b/Sources/Core/Logging/ConsoleLogger.swift index ea0ecde..98206c9 100644 --- a/Sources/Core/Logging/ConsoleLogger.swift +++ b/Sources/Core/Logging/ConsoleLogger.swift @@ -8,7 +8,7 @@ public final class ConsoleLogger: Loggable { /// - 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 message(_ message: String, level: PrintLevel, fileLocation: Location?) { + public func message(_ message: String, level: PrintLevel, location: Location?) { switch level { case .success: print(formattedCurrentTime(), "✅", message.green) diff --git a/Sources/Core/Logging/Loggable.swift b/Sources/Core/Logging/Loggable.swift index 2e94009..6baaf60 100644 --- a/Sources/Core/Logging/Loggable.swift +++ b/Sources/Core/Logging/Loggable.swift @@ -4,7 +4,7 @@ import Foundation public var log: Loggable = ConsoleLogger() public protocol Loggable { - func message(_ message: String, level: PrintLevel, fileLocation: Location?) + func message(_ message: String, level: PrintLevel, location: Location?) } extension Loggable { @@ -21,6 +21,6 @@ extension Loggable { /// Convenience overload of `message(:level:fileLocation:)` with `fileLocation` set to `nil`. public func message(_ message: String, level: PrintLevel) { - self.message(message, level: level, fileLocation: nil) + self.message(message, level: level, location: nil) } } diff --git a/Sources/Core/Logging/XcodeLogger.swift b/Sources/Core/Logging/XcodeLogger.swift index 2c76733..1d033c4 100644 --- a/Sources/Core/Logging/XcodeLogger.swift +++ b/Sources/Core/Logging/XcodeLogger.swift @@ -8,11 +8,11 @@ public final class XcodeLogger: Loggable { /// - 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 message(_ message: String, level: PrintLevel, fileLocation: Location?) { + public func message(_ message: String, level: PrintLevel, location: Location?) { var locationPrefix = "" - if let fileLocation = fileLocation { - locationPrefix = fileLocation.locationMessage(pathType: .absolute) + " " + if let location = location { + locationPrefix = location.locationMessage(pathType: .absolute) + " " } print("\(locationPrefix)\(level.rawValue): AnyLint: \(message)") diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index df0104f..6f99736 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -1,12 +1,12 @@ import Foundation /// A violation found in a check. -public struct Violation: Codable { +public struct Violation: Codable, Equatable { /// 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 fileLocation: Location? + public let location: Location? /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? @@ -14,11 +14,11 @@ public struct Violation: Codable { /// Initializes a violation object. public init( matchedString: String? = nil, - fileLocation: Location? = nil, + location: Location? = nil, appliedAutoCorrection: AutoCorrection? = nil ) { self.matchedString = matchedString - self.fileLocation = fileLocation + self.location = location self.appliedAutoCorrection = appliedAutoCorrection } } diff --git a/Sources/Reporting/Extensions/JSONDecoderExt.swift b/Sources/Reporting/Extensions/JSONDecoderExt.swift index c5dbc53..7e5bf7d 100644 --- a/Sources/Reporting/Extensions/JSONDecoderExt.swift +++ b/Sources/Reporting/Extensions/JSONDecoderExt.swift @@ -1,7 +1,7 @@ import Foundation extension JSONDecoder { - static var iso: JSONDecoder { + public static var iso: JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder diff --git a/Sources/Reporting/Extensions/JSONEncoderExt.swift b/Sources/Reporting/Extensions/JSONEncoderExt.swift index 7a48077..1e54af8 100644 --- a/Sources/Reporting/Extensions/JSONEncoderExt.swift +++ b/Sources/Reporting/Extensions/JSONEncoderExt.swift @@ -1,9 +1,10 @@ import Foundation extension JSONEncoder { - static var iso: JSONEncoder { + public static var iso: JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .prettyPrinted return encoder } } diff --git a/Sources/Reporting/LintResults.swift b/Sources/Reporting/LintResults.swift index e8fd046..bbfa606 100644 --- a/Sources/Reporting/LintResults.swift +++ b/Sources/Reporting/LintResults.swift @@ -6,11 +6,13 @@ import OrderedCollections public typealias LintResults = OrderedDictionary> extension LintResults { - var allExecutedChecks: [CheckInfo] { + /// Returns a list of all executed checks. + public var allExecutedChecks: [CheckInfo] { values.reduce(into: []) { $0.append(contentsOf: $1.keys) } } - var allFoundViolations: [Violation] { + /// Returns a list of all found violations. + public var allFoundViolations: [Violation] { values.reduce(into: []) { $0.append(contentsOf: $1.values.flatMap { $0 }) } } @@ -102,7 +104,7 @@ extension LintResults { let checkViolations = violations(check: check, excludeAutocorrected: false) if checkViolations.isFilled { - let violationsWithLocationMessage = checkViolations.filter { $0.fileLocation != nil } + let violationsWithLocationMessage = checkViolations.filter { $0.location != nil } if violationsWithLocationMessage.isFilled { log.message( @@ -115,7 +117,7 @@ extension LintResults { let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) let prefix = "> \(violationNumString). " log.message( - prefix + violation.fileLocation!.locationMessage(pathType: .relative), + prefix + violation.location!.locationMessage(pathType: .relative), level: check.severity.logLevel ) @@ -171,7 +173,7 @@ extension LintResults { log.message( "[\(checkInfo.id)] \(checkInfo.hint)", level: severity.logLevel, - fileLocation: violation.fileLocation + location: violation.location ) } } diff --git a/Sources/TestSupport/TestLogger.swift b/Sources/TestSupport/TestLogger.swift index 1d12b57..7925569 100644 --- a/Sources/TestSupport/TestLogger.swift +++ b/Sources/TestSupport/TestLogger.swift @@ -9,10 +9,10 @@ public final class TestLogger: Loggable { loggedMessages = [] } - public func message(_ message: String, level: PrintLevel, fileLocation: Location?) { - if let fileLocation = fileLocation { + public func message(_ message: String, level: PrintLevel, location: Location?) { + if let location = location { loggedMessages.append( - "[\(level.rawValue)] \(fileLocation.locationMessage(pathType: .relative)) \(message)" + "[\(level.rawValue)] \(location.locationMessage(pathType: .relative)) \(message)" ) } else { diff --git a/Tests/CheckersTests/FileContentsCheckerTests.swift b/Tests/CheckersTests/FileContentsCheckerTests.swift index 592cf9d..b2561de 100644 --- a/Tests/CheckersTests/FileContentsCheckerTests.swift +++ b/Tests/CheckersTests/FileContentsCheckerTests.swift @@ -25,14 +25,14 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 2) XCTAssertEqual(violations[0].matchedString, "let x=5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 1) - XCTAssertEqual(violations[0].fileLocation!.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[0].location?.row, 1) + XCTAssertEqual(violations[0].location!.column, 1) XCTAssertEqual(violations[1].matchedString, "var y=10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 2) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].location?.row, 2) + XCTAssertEqual(violations[1].location?.column, 1) } } @@ -61,14 +61,14 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 2) XCTAssertEqual(violations[0].matchedString, "let x=5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 4) - XCTAssertEqual(violations[0].fileLocation?.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[0].location?.row, 4) + XCTAssertEqual(violations[0].location?.column, 1) XCTAssertEqual(violations[1].matchedString, "var y=10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 5) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[1].location?.row, 5) + XCTAssertEqual(violations[1].location?.column, 1) } } @@ -97,34 +97,34 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 6) XCTAssertEqual(violations[0].matchedString, "let x=5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 4) - XCTAssertEqual(violations[0].fileLocation?.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/Hello.swift") + XCTAssertEqual(violations[0].location?.row, 4) + XCTAssertEqual(violations[0].location?.column, 1) XCTAssertEqual(violations[1].matchedString, "var y=10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 5) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/Hello.swift") + XCTAssertEqual(violations[1].location?.row, 5) + XCTAssertEqual(violations[1].location?.column, 1) XCTAssertEqual(violations[2].matchedString, "var y=10") - XCTAssertEqual(violations[2].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[2].fileLocation?.row, 5) - XCTAssertEqual(violations[2].fileLocation?.column, 1) + XCTAssertEqual(violations[2].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[2].location?.row, 5) + XCTAssertEqual(violations[2].location?.column, 1) XCTAssertEqual(violations[3].matchedString, "let x=5") - XCTAssertEqual(violations[3].fileLocation?.filePath, "\(tempDir)/Sources/Foo.swift") - XCTAssertEqual(violations[3].fileLocation?.row, 4) - XCTAssertEqual(violations[3].fileLocation?.column, 1) + XCTAssertEqual(violations[3].location?.filePath, "\(tempDir)/Sources/Foo.swift") + XCTAssertEqual(violations[3].location?.row, 4) + XCTAssertEqual(violations[3].location?.column, 1) XCTAssertEqual(violations[4].matchedString, "let x=5") - XCTAssertEqual(violations[4].fileLocation?.filePath, "\(tempDir)/Sources/Bar.swift") - XCTAssertEqual(violations[4].fileLocation?.row, 4) - XCTAssertEqual(violations[4].fileLocation?.column, 1) + XCTAssertEqual(violations[4].location?.filePath, "\(tempDir)/Sources/Bar.swift") + XCTAssertEqual(violations[4].location?.row, 4) + XCTAssertEqual(violations[4].location?.column, 1) XCTAssertEqual(violations[5].matchedString, "var y=10") - XCTAssertEqual(violations[5].fileLocation?.filePath, "\(tempDir)/Sources/Bar.swift") - XCTAssertEqual(violations[5].fileLocation?.row, 5) - XCTAssertEqual(violations[5].fileLocation?.column, 1) + XCTAssertEqual(violations[5].location?.filePath, "\(tempDir)/Sources/Bar.swift") + XCTAssertEqual(violations[5].location?.row, 5) + XCTAssertEqual(violations[5].location?.column, 1) } } @@ -149,14 +149,14 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 2) XCTAssertEqual(violations[0].matchedString, "let x =5") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 1) - XCTAssertEqual(violations[0].fileLocation?.column, 1) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[0].location?.row, 1) + XCTAssertEqual(violations[0].location?.column, 1) XCTAssertEqual(violations[1].matchedString, "var y= 10") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 2) - XCTAssertEqual(violations[1].fileLocation?.column, 1) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].location?.row, 2) + XCTAssertEqual(violations[1].location?.column, 1) } } @@ -181,45 +181,45 @@ final class FileContentsCheckerTests: XCTestCase { XCTAssertEqual(violations.count, 7) XCTAssertEqual(violations[0].matchedString, "10000") - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/Hello.swift") - XCTAssertEqual(violations[0].fileLocation?.row, 2) - XCTAssertEqual(violations[0].fileLocation?.column, 9) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/Hello.swift") + XCTAssertEqual(violations[0].location?.row, 2) + XCTAssertEqual(violations[0].location?.column, 9) XCTAssertEqual(violations[0].appliedAutoCorrection!.after, "10_000") XCTAssertEqual(violations[1].matchedString, "50000000") - XCTAssertEqual(violations[1].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[1].fileLocation?.row, 1) - XCTAssertEqual(violations[1].fileLocation?.column, 9) + XCTAssertEqual(violations[1].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[1].location?.row, 1) + XCTAssertEqual(violations[1].location?.column, 9) XCTAssertEqual(violations[1].appliedAutoCorrection!.after, "50000_000") XCTAssertEqual(violations[2].matchedString, "100000000000000") - XCTAssertEqual(violations[2].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[2].fileLocation?.row, 2) - XCTAssertEqual(violations[2].fileLocation?.column, 9) + XCTAssertEqual(violations[2].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[2].location?.row, 2) + XCTAssertEqual(violations[2].location?.column, 9) XCTAssertEqual(violations[2].appliedAutoCorrection!.after, "100000000000_000") XCTAssertEqual(violations[3].matchedString, "50000") - XCTAssertEqual(violations[3].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[3].fileLocation?.row, 1) - XCTAssertEqual(violations[3].fileLocation?.column, 9) + XCTAssertEqual(violations[3].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[3].location?.row, 1) + XCTAssertEqual(violations[3].location?.column, 9) XCTAssertEqual(violations[3].appliedAutoCorrection!.after, "50_000") XCTAssertEqual(violations[4].matchedString, "100000000000") - XCTAssertEqual(violations[4].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[4].fileLocation?.row, 2) - XCTAssertEqual(violations[4].fileLocation?.column, 9) + XCTAssertEqual(violations[4].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[4].location?.row, 2) + XCTAssertEqual(violations[4].location?.column, 9) XCTAssertEqual(violations[4].appliedAutoCorrection!.after, "100000000_000") XCTAssertEqual(violations[5].matchedString, "100000000") - XCTAssertEqual(violations[5].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[5].fileLocation?.row, 2) - XCTAssertEqual(violations[5].fileLocation?.column, 9) + XCTAssertEqual(violations[5].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[5].location?.row, 2) + XCTAssertEqual(violations[5].location?.column, 9) XCTAssertEqual(violations[5].appliedAutoCorrection!.after, "100000_000") XCTAssertEqual(violations[6].matchedString, "100000") - XCTAssertEqual(violations[6].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertEqual(violations[6].fileLocation?.row, 2) - XCTAssertEqual(violations[6].fileLocation?.column, 9) + XCTAssertEqual(violations[6].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertEqual(violations[6].location?.row, 2) + XCTAssertEqual(violations[6].location?.column, 9) XCTAssertEqual(violations[6].appliedAutoCorrection!.after, "100_000") } } diff --git a/Tests/CheckersTests/FilePathsCheckerTests.swift b/Tests/CheckersTests/FilePathsCheckerTests.swift index 447e23b..1c1bd66 100644 --- a/Tests/CheckersTests/FilePathsCheckerTests.swift +++ b/Tests/CheckersTests/FilePathsCheckerTests.swift @@ -19,7 +19,7 @@ final class FilePathsCheckerTests: XCTestCase { let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 1) - XCTAssertNil(violations[0].fileLocation) + XCTAssertNil(violations[0].location) } withTemporaryFiles( @@ -31,9 +31,9 @@ final class FilePathsCheckerTests: XCTestCase { let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 1) - XCTAssertEqual(violations[0].fileLocation?.filePath, "\(tempDir)/Sources/World.swift") - XCTAssertNil(violations[0].fileLocation?.row) - XCTAssertNil(violations[0].fileLocation?.column) + XCTAssertEqual(violations[0].location?.filePath, "\(tempDir)/Sources/World.swift") + XCTAssertNil(violations[0].location?.row) + XCTAssertNil(violations[0].location?.column) } } diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index e99f942..2b874ad 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -2,6 +2,8 @@ import XCTest import Core import TestSupport +import CustomDump +import Reporting final class LintTests: XCTestCase { var testLogger: TestLogger = .init() @@ -117,12 +119,73 @@ final class LintTests: XCTestCase { // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) } - func testRunCustomScript() { - // TODO: [cg_2021-09-05] not yet implemented + func testRunCustomScript() throws { + var lintResults: LintResults = try Lint.runCustomScript( + checkInfo: .init(id: "1", hint: "hint #1"), + command: """ + if which echo > /dev/null; then + echo 'Executed custom checks with following result: + { + "warning": { + "A@warning: hint for A": [ + {}, + { "matchedString": "A" }, + { + "matchedString": "AAA", + "location": { "filePath": "/some/path", "row": 5 }, + "appliedAutoCorrection": { "before": "AAA", "after": "A" } + } + ] + }, + "info": { + "B@info: hint for B": [] + } + } + + Total: 0 errors, 3 warnings, 0 info.' + fi + + """ + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["A", "B"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 3) + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), ["A", "AAA"]) + 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") } func testValidateParameterCombinations() { - // TODO: [cg_2021-09-05] not yet implemented + XCTAssertNoDifference(testLogger.loggedMessages, []) + + Lint.validateParameterCombinations( + checkInfo: .init(id: "1", hint: "hint #1"), + autoCorrectReplacement: nil, + autoCorrectExamples: [.init(before: "abc", after: "cba")], + violateIfNoMatchesFound: false + ) + + XCTAssertNoDifference( + testLogger.loggedMessages, + ["[warning] `autoCorrectExamples` provided for check 1 without specifying an `autoCorrectReplacement`."] + ) + + // 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"), + // autoCorrectReplacement: "$3$2$1", + // autoCorrectExamples: [.init(before: "abc", after: "cba")], + // violateIfNoMatchesFound: true + // ) + // + // + // XCTAssertEqual( + // testLogger.loggedMessages.last, + // "Incompatible options specified for check 2: `autoCorrectReplacement` and `violateIfNoMatchesFound` can't be used together." + // ) } func testCheckFileContents() { diff --git a/Tests/CoreTests/ViolationTests.swift b/Tests/CoreTests/ViolationTests.swift index ff83337..e9425a8 100644 --- a/Tests/CoreTests/ViolationTests.swift +++ b/Tests/CoreTests/ViolationTests.swift @@ -3,14 +3,14 @@ import XCTest final class ViolationTests: XCTestCase { func testLocationMessage() { - XCTAssertNil(Violation().fileLocation?.locationMessage(pathType: .relative)) + XCTAssertNil(Violation().location?.locationMessage(pathType: .relative)) - let fileViolation = Violation(fileLocation: .init(filePath: "Temp/Sources/Hello.swift")) - XCTAssertEqual(fileViolation.fileLocation?.locationMessage(pathType: .relative), "Temp/Sources/Hello.swift") + let fileViolation = Violation(location: .init(filePath: "Temp/Sources/Hello.swift")) + XCTAssertEqual(fileViolation.location?.locationMessage(pathType: .relative), "Temp/Sources/Hello.swift") - let locationInfoViolation = Violation(fileLocation: .init(filePath: "Temp/Sources/World.swift", row: 5, column: 15)) + let locationInfoViolation = Violation(location: .init(filePath: "Temp/Sources/World.swift", row: 5, column: 15)) XCTAssertEqual( - locationInfoViolation.fileLocation?.locationMessage(pathType: .relative), + locationInfoViolation.location?.locationMessage(pathType: .relative), "Temp/Sources/World.swift:5:15:" ) } diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index 7b31fa1..8e1b077 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -9,27 +9,27 @@ final class LintResultsTests: XCTestCase { [ Severity.error: [ CheckInfo(id: "1", hint: "hint #1", severity: .error): [ - Violation(matchedString: "oink1", fileLocation: .init(filePath: "/sample/path1", row: 4, column: 2)), - Violation(matchedString: "boo1", fileLocation: .init(filePath: "/sample/path2", row: 40, column: 20)), + Violation(matchedString: "oink1", location: .init(filePath: "/sample/path1", row: 4, column: 2)), + Violation(matchedString: "boo1", location: .init(filePath: "/sample/path2", row: 40, column: 20)), Violation( - fileLocation: .init(filePath: "/sample/path2"), + location: .init(filePath: "/sample/path2"), appliedAutoCorrection: .init(before: "foo", after: "bar") ), ] ], Severity.warning: [ CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ - Violation(matchedString: "oink2", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), - Violation(matchedString: "boo2", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), + 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( - fileLocation: .init(filePath: "/sample/path4"), + location: .init(filePath: "/sample/path4"), appliedAutoCorrection: .init(before: "fool", after: "barl") ), ] ], Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], ] @@ -45,7 +45,7 @@ final class LintResultsTests: XCTestCase { let allFoundViolations = sampleLintResults.allFoundViolations XCTAssertNoDifference(allFoundViolations.count, 7) XCTAssertNoDifference( - allFoundViolations.map(\.fileLocation).map(\.?.filePath).map(\.?.last), + allFoundViolations.map(\.location).map(\.?.filePath).map(\.?.last), ["1", "2", "2", "1", "3", "4", "0"] ) XCTAssertNoDifference( @@ -58,15 +58,15 @@ final class LintResultsTests: XCTestCase { let otherLintResults: LintResults = [ Severity.error: [ CheckInfo(id: "1", hint: "hint #1", severity: .warning): [ - Violation(matchedString: "muuh", fileLocation: .init(filePath: "/sample/path4", row: 6, column: 3)), + Violation(matchedString: "muuh", location: .init(filePath: "/sample/path4", row: 6, column: 3)), Violation( - fileLocation: .init(filePath: "/sample/path5"), + 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): [ - Violation(matchedString: "super", fileLocation: .init(filePath: "/sample/path1", row: 2, column: 200)) + Violation(matchedString: "super", location: .init(filePath: "/sample/path1", row: 2, column: 200)) ], ] ] @@ -81,7 +81,7 @@ final class LintResultsTests: XCTestCase { XCTAssertNoDifference(allFoundViolations.count, 10) XCTAssertNoDifference( - allFoundViolations.map(\.fileLocation).map(\.?.filePath).map(\.?.last), + allFoundViolations.map(\.location).map(\.?.filePath).map(\.?.last), ["1", "2", "2", "4", "5", "1", "1", "3", "4", "0"] ) XCTAssertNoDifference( @@ -104,10 +104,10 @@ final class LintResultsTests: XCTestCase { lintResults.appendViolations( [ - Violation(matchedString: "A", fileLocation: .init(filePath: "/sample/path5", row: 7, column: 7)), - Violation(matchedString: "B", fileLocation: .init(filePath: "/sample/path6", row: 70, column: 70)), + Violation(matchedString: "A", location: .init(filePath: "/sample/path5", row: 7, column: 7)), + Violation(matchedString: "B", location: .init(filePath: "/sample/path6", row: 70, column: 70)), Violation( - fileLocation: .init(filePath: "/sample/path7"), + location: .init(filePath: "/sample/path7"), appliedAutoCorrection: .init(before: "C", after: "D") ), ], @@ -239,17 +239,17 @@ final class LintResultsTests: XCTestCase { ], Severity.warning: [ CheckInfo(id: "2", hint: "hint #2", severity: .warning): [ - Violation(matchedString: "oink2", fileLocation: .init(filePath: "/sample/path1", row: 5, column: 6)), - Violation(matchedString: "boo2", fileLocation: .init(filePath: "/sample/path3", row: 50, column: 60)), + 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( - fileLocation: .init(filePath: "/sample/path4"), + location: .init(filePath: "/sample/path4"), appliedAutoCorrection: .init(before: "fool", after: "barl") ), ] ], Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], ] @@ -264,7 +264,7 @@ final class LintResultsTests: XCTestCase { ], Severity.info: [ CheckInfo(id: "3", hint: "hint #3", severity: .info): [ - Violation(matchedString: "blubb", fileLocation: .init(filePath: "/sample/path0", row: 10, column: 20)) + Violation(matchedString: "blubb", location: .init(filePath: "/sample/path0", row: 10, column: 20)) ] ], ] @@ -285,4 +285,51 @@ final class LintResultsTests: XCTestCase { XCTAssertEqual(LintResults().maxViolationSeverity(excludeAutocorrected: false), nil) } + + func testCodable() throws { + let expectedJsonOutput = """ + { + "warning": { + "1@error: hint for #1": [{ + + }, + { + "appliedAutoCorrection": { + "after": "A", + "before": "AAA" + }, + "matchedString": "A" + }, + { + "location": { + "row": 5, + "column": 2, + "filePath": "/some/path" + }, + "matchedString": "AAA" + } + ] + }, + "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)), + ] + ], + .info: [:], + ] + let encodedData = try JSONEncoder.iso.encode(lintResults) + XCTAssertNoDifference(String(data: encodedData, encoding: .utf8), expectedJsonOutput) + + let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) + XCTAssertNoDifference(decodedLintResults, lintResults) + } } From 8b4416d42468863a50e0626011ae62e9219906f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 7 Sep 2021 16:22:05 +0200 Subject: [PATCH 25/37] Fix LintResults coding issues + rename CheckInfo --- CHANGELOG.md | 4 +- README.md | 28 ++-- Sources/Checkers/FileContentsChecker.swift | 4 +- Sources/Checkers/Lint.swift | 62 ++++----- Sources/Commands/LintCommand.swift | 12 +- Sources/Core/{CheckInfo.swift => Check.swift} | 16 ++- Sources/Core/Violation.swift | 11 ++ Sources/Reporting/CodableLintResults.swift | 28 ++++ .../Reporting/CodableOrderedDictionary.swift | 45 +++++++ Sources/Reporting/LintResults.swift | 116 +++++++++++++---- Sources/TestSupport/Extensions/DateExt.swift | 8 ++ Tests/CheckersTests/LintTests.swift | 30 ++--- Tests/CoreTests/CheckInfoTests.swift | 24 ++-- Tests/ReportingTests/LintResultsTests.swift | 121 +++++++++++------- lint.swift | 34 ++--- 15 files changed, 373 insertions(+), 170 deletions(-) rename Sources/Core/{CheckInfo.swift => Check.swift} (88%) create mode 100644 Sources/Reporting/CodableLintResults.swift create mode 100644 Sources/Reporting/CodableOrderedDictionary.swift create mode 100644 Sources/TestSupport/Extensions/DateExt.swift 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"], From 791b96310d365bdde562f887693cd21b40d22cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 7 Sep 2021 16:49:53 +0200 Subject: [PATCH 26/37] Fully test & fix implementation of custom scripts --- Sources/Checkers/Lint.swift | 50 ++++++++++++------ Sources/Core/Violation.swift | 7 ++- Tests/CheckersTests/LintTests.swift | 78 +++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 25 deletions(-) diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index d26739c..6284be7 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -131,26 +131,46 @@ 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. + /// - 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(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]) - if let jsonString = output.lintResultsJsonString, - let jsonData = jsonString.data(using: .utf8), - let lintResults: LintResults = try? JSONDecoder.iso.decode(LintResults.self, from: jsonData) - { - return lintResults - } - else if let jsonString = output.violationsArrayJsonString, - let jsonData = jsonString.data(using: .utf8), - let violations: [Violation] = try? JSONDecoder.iso.decode([Violation].self, from: jsonData) - { - return [check.severity: [check: violations]] + do { + let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) + try FileManager.default.removeItem(at: tempScriptFileUrl) + + if let jsonString = output.lintResultsJsonString, + let jsonData = jsonString.data(using: .utf8), + let lintResults: LintResults = try? JSONDecoder.iso.decode(LintResults.self, from: jsonData) + { + return lintResults + } + else if let jsonString = output.violationsArrayJsonString, + let jsonData = jsonString.data(using: .utf8), + let violations: [Violation] = try? JSONDecoder.iso.decode([Violation].self, from: jsonData) + { + return [check.severity: [check: violations]] + } + else { + // if the command fails, a ShellOutError will be thrown – here, none is thrown, so no violations + return [check.severity: [check: []]] + } } - else { - return [check.severity: [check: [Violation()]]] + catch { + if let shellOutError = error as? ShellOutError, shellOutError.terminationStatus != 0 { + return [ + check.severity: [ + check: [ + Violation(message: shellOutError.output.components(separatedBy: .newlines).last) + ] + ] + ] + } + + throw error } } diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index a932c99..b585a59 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -14,17 +14,22 @@ public struct Violation: Codable, Equatable { /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? + /// A custom violation message. + public let message: String? + /// Initializes a violation object. public init( discoverDate: Date = Date(), matchedString: String? = nil, location: Location? = nil, - appliedAutoCorrection: AutoCorrection? = nil + appliedAutoCorrection: AutoCorrection? = nil, + message: String? = nil ) { self.discoverDate = discoverDate self.matchedString = matchedString self.location = location self.appliedAutoCorrection = appliedAutoCorrection + self.message = message } } diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index d4e043a..c50c720 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -122,17 +122,18 @@ final class LintTests: XCTestCase { func testRunCustomScript() throws { var lintResults: LintResults = try Lint.runCustomScript( check: .init(id: "1", hint: "hint #1"), - command: """ + command: #""" if which echo > /dev/null; then echo 'Executed custom checks with following result: { "warning": { "A@warning: hint for A": [ - {}, - { "matchedString": "A" }, + { "discoverDate": "2001-01-01T00:00:00Z" }, + { "discoverDate" : "2001-01-01T01:00:00Z", "matchedString": "A" }, { + "discoverDate" : "2001-01-01T02:00:00Z", "matchedString": "AAA", - "location": { "filePath": "/some/path", "row": 5 }, + "location": { "filePath": "\/some\/path", "row": 5 }, "appliedAutoCorrection": { "before": "AAA", "after": "A" } } ] @@ -145,17 +146,76 @@ final class LintTests: XCTestCase { Total: 0 errors, 3 warnings, 0 info.' fi - """ + """# ) XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["A", "B"]) XCTAssertEqual(lintResults.allFoundViolations.count, 3) - XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), ["A", "AAA"]) - 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") + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), [nil, "A", "AAA"]) + XCTAssertEqual(lintResults.allFoundViolations[2].location?.filePath, "/some/path") + XCTAssertEqual(lintResults.allFoundViolations[2].location?.row, 5) + XCTAssertEqual(lintResults.allFoundViolations[2].appliedAutoCorrection?.after, "A") XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "B") + + lintResults = try Lint.runCustomScript( + check: .init(id: "1", hint: "hint #1", severity: .info), + command: #""" + if which echo > /dev/null; then + echo 'Executed custom check with following violations: + [ + { "discoverDate": "2001-01-01T00:00:00Z" }, + { "discoverDate" : "2001-01-01T01:00:00Z", "matchedString": "A" }, + { + "discoverDate" : "2001-01-01T02:00:00Z", + "matchedString": "AAA", + "location": { "filePath": "\/some\/path", "row": 5 }, + "appliedAutoCorrection": { "before": "AAA", "after": "A" } + } + ] + + Total: 0 errors, 3 warnings, 0 info.' + fi + + """# + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 3) + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), [nil, "A", "AAA"]) + XCTAssertEqual(lintResults.allFoundViolations[2].location?.filePath, "/some/path") + XCTAssertEqual(lintResults.allFoundViolations[2].location?.row, 5) + XCTAssertEqual(lintResults.allFoundViolations[2].appliedAutoCorrection?.after, "A") + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "1") + + lintResults = try Lint.runCustomScript( + check: .init(id: "1", hint: "hint #1", severity: .info), + command: + "echo 'Executed custom check with 100 files.\nCustom check failed, please check file at path /some/path.' && exit 1" + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 1) + XCTAssertNoDifference( + lintResults.allFoundViolations.map(\.message), + ["Custom check failed, please check file at path /some/path."] + ) + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "1") + + lintResults = try Lint.runCustomScript( + check: .init(id: "1", hint: "hint #1", severity: .info), + command: #""" + echo 'Executed custom check with 100 files.\nNo issues found.' && exit 0 + """# + ) + + XCTAssertNoDifference(lintResults.allExecutedChecks.map(\.id), ["1"]) + XCTAssertEqual(lintResults.allFoundViolations.count, 0) + XCTAssertNoDifference(lintResults.allFoundViolations.map(\.matchedString), []) + XCTAssertNil(lintResults.checkViolationsBySeverity[.error]?.keys.first) + XCTAssertEqual(lintResults.checkViolationsBySeverity[.info]?.keys.first?.id, "1") } func testValidateParameterCombinations() { From f778aedd0b702c44522cba74b774ef0952177442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 11 Sep 2021 21:46:41 +0200 Subject: [PATCH 27/37] Cover missing tests & cleanup unused files --- Sources/Checkers/CustomScriptsChecker.swift | 23 ----- Sources/Checkers/Lint.swift | 11 ++- Sources/Reporting/CodableLintResults.swift | 28 ------ .../Reporting/CodableOrderedDictionary.swift | 45 --------- .../Extensions/RegexExtTests.swift | 14 +++ Tests/CheckersTests/LintTests.swift | 97 ++++++++++++++++--- 6 files changed, 105 insertions(+), 113 deletions(-) delete mode 100644 Sources/Checkers/CustomScriptsChecker.swift delete mode 100644 Sources/Reporting/CodableLintResults.swift delete mode 100644 Sources/Reporting/CodableOrderedDictionary.swift diff --git a/Sources/Checkers/CustomScriptsChecker.swift b/Sources/Checkers/CustomScriptsChecker.swift deleted file mode 100644 index 80d8418..0000000 --- a/Sources/Checkers/CustomScriptsChecker.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import Core - -/// The checker for the `CustomScripts` configuration. Runs custom commands and checks their output & status for determining violations. -public struct CustomScriptsChecker { - /// 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 severity level for the report in case the check fails. - public let severity: Severity - - /// The script to execute. Expected output can be AnyLint standardized JSON or, if not, will use exit code to determine failed or not. - public let script: String -} - -extension CustomScriptsChecker: Checker { - public func performCheck() throws -> [Violation] { - fatalError() // TODO: [cg_2021-07-02] not yet implemented - } -} diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index 6284be7..ce9644a 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -40,7 +40,7 @@ public enum Lint { ) if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( + validateAutocorrectsAllExamples( check: check, examples: autoCorrectExamples, regex: regex, @@ -101,7 +101,7 @@ public enum Lint { ) if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll( + validateAutocorrectsAllExamples( check: check, examples: autoCorrectExamples, regex: regex, @@ -140,6 +140,8 @@ public enum Lint { do { let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) + + // clean up temporary script file after successful execution try FileManager.default.removeItem(at: tempScriptFileUrl) if let jsonString = output.lintResultsJsonString, @@ -160,6 +162,9 @@ public enum Lint { } } catch { + // clean up temporary script file after failed execution + try? FileManager.default.removeItem(at: tempScriptFileUrl) + if let shellOutError = error as? ShellOutError, shellOutError.terminationStatus != 0 { return [ check.severity: [ @@ -198,7 +203,7 @@ public enum Lint { } } - static func validateAutocorrectsAll( + static func validateAutocorrectsAllExamples( check: Check, examples: [AutoCorrection], regex: Regex, diff --git a/Sources/Reporting/CodableLintResults.swift b/Sources/Reporting/CodableLintResults.swift deleted file mode 100644 index 2b37851..0000000 --- a/Sources/Reporting/CodableLintResults.swift +++ /dev/null @@ -1,28 +0,0 @@ -//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 deleted file mode 100644 index 87f130a..0000000 --- a/Sources/Reporting/CodableOrderedDictionary.swift +++ /dev/null @@ -1,45 +0,0 @@ -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/Tests/CheckersTests/Extensions/RegexExtTests.swift b/Tests/CheckersTests/Extensions/RegexExtTests.swift index 36a1c0d..6dfaa71 100644 --- a/Tests/CheckersTests/Extensions/RegexExtTests.swift +++ b/Tests/CheckersTests/Extensions/RegexExtTests.swift @@ -11,4 +11,18 @@ final class RegexExtTests: XCTestCase { "\n- Sample Text. \n" ) } + + func testReplaceAllCaptures() throws { + let anonymousRefsRegex = try Regex(#"(\w+)\.(\w+)\.(\w+)"#) + XCTAssertEqual( + anonymousRefsRegex.replaceAllCaptures(in: "prefix.content.suffix", with: "$3-$2-$1"), + "suffix-content-prefix" + ) + + let namedRefsRegex = try Regex(#"(?\w+)\.(?\w+)\.(?\w+)"#) + XCTAssertEqual( + namedRefsRegex.replaceAllCaptures(in: "prefix.content.suffix", with: "$suffix-$content-$prefix"), + "suffix-content-prefix" + ) + } } diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index c50c720..a0141fb 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -60,29 +60,29 @@ final class LintTests: XCTestCase { func testValidateAutocorrectsAllExamplesWithAnonymousGroups() { XCTAssertNil(testLogger.exitStatusCode) - let anonymousCaptureRegex = try? Regex(#"([^\.]+)(\.)([^\.]+)(\.)([^\.]+)"#) + let anonymousCaptureRegex = try? Regex(#"([^\.]+)\.([^\.]+)\.([^\.]+)"#) - Lint.validateAutocorrectsAll( + Lint.validateAutocorrectsAllExamples( 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"), ], regex: anonymousCaptureRegex!, - autocorrectReplacement: "$5$2$3$4$1" + autocorrectReplacement: "$3.$2.$1" ) XCTAssertNil(testLogger.exitStatusCode) // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` - // Lint.validateAutocorrectsAll( + // Lint.validateAutocorrectsAllExamples( // 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"), // ], // regex: anonymousCaptureRegex!, - // autocorrectReplacement: "$4$1$2$3$0" + // autocorrectReplacement: "$2.$1.$0" // ) // // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) @@ -91,29 +91,29 @@ final class LintTests: XCTestCase { func testValidateAutocorrectsAllExamplesWithNamedGroups() { XCTAssertNil(testLogger.exitStatusCode) - let namedCaptureRegex = try! Regex(#"([^\.]+)\.([^\.]+)\.([^\.]+)"#) + let namedCaptureRegex = try! Regex(#"(?[^\.]+)\.(?[^\.]+)\.(?[^\.]+)"#) - Lint.validateAutocorrectsAll( + Lint.validateAutocorrectsAllExamples( 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"), ], regex: namedCaptureRegex, - autocorrectReplacement: "$3.$2.$1" + autocorrectReplacement: "$suffix.$content.$prefix" ) XCTAssertNil(testLogger.exitStatusCode) // TODO: [cg_2021-09-05] Swift / XCTest doesn't have a way to test for functions returning `Never` - // Lint.validateAutocorrectsAll( + // Lint.validateAutocorrectsAllExamples( // 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"), // ], // regex: namedCaptureRegex, - // autocorrectReplacement: "$sfx$sep1$cnt$sep2$pref" + // autocorrectReplacement: "$sfx.$cnt.$pref" // ) // // XCTAssertEqual(testLogger.exitStatusCode, EXIT_FAILURE) @@ -248,11 +248,80 @@ final class LintTests: XCTestCase { // ) } - func testCheckFileContents() { - // TODO: [cg_2021-09-05] not yet implemented + func testCheckFileContents() throws { + let temporaryFiles: [TemporaryFile] = [ + ( + subpath: "Sources/Hello.swift", + contents: """ + let x = 5 + var y = 10 + """ + ), + ( + subpath: "Sources/World.swift", + contents: """ + let x=5 + var y=10 + """ + ), + ] + + withTemporaryFiles(temporaryFiles) { filePathsToCheck in + var fileContents: [String] = try filePathsToCheck.map { try String(contentsOfFile: $0) } + + XCTAssertNoDifference(temporaryFiles[0].contents, fileContents[0]) + XCTAssertNoDifference(temporaryFiles[1].contents, fileContents[1]) + + let violations = try Lint.checkFileContents( + check: .init(id: "1", hint: "hint #1"), + regex: .init(#"(let|var) (\w+)=(\w+)"#), + matchingExamples: ["let x=4"], + autoCorrectReplacement: "$1 $2 = $3", + autoCorrectExamples: [.init(before: "let x=4", after: "let x = 4")] + ) + + fileContents = try filePathsToCheck.map { try String(contentsOfFile: $0) } + + XCTAssertEqual(violations.count, 2) + XCTAssertNoDifference(violations.map(\.matchedString), ["let x=5", "var y=10"]) + XCTAssertNoDifference( + violations.map(\.location)[0], + Location(filePath: "\(tempDir)/Sources/World.swift", row: 1, column: 1) + ) + XCTAssertNoDifference( + violations.map(\.location)[1], + Location(filePath: "\(tempDir)/Sources/World.swift", row: 2, column: 1) + ) + XCTAssertNoDifference(fileContents[0], temporaryFiles[0].contents) + XCTAssertNoDifference( + fileContents[1], + """ + let x = 5 + var y = 10 + """ + ) + } } - func testCheckFilePaths() { - // TODO: [cg_2021-09-05] not yet implemented + func testCheckFilePaths() throws { + let violations = try Lint.checkFilePaths( + check: .init(id: "2", hint: "hint for #2", severity: .warning), + regex: .init(#"README\.md"#), + violateIfNoMatchesFound: true + ) + + XCTAssertEqual(violations.count, 1) + XCTAssertNoDifference( + violations.map(\.matchedString), + [nil] + ) + XCTAssertNoDifference( + violations.map(\.location), + [nil] + ) + XCTAssertNoDifference( + violations.map(\.appliedAutoCorrection), + [nil] + ) } } From 19779051c2bab2536d2f497b80b453ac1d795efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 11 Sep 2021 22:12:26 +0200 Subject: [PATCH 28/37] Fix tests when not running from within Xcode --- Package.swift | 1 + Sources/Core/AutoCorrection.swift | 26 ++++++++++++++++++++++---- Sources/TestSupport/TestLogger.swift | 5 +++-- Tests/CheckersTests/LintTests.swift | 3 ++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Package.swift b/Package.swift index 010ef1d..996d7b5 100644 --- a/Package.swift +++ b/Package.swift @@ -80,6 +80,7 @@ let package = Package( dependencies: [ "Core", .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "Rainbow", package: "Rainbow"), ] ), .testTarget(name: "CoreTests", dependencies: ["Core", "TestSupport"]), diff --git a/Sources/Core/AutoCorrection.swift b/Sources/Core/AutoCorrection.swift index 426b04b..70831ba 100644 --- a/Sources/Core/AutoCorrection.swift +++ b/Sources/Core/AutoCorrection.swift @@ -25,10 +25,10 @@ public struct AutoCorrection: Codable, Equatable { for difference in afterLines.difference(from: beforeLines).sorted() { switch difference { case let .insert(offset, element, _): - lines.append("+ [L\(offset + 1)] \(element)".green) + lines.append("+ [L\(offset + 1)] \(element)".coloredAsAdded) case let .remove(offset, element, _): - lines.append("- [L\(offset + 1)] \(element)".red) + lines.append("- [L\(offset + 1)] \(element)".coloredAsRemoved) } } @@ -37,8 +37,8 @@ public struct AutoCorrection: Codable, Equatable { else { return [ "Autocorrection applied, the diff is: (+ added, - removed)", - "- \(before.showWhitespacesAndNewlines())".red, - "+ \(after.showWhitespacesAndNewlines())".green, + "- \(before.showWhitespacesAndNewlines())".coloredAsRemoved, + "+ \(after.showWhitespacesAndNewlines())".coloredAsAdded, ] } } @@ -86,3 +86,21 @@ extension CollectionDifference.Change: Comparable where ChangeElement == String } } } + +fileprivate extension String { + var coloredAsAdded: String { + #if DEBUG + return self // do not color when running tests + #else + return green + #endif + } + + var coloredAsRemoved: String { + #if DEBUG + return self // do not color when running tests + #else + return red + #endif + } +} diff --git a/Sources/TestSupport/TestLogger.swift b/Sources/TestSupport/TestLogger.swift index 7925569..92c92e6 100644 --- a/Sources/TestSupport/TestLogger.swift +++ b/Sources/TestSupport/TestLogger.swift @@ -1,5 +1,6 @@ import Foundation import Core +import Rainbow public final class TestLogger: Loggable { public var loggedMessages: [String] @@ -12,12 +13,12 @@ public final class TestLogger: Loggable { public func message(_ message: String, level: PrintLevel, location: Location?) { if let location = location { loggedMessages.append( - "[\(level.rawValue)] \(location.locationMessage(pathType: .relative)) \(message)" + "[\(level.rawValue)] \(location.locationMessage(pathType: .relative)) \(message.clearColor.clearStyles)" ) } else { loggedMessages.append( - "[\(level.rawValue)] \(message)" + "[\(level.rawValue)] \(message.clearColor.clearStyles)" ) } } diff --git a/Tests/CheckersTests/LintTests.swift b/Tests/CheckersTests/LintTests.swift index a0141fb..a49b228 100644 --- a/Tests/CheckersTests/LintTests.swift +++ b/Tests/CheckersTests/LintTests.swift @@ -276,6 +276,7 @@ final class LintTests: XCTestCase { check: .init(id: "1", hint: "hint #1"), regex: .init(#"(let|var) (\w+)=(\w+)"#), matchingExamples: ["let x=4"], + includeFilters: [try! Regex(#"\#(tempDir)/*"#)], autoCorrectReplacement: "$1 $2 = $3", autoCorrectExamples: [.init(before: "let x=4", after: "let x = 4")] ) @@ -306,7 +307,7 @@ final class LintTests: XCTestCase { func testCheckFilePaths() throws { let violations = try Lint.checkFilePaths( check: .init(id: "2", hint: "hint for #2", severity: .warning), - regex: .init(#"README\.md"#), + regex: .init(#"README\.markdown"#), violateIfNoMatchesFound: true ) From a865f9baa5694f817260e6bfc878c5e8c5bb47bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 11 Sep 2021 22:25:34 +0200 Subject: [PATCH 29/37] Fix test issue with unordered dictionary on Linux --- Tests/ReportingTests/LintResultsTests.swift | 40 ++++----------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index f1e1737..ae91449 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -309,38 +309,6 @@ final class LintResultsTests: XCTestCase { } func testCodable() throws { - let expectedJsonOutput = #""" - { - "warning" : { - "1@error: hint for #1" : [ - { - "discoverDate" : "2001-01-01T00:00:00Z" - }, - { - "discoverDate" : "2001-01-01T01:00:00Z", - "appliedAutoCorrection" : { - "after" : "A", - "before" : "AAA" - }, - "matchedString" : "A" - }, - { - "discoverDate" : "2001-01-01T02:00:00Z", - "location" : { - "row" : 5, - "column" : 2, - "filePath" : "\/some\/path" - }, - "matchedString" : "AAA" - } - ] - }, - "info" : { - - } - } - """# - let lintResults: LintResults = [ .warning: [ .init(id: "1", hint: "hint for #1"): [ @@ -360,7 +328,13 @@ final class LintResultsTests: XCTestCase { .info: [:], ] let encodedData = try JSONEncoder.iso.encode(lintResults) - XCTAssertNoDifference(String(data: encodedData, encoding: .utf8), expectedJsonOutput) + let encodedString = String(data: encodedData, encoding: .utf8)! + XCTAssert(encodedString.contains(#""warning" : {"#)) + XCTAssert(encodedString.contains(#""1@error: hint for #1" : ["#)) + XCTAssert(encodedString.contains(#""discoverDate" : "2001-01-01T01:00:00Z","#)) + XCTAssert(encodedString.contains(#""matchedString" : "A""#)) + XCTAssert(encodedString.contains(#""filePath" : "\/some\/path""#)) + XCTAssert(encodedString.contains(#""discoverDate" : "2001-01-01T02:00:00Z","#)) let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) XCTAssertNoDifference(decodedLintResults, lintResults) From 81e54ead43ee56f172edf9af9d79c785e52886a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 11 Sep 2021 22:39:34 +0200 Subject: [PATCH 30/37] Add CI tests targeting Windows platform --- .github/workflows/pull-request.yml | 36 +++++++++++++++++++++ Tests/ReportingTests/LintResultsTests.swift | 1 - 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 19021c0..c95a492 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -43,3 +43,39 @@ jobs: - name: Run tests run: swift test -v --enable-code-coverage + + # originally from: https://github.com/compnerd/swift-build/blob/master/.github/workflows/swift-argument-parser.yml + test-windows: + runs-on: windows-latest + + strategy: + matrix: + include: + - tag: 5.4.3-RELEASE + branch: swift-5.4.3-release + + steps: + - uses: actions/checkout@v2 + + - name: Install Swift ${{ matrix.tag }} + run: | + function Update-EnvironmentVariables { + foreach ($level in "Machine", "User") { + [Environment]::GetEnvironmentVariables($level).GetEnumerator() | % { + # For Path variables, append the new values, if they're not already in there + if ($_.Name -Match 'Path$') { + $_.Value = ($((Get-Content "Env:$($_.Name)") + ";$($_.Value)") -Split ';' | Select -Unique) -Join ';' + } + $_ + } | Set-Content -Path { "Env:$($_.Name)" } + } + } + + Install-Binary -Url "https://swift.org/builds/${{ matrix.branch }}/windows10/swift-${{ matrix.tag }}/swift-${{ matrix.tag }}-windows10.exe" -Name "installer.exe" -ArgumentList ("-q") + Update-EnvironmentVariables + # Reset Path and environment + echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 + Get-ChildItem Env: | % { echo "$($_.Name)=$($_.Value)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append } + + - name: Run tests + run: swift test -v --enable-test-discovery diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index ae91449..c30eefa 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -334,7 +334,6 @@ final class LintResultsTests: XCTestCase { XCTAssert(encodedString.contains(#""discoverDate" : "2001-01-01T01:00:00Z","#)) XCTAssert(encodedString.contains(#""matchedString" : "A""#)) XCTAssert(encodedString.contains(#""filePath" : "\/some\/path""#)) - XCTAssert(encodedString.contains(#""discoverDate" : "2001-01-01T02:00:00Z","#)) let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) XCTAssertNoDifference(decodedLintResults, lintResults) From 9b693e61a3f740b0ae5c1cfd15fddd037a421bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 11 Sep 2021 22:47:13 +0200 Subject: [PATCH 31/37] Remove unnecessary argument + add to main tests --- .github/workflows/main.yml | 38 +++++++++++++++++++++++++++++- .github/workflows/pull-request.yml | 4 ++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b1f7f8..7f451e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - name: Run tests - run: swift test -v --enable-test-discovery + run: swift test -v test-macos: runs-on: macos-latest @@ -22,3 +22,39 @@ jobs: - name: Run tests run: swift test -v --enable-code-coverage + + # originally from: https://github.com/compnerd/swift-build/blob/master/.github/workflows/swift-argument-parser.yml + test-windows: + runs-on: windows-latest + + strategy: + matrix: + include: + - tag: 5.4.3-RELEASE + branch: swift-5.4.3-release + + steps: + - uses: actions/checkout@v2 + + - name: Install Swift ${{ matrix.tag }} + run: | + function Update-EnvironmentVariables { + foreach ($level in "Machine", "User") { + [Environment]::GetEnvironmentVariables($level).GetEnumerator() | % { + # For Path variables, append the new values, if they're not already in there + if ($_.Name -Match 'Path$') { + $_.Value = ($((Get-Content "Env:$($_.Name)") + ";$($_.Value)") -Split ';' | Select -Unique) -Join ';' + } + $_ + } | Set-Content -Path { "Env:$($_.Name)" } + } + } + + Install-Binary -Url "https://swift.org/builds/${{ matrix.branch }}/windows10/swift-${{ matrix.tag }}/swift-${{ matrix.tag }}-windows10.exe" -Name "installer.exe" -ArgumentList ("-q") + Update-EnvironmentVariables + # Reset Path and environment + echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 + Get-ChildItem Env: | % { echo "$($_.Name)=$($_.Value)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append } + + - name: Run tests + run: swift test -v diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c95a492..be4f47f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v2 - name: Run tests - run: swift test -v --enable-test-discovery + run: swift test -v test-macos: runs-on: macos-latest @@ -78,4 +78,4 @@ jobs: Get-ChildItem Env: | % { echo "$($_.Name)=$($_.Value)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append } - name: Run tests - run: swift test -v --enable-test-discovery + run: swift test -v From c566b0b0d4f4931fe046dfa0473bc6cc439c8ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 12 Sep 2021 17:41:59 +0200 Subject: [PATCH 32/37] Add CI limit on Windows + start improving CLI --- .github/workflows/main.yml | 2 + .github/workflows/pull-request.yml | 2 + README.md | 85 ++++++++------------- Sources/Commands/InitCommand.swift | 4 - Sources/Commands/LintCommand.swift | 2 +- Tests/ReportingTests/LintResultsTests.swift | 6 +- 6 files changed, 38 insertions(+), 63 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f451e4..a691807 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,8 @@ jobs: test-windows: runs-on: windows-latest + timeout-minutes: 15 + strategy: matrix: include: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index be4f47f..7de1829 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -48,6 +48,8 @@ jobs: test-windows: runs-on: windows-latest + timeout-minutes: 15 + strategy: matrix: include: diff --git a/README.md b/README.md index ebc1402..e234a31 100644 --- a/README.md +++ b/README.md @@ -76,63 +76,42 @@ mint install Flinesoft/AnyLint To initialize AnyLint in a project, run: ```bash -anylint --init blank +anylint init ``` This will create the Swift script file `anylint.yml` with something like the following contents: ```yaml -CheckFileContents: - - id: Readme - hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' - regex: '^README\.md$' - violateIfNoMatchesFound: true - matchingExamples: ['README.md'] - nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] - - - id: ReadmeTopLevelTitle - hint: 'The README.md file should only contain a single top level title.' - regex: '(^|\n)#[^#](.*\n)*\n#[^#]' - includeFilter: ['^README\.md$'] - matchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum - - # Other Title - ## Other Subtitle - nonMatchingExamples: - - | - # Title - ## Subtitle - Lorem ipsum #1 and # 2. - - ## Other Subtitle - ### Other Subsubtitle - - - id: ReadmeTypoLicense - hint: 'ReadmeTypoLicense: Misspelled word `license`.' - regex: '([\s#]L|l)isence([\s\.,:;])' - matchingExamples: [' lisence:', '## Lisence\n'] - nonMatchingExamples: [' license:', '## License\n'] - includeFilters: ['^README\.md$'] - autoCorrectReplacement: '$1icense$2' - autoCorrectExamples: - - { before: ' lisence:', after: ' license:' } - - { before: '## Lisence\n', after: '## License\n' } - -CheckFilePaths: - - id: 'ReadmePath' - hint: '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' } +FileContents: [] +# - id: Readme +# hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' +# regex: '^README\.md$' +# violateIfNoMatchesFound: true +# matchingExamples: ['README.md'] +# nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + +FilePaths: [] +# - id: 'ReadmePath' +# hint: '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' } + +CustomScripts: [] +# - id: LintConfig +# hint: 'Lint the AnyLint config file to conform to YAML best practices.' +# command: |- +# if which yamllint > /dev/null; then +# yamllint anylint.yml +# else +# echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' +# fi + ``` Having this configuration file, you can now run `anylint` to run your lint checks. By default, if any check fails, the entire command fails and reports the violation reason. To learn more about how to configure your own checks, see the [Configuration](#configuration) section below. @@ -141,7 +120,7 @@ If you want to create and run multiple configuration files or if you want a diff Initializes the configuration files at the given locations: ```bash -anylint --init blank --path Sources/anylint.yml --path Tests/anylint.yml +anylint --init template=OpenSource --path Sources/anylint.yml --path Tests/anylint.yml ``` Runs the lint checks for both configuration files: diff --git a/Sources/Commands/InitCommand.swift b/Sources/Commands/InitCommand.swift index a64a0a7..5c2ce09 100644 --- a/Sources/Commands/InitCommand.swift +++ b/Sources/Commands/InitCommand.swift @@ -46,11 +46,7 @@ struct InitCommand: ParsableCommand { attributes: nil ) - log.message("Making config file executable ...", level: .info) - try shellOut(to: "chmod", arguments: ["+x", path]) - log.message("Successfully created config file at \(path)", level: .success) - } } diff --git a/Sources/Commands/LintCommand.swift b/Sources/Commands/LintCommand.swift index 81bad83..0dd01a2 100644 --- a/Sources/Commands/LintCommand.swift +++ b/Sources/Commands/LintCommand.swift @@ -54,7 +54,7 @@ struct LintCommand: ParsableCommand { guard FileManager.default.fileExists(atPath: config) else { log.message( - "No configuration file found at \(config) – consider running `anylint --init` with a template.", + "No configuration file found at \(config) – consider running `anylint init` with a template.", level: .error ) log.exit(fail: true) diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index c30eefa..e9ccebd 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -329,11 +329,7 @@ final class LintResultsTests: XCTestCase { ] let encodedData = try JSONEncoder.iso.encode(lintResults) let encodedString = String(data: encodedData, encoding: .utf8)! - XCTAssert(encodedString.contains(#""warning" : {"#)) - XCTAssert(encodedString.contains(#""1@error: hint for #1" : ["#)) - XCTAssert(encodedString.contains(#""discoverDate" : "2001-01-01T01:00:00Z","#)) - XCTAssert(encodedString.contains(#""matchedString" : "A""#)) - XCTAssert(encodedString.contains(#""filePath" : "\/some\/path""#)) + XCTAssert(encodedString.count > 500) let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) XCTAssertNoDifference(decodedLintResults, lintResults) From d343473e09207752a16e08a7f5159db7ffbc1946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 12 Sep 2021 18:42:48 +0200 Subject: [PATCH 33/37] Upgrade Windows Swift tooling to 5.5 dev snapshot --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7de1829..f0bd240 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -53,8 +53,8 @@ jobs: strategy: matrix: include: - - tag: 5.4.3-RELEASE - branch: swift-5.4.3-release + - tag: 5.5-DEVELOPMENT-SNAPSHOT-2021-08-30-a + branch: swift-5.5-branch steps: - uses: actions/checkout@v2 From 584f6036ae4b4286ce9f28b6042468995a3d7247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 12 Sep 2021 19:30:56 +0200 Subject: [PATCH 34/37] Fix issues with templates & simplify Windows CI --- .github/workflows/main.yml | 33 +------- .github/workflows/pull-request.yml | 33 +------- README.md | 14 ++-- Sources/Configuration/Templates/Blank.yml | 83 +++++++++++++------ .../Configuration/Templates/OpenSource.yml | 22 ++--- 5 files changed, 86 insertions(+), 99 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a691807..0b22d6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,40 +23,15 @@ jobs: - name: Run tests run: swift test -v --enable-code-coverage - # originally from: https://github.com/compnerd/swift-build/blob/master/.github/workflows/swift-argument-parser.yml test-windows: runs-on: windows-latest timeout-minutes: 15 - strategy: - matrix: - include: - - tag: 5.4.3-RELEASE - branch: swift-5.4.3-release - steps: - uses: actions/checkout@v2 - - name: Install Swift ${{ matrix.tag }} - run: | - function Update-EnvironmentVariables { - foreach ($level in "Machine", "User") { - [Environment]::GetEnvironmentVariables($level).GetEnumerator() | % { - # For Path variables, append the new values, if they're not already in there - if ($_.Name -Match 'Path$') { - $_.Value = ($((Get-Content "Env:$($_.Name)") + ";$($_.Value)") -Split ';' | Select -Unique) -Join ';' - } - $_ - } | Set-Content -Path { "Env:$($_.Name)" } - } - } - - Install-Binary -Url "https://swift.org/builds/${{ matrix.branch }}/windows10/swift-${{ matrix.tag }}/swift-${{ matrix.tag }}-windows10.exe" -Name "installer.exe" -ArgumentList ("-q") - Update-EnvironmentVariables - # Reset Path and environment - echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 - Get-ChildItem Env: | % { echo "$($_.Name)=$($_.Value)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append } - - - name: Run tests - run: swift test -v + - name: Install Swift & Run tests + uses: MaxDesiatov/swift-windows-action@v1 + with: + shell-action: swift test -v diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f0bd240..c8a4491 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -44,40 +44,15 @@ jobs: - name: Run tests run: swift test -v --enable-code-coverage - # originally from: https://github.com/compnerd/swift-build/blob/master/.github/workflows/swift-argument-parser.yml test-windows: runs-on: windows-latest timeout-minutes: 15 - strategy: - matrix: - include: - - tag: 5.5-DEVELOPMENT-SNAPSHOT-2021-08-30-a - branch: swift-5.5-branch - steps: - uses: actions/checkout@v2 - - name: Install Swift ${{ matrix.tag }} - run: | - function Update-EnvironmentVariables { - foreach ($level in "Machine", "User") { - [Environment]::GetEnvironmentVariables($level).GetEnumerator() | % { - # For Path variables, append the new values, if they're not already in there - if ($_.Name -Match 'Path$') { - $_.Value = ($((Get-Content "Env:$($_.Name)") + ";$($_.Value)") -Split ';' | Select -Unique) -Join ';' - } - $_ - } | Set-Content -Path { "Env:$($_.Name)" } - } - } - - Install-Binary -Url "https://swift.org/builds/${{ matrix.branch }}/windows10/swift-${{ matrix.tag }}/swift-${{ matrix.tag }}-windows10.exe" -Name "installer.exe" -ArgumentList ("-q") - Update-EnvironmentVariables - # Reset Path and environment - echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 - Get-ChildItem Env: | % { echo "$($_.Name)=$($_.Value)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append } - - - name: Run tests - run: swift test -v + - name: Install Swift & Run tests + uses: MaxDesiatov/swift-windows-action@v1 + with: + shell-action: swift test -v diff --git a/README.md b/README.md index e234a31..0b5b714 100644 --- a/README.md +++ b/README.md @@ -479,7 +479,7 @@ Here are some **advanced Regex features** you might want to use or learn more ab For example, consider a regex violating if there's an empty line after an opening curly brace like so: `{\n\s*\n\s*\S`. This would match the lines of `func do() {\n\n return 5}`, but what you actually want is it to start matching on the empty newline like so: `(?<={\n)\s*\n\s*\S`. See also [#3](https://github.com/Flinesoft/AnyLint/issues/3) - + ## YAML Cheat Sheet Please be aware that in YAML indentation (whitespaces) and newlines are actually important. @@ -492,6 +492,8 @@ string2: 'This is with single quotes.' string3: "This is with double quotes." ``` +Note that only in double-quoted strings you can escape characters, e.g. `'Line1\nLine2'` will keep the `\n` as two characters in the result, whereas `"Line1\nLine2"` will escape `\n` to a newline. We recommend to use single quotes for `regex` arguments (the escaping will happen in the Regex parser) and double-quotes for any examples where you need escaping to be evaluated. + **Multi-line strings** can be written by specifying `|` and then a newline: ```yaml multiline1: | @@ -504,13 +506,13 @@ An additional `+` or `-` specified what to do with trailing newlines: ```yaml multiline2: |+ This will make sure both trailing newlines are kept (ends with ".\n\n"). - - + + multiline3: |- - This will ignore any trailing newlines and + This will ignore any trailing newlines and will end with the last non-newline character (the following dot in this case -->). - - + + ``` **Arrays** can be written in two ways: diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index 8dd7b5d..9d9bddf 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -1,29 +1,64 @@ FileContents: [] -# - id: Readme -# hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' -# regex: '^README\.md$' -# violateIfNoMatchesFound: true -# matchingExamples: ['README.md'] -# nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] +# TODO: replace below sample checks with your custom checks and remove empty array specifier `[]` from above + # - id: ReadmeTopLevelTitle + # hint: 'The README.md file should only contain a single top level title.' + # regex: '(^|\n)#[^#](.*\n)*\n#[^#]' + # includeFilters: ['^README\.md$'] + # matchingExamples: + # - | + # # Title + # ## Subtitle + # Lorem ipsum + # + # # Other Title + # ## Other Subtitle + # nonMatchingExamples: + # - | + # # Title + # ## Subtitle + # Lorem ipsum #1 and # 2. + # + # ## Other Subtitle + # ### Other Subsubtitle + # + # - id: ReadmeTypoLicense + # hint: 'ReadmeTypoLicense: Misspelled word `license`.' + # regex: '([\s#]L|l)isence([\s\.,:;])' + # matchingExamples: [' lisence:', "## Lisence\n"] + # nonMatchingExamples: [' license:', "## License\n"] + # includeFilters: ['^README\.md$'] + # autoCorrectReplacement: '$1icense$2' + # autoCorrectExamples: + # - { before: ' lisence:', after: ' license:' } + # - { before: "## Lisence\n", after: "## License\n" } FilePaths: [] -# - id: 'ReadmePath' -# hint: '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' } +# TODO: replace below sample checks with your custom checks and remove empty array specifier `[]` from above + # - id: Readme + # hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + # regex: '^README\.md$' + # violateIfNoMatchesFound: true + # matchingExamples: ['README.md'] + # nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + # + # - id: ReadmePath + # hint: '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' } CustomScripts: [] -# - id: LintConfig -# hint: 'Lint the AnyLint config file to conform to YAML best practices.' -# command: |- -# if which yamllint > /dev/null; then -# yamllint anylint.yml -# else -# echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' -# fi +# TODO: replace below sample check with your custom checks and remove empty array specifier `[]` from above + # - id: LintConfig + # hint: 'Lint the AnyLint config file to conform to YAML best practices.' + # command: |- + # if which yamllint > /dev/null; then + # yamllint anylint.yml + # else + # echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' + # fi diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 78cf79f..8f7f88d 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -1,15 +1,8 @@ FileContents: - - id: Readme - hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' - regex: '^README\.md$' - violateIfNoMatchesFound: true - matchingExamples: ['README.md'] - nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] - - id: ReadmeTopLevelTitle hint: 'The README.md file should only contain a single top level title.' regex: '(^|\n)#[^#](.*\n)*\n#[^#]' - includeFilter: ['^README\.md$'] + includeFilters: ['^README\.md$'] matchingExamples: - | # Title @@ -30,15 +23,22 @@ FileContents: - id: ReadmeTypoLicense hint: 'ReadmeTypoLicense: Misspelled word `license`.' regex: '([\s#]L|l)isence([\s\.,:;])' - matchingExamples: [' lisence:', '## Lisence\n'] - nonMatchingExamples: [' license:', '## License\n'] + matchingExamples: [' lisence:', "## Lisence\n"] + nonMatchingExamples: [' license:', "## License\n"] includeFilters: ['^README\.md$'] autoCorrectReplacement: '$1icense$2' autoCorrectExamples: - { before: ' lisence:', after: ' license:' } - - { before: '## Lisence\n', after: '## License\n' } + - { before: "## Lisence\n", after: "## License\n" } FilePaths: + - id: Readme + hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + regex: '^README\.md$' + violateIfNoMatchesFound: true + matchingExamples: ['README.md'] + nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + - id: ReadmePath hint: 'The README file should be named exactly `README.md`.' regex: '^(.*/)?([Rr][Ee][Aa][Dd][Mm][Ee]\.markdown|readme\.md|Readme\.md|ReadMe\.md)$' From 0bbe6cf1b33cc726c769a9a0465830ea37b5a012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 13 Sep 2021 06:58:18 +0200 Subject: [PATCH 35/37] Fix an issue with decoding Violation without date --- Package.swift | 1 + README.md | 62 ++++++++++++------- Sources/Checkers/Lint.swift | 1 + .../Core+DefaultCodableStrategy.swift | 1 + Sources/Configuration/Templates/Blank.yml | 2 +- .../Configuration/Templates/OpenSource.yml | 2 +- .../Date+DefaultCodableStrategy.swift | 9 +++ Sources/Core/Violation.swift | 4 +- 8 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 Sources/Core/Extensions/Date+DefaultCodableStrategy.swift diff --git a/Package.swift b/Package.swift index 996d7b5..2fca0db 100644 --- a/Package.swift +++ b/Package.swift @@ -33,6 +33,7 @@ let package = Package( .target( name: "Core", dependencies: [ + .product(name: "BetterCodable", package: "BetterCodable"), .product(name: "Rainbow", package: "Rainbow"), ] ), diff --git a/README.md b/README.md index 0b5b714..ca413c0 100644 --- a/README.md +++ b/README.md @@ -83,34 +83,48 @@ This will create the Swift script file `anylint.yml` with something like the fol ```yaml FileContents: [] -# - id: Readme -# hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' -# regex: '^README\.md$' -# violateIfNoMatchesFound: true -# matchingExamples: ['README.md'] -# nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] +# TODO: replace below sample checks with your custom checks and remove empty array specifier `[]` from above + # - id: ReadmeTypoLicense + # hint: 'ReadmeTypoLicense: Misspelled word `license`.' + # regex: '([\s#]L|l)isence([\s\.,:;])' + # matchingExamples: [' lisence:', "## Lisence\n"] + # nonMatchingExamples: [' license:', "## License\n"] + # includeFilters: ['^README\.md$'] + # autoCorrectReplacement: '$1icense$2' + # autoCorrectExamples: + # - { before: ' lisence:', after: ' license:' } + # - { before: "## Lisence\n", after: "## License\n" } FilePaths: [] -# - id: 'ReadmePath' -# hint: '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' } +# TODO: replace below sample checks with your custom checks and remove empty array specifier `[]` from above + # - id: Readme + # hint: 'Each project should have a README.md file, explaining how to use or contribute to the project.' + # regex: '^README\.md$' + # violateIfNoMatchesFound: true + # matchingExamples: ['README.md'] + # nonMatchingExamples: ['README.markdown', 'Readme.md', 'ReadMe.md'] + # + # - id: ReadmePath + # hint: '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' } CustomScripts: [] -# - id: LintConfig -# hint: 'Lint the AnyLint config file to conform to YAML best practices.' -# command: |- -# if which yamllint > /dev/null; then -# yamllint anylint.yml -# else -# echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' -# fi +# TODO: replace below sample check with your custom checks and remove empty array specifier `[]` from above + # - id: LintConfig + # hint: 'Lint the AnyLint config file to conform to YAML best practices.' + # command: |- + # if which yamllint > /dev/null; then + # yamllint anylint.yml + # else + # echo '{ "warning": { "YamlLint@warning: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' + # fi ``` diff --git a/Sources/Checkers/Lint.swift b/Sources/Checkers/Lint.swift index ce9644a..58206d4 100644 --- a/Sources/Checkers/Lint.swift +++ b/Sources/Checkers/Lint.swift @@ -137,6 +137,7 @@ public enum Lint { 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) + try shellOut(to: "chmod", arguments: ["+x", tempScriptFileUrl.path]) do { let output = try shellOut(to: "/bin/bash", arguments: [tempScriptFileUrl.path]) diff --git a/Sources/Configuration/Core+DefaultCodableStrategy.swift b/Sources/Configuration/Core+DefaultCodableStrategy.swift index 65c96c8..aea7e58 100644 --- a/Sources/Configuration/Core+DefaultCodableStrategy.swift +++ b/Sources/Configuration/Core+DefaultCodableStrategy.swift @@ -10,6 +10,7 @@ extension Severity { } extension Regex { + /// Use to set the default value of `Regex` instances to `.*` in rules when users don't provide an explicit value. public enum DefaultToMatchAllArray: DefaultCodableStrategy { public static var defaultValue: [Regex] { [try! Regex(".*")] } } diff --git a/Sources/Configuration/Templates/Blank.yml b/Sources/Configuration/Templates/Blank.yml index 9d9bddf..8f5cab2 100644 --- a/Sources/Configuration/Templates/Blank.yml +++ b/Sources/Configuration/Templates/Blank.yml @@ -60,5 +60,5 @@ CustomScripts: [] # if which yamllint > /dev/null; then # yamllint anylint.yml # else - # echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' + # echo '{ "warning": { "YamlLint@warning: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' # fi diff --git a/Sources/Configuration/Templates/OpenSource.yml b/Sources/Configuration/Templates/OpenSource.yml index 8f7f88d..3fb7dfe 100644 --- a/Sources/Configuration/Templates/OpenSource.yml +++ b/Sources/Configuration/Templates/OpenSource.yml @@ -57,5 +57,5 @@ CustomScripts: if which yamllint > /dev/null; then yamllint anylint.yml else - echo '{ "warning": { "YamlLint: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' + echo '{ "warning": { "YamlLint@warning: Not installed, see instructions at https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint": [{}] } }' fi diff --git a/Sources/Core/Extensions/Date+DefaultCodableStrategy.swift b/Sources/Core/Extensions/Date+DefaultCodableStrategy.swift new file mode 100644 index 0000000..d72aedd --- /dev/null +++ b/Sources/Core/Extensions/Date+DefaultCodableStrategy.swift @@ -0,0 +1,9 @@ +import Foundation +import BetterCodable + +extension Date { + /// Use to set the default value of `Date` instances to `Date.now` in rules when users don't provide an explicit value. + public enum DefaultToNow: DefaultCodableStrategy { + public static var defaultValue: Date { Date() } + } +} diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index b585a59..f0d3d8b 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -1,9 +1,11 @@ import Foundation +import BetterCodable /// 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 + @DefaultCodable + public var discoverDate: Date /// The matched string that violates the check. public let matchedString: String? From 1fee0c07e8360f1ab0760a6203203566024a6b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 13 Sep 2021 07:23:44 +0200 Subject: [PATCH 36/37] Add temporary workaround for a Date coding issue See https://github.com/marksands/BetterCodable/issues/45 --- Sources/Reporting/Extensions/JSONDecoderExt.swift | 3 ++- Sources/Reporting/Extensions/JSONEncoderExt.swift | 3 ++- Tests/ReportingTests/LintResultsTests.swift | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/Reporting/Extensions/JSONDecoderExt.swift b/Sources/Reporting/Extensions/JSONDecoderExt.swift index 7e5bf7d..66cda4b 100644 --- a/Sources/Reporting/Extensions/JSONDecoderExt.swift +++ b/Sources/Reporting/Extensions/JSONDecoderExt.swift @@ -3,7 +3,8 @@ import Foundation extension JSONDecoder { public static var iso: JSONDecoder { let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 + // TODO: uncomment once following issue is fixed: https://github.com/marksands/BetterCodable/issues/45 + // decoder.dateDecodingStrategy = .iso8601 return decoder } } diff --git a/Sources/Reporting/Extensions/JSONEncoderExt.swift b/Sources/Reporting/Extensions/JSONEncoderExt.swift index 1e54af8..9b360e3 100644 --- a/Sources/Reporting/Extensions/JSONEncoderExt.swift +++ b/Sources/Reporting/Extensions/JSONEncoderExt.swift @@ -3,7 +3,8 @@ import Foundation extension JSONEncoder { public static var iso: JSONEncoder { let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 + // TODO: uncomment once following issue is fixed: https://github.com/marksands/BetterCodable/issues/45 + // encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = .prettyPrinted return encoder } diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index e9ccebd..6a65924 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -329,7 +329,8 @@ final class LintResultsTests: XCTestCase { ] let encodedData = try JSONEncoder.iso.encode(lintResults) let encodedString = String(data: encodedData, encoding: .utf8)! - XCTAssert(encodedString.count > 500) + // TODO: set back to 500 once following issue is fixed: https://github.com/marksands/BetterCodable/issues/45 + XCTAssert(encodedString.count > 400) let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) XCTAssertNoDifference(decodedLintResults, lintResults) From 3679098bc2f68cc8b0860376e199221d6996476a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 13 Sep 2021 07:39:15 +0200 Subject: [PATCH 37/37] Add workaround for Codable date encoding issue --- .../Core/Extensions/Date+BetterCodable.swift | 28 +++++++++++++++++++ .../Date+DefaultCodableStrategy.swift | 9 ------ Sources/Core/Extensions/JSONDecoderExt.swift | 9 ++++++ .../Extensions/JSONEncoderExt.swift | 3 +- Sources/Core/Violation.swift | 2 +- .../Reporting/Extensions/JSONDecoderExt.swift | 10 ------- Tests/ReportingTests/LintResultsTests.swift | 3 +- 7 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 Sources/Core/Extensions/Date+BetterCodable.swift delete mode 100644 Sources/Core/Extensions/Date+DefaultCodableStrategy.swift create mode 100644 Sources/Core/Extensions/JSONDecoderExt.swift rename Sources/{Reporting => Core}/Extensions/JSONEncoderExt.swift (54%) delete mode 100644 Sources/Reporting/Extensions/JSONDecoderExt.swift diff --git a/Sources/Core/Extensions/Date+BetterCodable.swift b/Sources/Core/Extensions/Date+BetterCodable.swift new file mode 100644 index 0000000..6f9023d --- /dev/null +++ b/Sources/Core/Extensions/Date+BetterCodable.swift @@ -0,0 +1,28 @@ +import Foundation +import BetterCodable + +extension Date { + /// Use to set the default value of `Date` instances to `Date.now` in rules when users don't provide an explicit value. + public enum DefaultToNowISO8601: DateValueCodableStrategy { + public static func decode(_ value: String) throws -> Date { + (try? JSONDecoder.iso.decode(Date.self, from: value.data(using: .utf8)!)) ?? Date() + } + + public static func encode(_ date: Date) -> String { + String(data: try! JSONEncoder.iso.encode(date), encoding: .utf8)! + } + } +} + +// TODO: remove these once the related PR is merged: https://github.com/marksands/BetterCodable/pull/43 +extension DateValue: Equatable { + public static func == (lhs: DateValue, rhs: DateValue) -> Bool { + return lhs.wrappedValue == rhs.wrappedValue + } +} + +extension DateValue: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(wrappedValue) + } +} diff --git a/Sources/Core/Extensions/Date+DefaultCodableStrategy.swift b/Sources/Core/Extensions/Date+DefaultCodableStrategy.swift deleted file mode 100644 index d72aedd..0000000 --- a/Sources/Core/Extensions/Date+DefaultCodableStrategy.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import BetterCodable - -extension Date { - /// Use to set the default value of `Date` instances to `Date.now` in rules when users don't provide an explicit value. - public enum DefaultToNow: DefaultCodableStrategy { - public static var defaultValue: Date { Date() } - } -} diff --git a/Sources/Core/Extensions/JSONDecoderExt.swift b/Sources/Core/Extensions/JSONDecoderExt.swift new file mode 100644 index 0000000..7e5bf7d --- /dev/null +++ b/Sources/Core/Extensions/JSONDecoderExt.swift @@ -0,0 +1,9 @@ +import Foundation + +extension JSONDecoder { + public static var iso: JSONDecoder { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + } +} diff --git a/Sources/Reporting/Extensions/JSONEncoderExt.swift b/Sources/Core/Extensions/JSONEncoderExt.swift similarity index 54% rename from Sources/Reporting/Extensions/JSONEncoderExt.swift rename to Sources/Core/Extensions/JSONEncoderExt.swift index 9b360e3..1e54af8 100644 --- a/Sources/Reporting/Extensions/JSONEncoderExt.swift +++ b/Sources/Core/Extensions/JSONEncoderExt.swift @@ -3,8 +3,7 @@ import Foundation extension JSONEncoder { public static var iso: JSONEncoder { let encoder = JSONEncoder() - // TODO: uncomment once following issue is fixed: https://github.com/marksands/BetterCodable/issues/45 - // encoder.dateEncodingStrategy = .iso8601 + encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = .prettyPrinted return encoder } diff --git a/Sources/Core/Violation.swift b/Sources/Core/Violation.swift index f0d3d8b..83cb8fd 100644 --- a/Sources/Core/Violation.swift +++ b/Sources/Core/Violation.swift @@ -4,7 +4,7 @@ import BetterCodable /// A violation found in a check. public struct Violation: Codable, Equatable { /// The exact time this violation was discovered. Needed for sorting purposes. - @DefaultCodable + @DateValue public var discoverDate: Date /// The matched string that violates the check. diff --git a/Sources/Reporting/Extensions/JSONDecoderExt.swift b/Sources/Reporting/Extensions/JSONDecoderExt.swift deleted file mode 100644 index 66cda4b..0000000 --- a/Sources/Reporting/Extensions/JSONDecoderExt.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -extension JSONDecoder { - public static var iso: JSONDecoder { - let decoder = JSONDecoder() - // TODO: uncomment once following issue is fixed: https://github.com/marksands/BetterCodable/issues/45 - // decoder.dateDecodingStrategy = .iso8601 - return decoder - } -} diff --git a/Tests/ReportingTests/LintResultsTests.swift b/Tests/ReportingTests/LintResultsTests.swift index 6a65924..e9ccebd 100644 --- a/Tests/ReportingTests/LintResultsTests.swift +++ b/Tests/ReportingTests/LintResultsTests.swift @@ -329,8 +329,7 @@ final class LintResultsTests: XCTestCase { ] let encodedData = try JSONEncoder.iso.encode(lintResults) let encodedString = String(data: encodedData, encoding: .utf8)! - // TODO: set back to 500 once following issue is fixed: https://github.com/marksands/BetterCodable/issues/45 - XCTAssert(encodedString.count > 400) + XCTAssert(encodedString.count > 500) let decodedLintResults = try JSONDecoder.iso.decode(LintResults.self, from: encodedData) XCTAssertNoDifference(decodedLintResults, lintResults)