From bf349b2769792db84be5c565df38d0de500ee512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sat, 7 Mar 2020 22:08:59 +0100 Subject: [PATCH 01/27] Basic project structure setup with root files --- CHANGELOG.md | 32 +++++++++ LICENSE | 21 ++++++ Package.resolved | 16 +++++ Package.swift | 27 +++---- README.md | 78 ++++++++++++++++++++- Sources/AnyLint/AnyLint.swift | 6 +- Sources/AnyLintCLI/AnyLintCLI.swift | 3 + Tests/AnyLintCLITests/AnyLintCLITests.swift | 8 +++ Tests/AnyLintTests/AnyLintTests.swift | 9 +-- Tests/AnyLintTests/XCTestManifests.swift | 9 --- Tests/LinuxMain.swift | 7 -- 11 files changed, 176 insertions(+), 40 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 Package.resolved create mode 100644 Sources/AnyLintCLI/AnyLintCLI.swift create mode 100644 Tests/AnyLintCLITests/AnyLintCLITests.swift delete mode 100644 Tests/AnyLintTests/XCTestManifests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab46810 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). + +
+Formatting Rules for Entries +Each entry should use the following format: + +```markdown +- Summary of what was changed in a single line using past tense & followed by two whitespaces. + Issue: [#0](https://github.com/Flinesoft/AnyLint/issues/0) | PR: [#0](https://github.com/Flinesoft/AnyLint/pull/0) | Author: [Cihat Gündüz](https://github.com/Jeehut) +``` + +Note that at the end of the summary line, you need to add two whitespaces (` `) for correct rendering on GitHub. + +If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries separated by `, `. Also, remove entries not needed in the second line. +
+ +## [Unreleased] +### Added +- None. +### Changed +- None. +### Deprecated +- None. +### Removed +- None. +### Fixed +- None. +### Security +- None. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b1efe33 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..16f18a7 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "HandySwift", + "repositoryURL": "https://github.com/Flinesoft/HandySwift.git", + "state": { + "branch": null, + "revision": "083707d9f9da65bd57b756294653ee0fc50d8662", + "version": "3.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index f3df8ca..ca1d951 100644 --- a/Package.swift +++ b/Package.swift @@ -1,28 +1,31 @@ // swift-tools-version:5.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. - import PackageDescription let package = Package( name: "AnyLint", products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. - .library( - name: "AnyLint", - targets: ["AnyLint"]), + .library(name: "AnyLint", targets: ["AnyLint"]), + .executable(name: "anylint", targets: ["AnyLintCLI"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/Flinesoft/HandySwift.git", from: "3.1.0"), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "AnyLint", - dependencies: []), + dependencies: ["HandySwift"] + ), .testTarget( name: "AnyLintTests", - dependencies: ["AnyLint"]), + dependencies: ["AnyLint"] + ), + .target( + name: "AnyLintCLI", + dependencies: ["HandySwift"] + ), + .testTarget( + name: "AnyLintCLITests", + dependencies: ["AnyLintCLI"] + ), ] ) diff --git a/README.md b/README.md index 71f570d..fd5e0a1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,79 @@ # AnyLint -A description of this package. +Lint anything by combining the power of Swift & regular expressions. + +

+ + Build Status + + + Code Quality + + + Coverage + + + Version: 0.0.0 + + + License: MIT + +
+ + PayPal: Donate + + + GitHub: Become a sponsor + + + Patreon: Become a patron + +

+ +

+ Installation + • Configuration + • Usage + • Donation + • Issues + • Contributing + • License +

+ +# AnyLint + +This is the FitnessTracker app project for the Android platform. + +## Installation + +TODO + +## Configuration + +TODO + +## Usage + +TODO + +## Donation + +AnyLint was brought to you by [Cihat Gündüz](https://github.com/Jeehut) in his free time. If you want to thank me and support the development of this project, please **make a small donation on [PayPal](https://paypal.me/Dschee/5EUR)**. In case you also like my other [open source contributions](https://github.com/Flinesoft) and [articles](https://medium.com/@Jeehut), please consider motivating me by **becoming a sponsor on [GitHub](https://github.com/sponsors/Jeehut)** or a **patron on [Patreon](https://www.patreon.com/Jeehut)**. + +Thank you very much for any donation, it really helps out a lot! 💯 + +## Contributing + +Contributions are welcome. Feel free to open an issue on GitHub with your ideas or implement an idea yourself and post a pull request. If you want to contribute code, please try to follow the same syntax and semantic in your **commit messages** (see rationale [here](http://chris.beams.io/posts/git-commit/)). Also, please make sure to add an entry to the `CHANGELOG.md` file which explains your change. + +## License + +This library is released under the [MIT License](http://opensource.org/licenses/MIT). See LICENSE for details. diff --git a/Sources/AnyLint/AnyLint.swift b/Sources/AnyLint/AnyLint.swift index 78f37fa..934117e 100644 --- a/Sources/AnyLint/AnyLint.swift +++ b/Sources/AnyLint/AnyLint.swift @@ -1,3 +1,3 @@ -struct AnyLint { - var text = "Hello, World!" -} +import Foundation + +// TODO: [2020-03-07] not yet implemented diff --git a/Sources/AnyLintCLI/AnyLintCLI.swift b/Sources/AnyLintCLI/AnyLintCLI.swift new file mode 100644 index 0000000..934117e --- /dev/null +++ b/Sources/AnyLintCLI/AnyLintCLI.swift @@ -0,0 +1,3 @@ +import Foundation + +// TODO: [2020-03-07] not yet implemented diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift new file mode 100644 index 0000000..f8318d1 --- /dev/null +++ b/Tests/AnyLintCLITests/AnyLintCLITests.swift @@ -0,0 +1,8 @@ +import XCTest +@testable import AnyLintCLI + +final class AnyLintCLITests: XCTestCase { + func testExample() { + // TODO: [2020-03-07] not yet implemented + } +} diff --git a/Tests/AnyLintTests/AnyLintTests.swift b/Tests/AnyLintTests/AnyLintTests.swift index 5b13bbb..4ea10b5 100644 --- a/Tests/AnyLintTests/AnyLintTests.swift +++ b/Tests/AnyLintTests/AnyLintTests.swift @@ -3,13 +3,6 @@ import XCTest final class AnyLintTests: XCTestCase { func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(AnyLint().text, "Hello, World!") + // TODO: [2020-03-07] not yet implemented } - - static var allTests = [ - ("testExample", testExample), - ] } diff --git a/Tests/AnyLintTests/XCTestManifests.swift b/Tests/AnyLintTests/XCTestManifests.swift deleted file mode 100644 index 8ebc876..0000000 --- a/Tests/AnyLintTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(AnyLintTests.allTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 880dd06..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import AnyLintTests - -var tests = [XCTestCaseEntry]() -tests += AnyLintTests.allTests() -XCTMain(tests) From 024dc2ca18bf1f5d4ecf16184e9e62dd56eb9e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 8 Mar 2020 22:44:39 +0100 Subject: [PATCH 02/27] Fix CI & configure SwiftLint with warning fixes --- .swiftlint.yml | 376 ++++++++++++++++++++ Sources/AnyLint/AnyLint.swift | 2 +- Sources/AnyLintCLI/AnyLintCLI.swift | 2 +- Sources/AnyLintCLI/main.swift | 3 + Tests/AnyLintCLITests/AnyLintCLITests.swift | 4 +- Tests/AnyLintTests/AnyLintTests.swift | 4 +- 6 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 Sources/AnyLintCLI/main.swift diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..eeb8dea --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,376 @@ +# 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 +- let_var_whitespace +- 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 +- overridden_super_call +- 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 + +# 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: 160 + +nesting: + type_level: 3 + +trailing_comma: + mandatory_comma: true + +trailing_whitespace: + ignores_comments: false + +# Custom Rules +custom_rules: + class_name_suffix_collection_view_controller: + included: ".*.swift" + regex: 'class +\w+(?]+>)? *: +\w+CollectionViewController' + name: "Class Name Suffix View Controller" + message: "All `CollectionViewController` subclasses should end on `CollectionViewController`." + severity: warning + class_name_suffix_table_view_controller: + included: ".*.swift" + regex: 'class +\w+(?]+>)? *: +\w+TableViewController' + name: "Class Name Suffix View Controller" + message: "All `TableViewController` subclasses should end on `TableViewController`." + severity: warning + class_name_suffix_view_controller: + included: ".*.swift" + regex: 'class +\w+(?]+>)? *: +\w+ViewController' + name: "Class Name Suffix View Controller" + message: "All `ViewController` subclasses should end on `ViewController`." + severity: warning + closure_params_parantheses: + included: ".*.swift" + regex: '\{\s*\((?!self)[^):]+\)\s*in' + name: "Unnecessary Closure Params Parantheses" + message: "Don't use parantheses around non-typed parameters in a closure." + severity: warning + comment_type_note: + included: ".*.swift" + regex: '// *(?:WORKAROUND|HACK|WARNING)[:\\s]' + name: "Comment Type NOTE" + message: "Use a '// NOTE:' comment instead." + severity: warning + comment_type_refactor: + included: ".*.swift" + regex: '// *(?:TODO|NOTE)[:\\s][^\n]*(?:refactor|REFACTOR|Refactor)' + name: "Comment Type REFACTOR" + message: "Use a '// REFACTOR:' comment instead." + severity: warning + comment_type_todo: + included: ".*.swift" + regex: '// *(?:BUG|MOCK|FIXME|RELEASE|TEST)[:\\s]' + name: "Comment Type TODO" + message: "Use a '// TODO:' comment instead." + severity: warning + controller_class_name_suffix: + included: ".*.swift" + regex: 'class +\w+(?\w+)(?:<[^\>]+>)? *\{.*static let `default`(?:: *\k)? *= *\k\(.*(?<=private) init\(' + name: "Singleton Default Private Init" + message: "Singletons with a `default` object (pseudo-singletons) should not declare init methods as private." + severity: warning + singleton_shared_final: + included: ".*.swift" + regex: '(?\w+)(?:<[^\>]+>)? *\{.*static let shared(?:: *\k)? *= *\k\(' + name: "Singleton Shared Final" + message: "Singletons with a single object (`shared`) should be marked as final." + severity: warning + singleton_shared_private_init: + included: ".*.swift" + regex: 'class +(?\w+)(?:<[^\>]+>)? *\{.*static let shared(?:: *\k)? *= *\k\(.*(?<= |\t|public|internal) init\(' + name: "Singleton Shared Private Init" + message: "Singletons with a single object (`shared`) should declare their init method(s) as private." + severity: warning + singleton_shared_single_object: + included: ".*.swift" + regex: 'class +(?\w+)(?:<[^\>]+>)? *\{.*(?:static let shared(?:: *\k)? *= *\k\(.*static let \w+(?:: *\k)? *= *\k\(|static let \w+(?:: *\k)? *= *\k\(.*static let shared(?:: *\k)? *= *\k\()' + name: "Singleton Shared Single Object" + message: "Singletons with a `shared` object (real Singletons) should not have other static let properties. Use `default` instead (if needed)." + severity: warning + switch_associated_value_style: + included: ".*.swift" + regex: 'case\s+[^\(][^\n]*(?:\(let |[^\)], let)' + name: "Switch Associated Value Style" + message: "Always put the `let` in front of case – even if only one associated value captured." + severity: warning + todo_format: + included: ".*.swift" + regex: '\/\/ TODO: [^\n]{0,14}\n|\/\/ TODO: \[\S{1,12}\]|\/\/ TODO: [^\[]|\/\/ TODO: \[.{13}[^\]]|\/\/ TODO: \[[^a-z]{2}|\/\/ TODO: \[.{2}[^_]|\/\/ TODO: \[.{7}[^-]|\/\/ TODO: \[.{10}[^-]' + name: "Todo Date" + message: "All TODOs should have a format with creator credentials & date of their creation documented like this: `// TODO: [cg_YYYY-MM-DD] `." + severity: warning + todo_uppercase: + included: ".*.swift" + regex: '\/\/ ?tODO|\/\/ ?ToDO|\/\/ ?TOdO|\/\/ ?TODo|\/\/ ?todo|\/\/ ?Todo|\/\/ ?ToDo|\/\/ ?toDo' + name: "Todo Uppercase" + message: "All TODOs should be all-uppercased like this: `// TODO: [cg_YYYY-MM-DD] `." + severity: warning + todo_whitespacing: + included: ".*.swift" + regex: '\/\/TODO|\/\/ TODO\s|\/\/ TODO:[^ ]|\/\/ TODO: |\/\/ TODO: \[[^\s]{0,10}\][^ ]' + name: "Todo Whitespace" + message: "All TODOs should exactly start like this (mind the whitespacing): `// TODO: [cg_YYYY-MM-DD] `." + severity: warning + tuple_index: + included: ".*.swift" + regex: '(\$\d|\w*[^\d \(\[\{])\.\d' + name: "Tuple Index" + message: "Prevent unwraping tuples by their index – define a typealias with named components instead." + severity: warning + unnecessary_case_break: + included: ".*.swift" + regex: '(case |default)(?:[^\n\}]+\n){2,}\s*break *\n|\n *\n *break(?:\n *\n|\n *\})' + name: "Unnecessary Case Break" + message: "Don't use break in switch cases – Swift breaks by default." + severity: warning + unnecessary_nil_assignment: + included: ".*.swift" + regex: 'var \S+\s*:\s*[^\s]+\?\s*=\s*nil' + name: "Unnecessary Nil Assignment" + message: "Don't assign nil as a value when defining an optional type – it's nil by default." + severity: warning + vertical_whitespaces_around_mark: + included: ".*.swift" + regex: '\/\/\s*MARK:[^\n]*(\n\n)|(\n\n\n)[ \t]*\/\/\s*MARK:|[^\s{]\n[^\n\/]*\/\/\s*MARK:' + name: "Vertical Whitespaces Around MARK:" + message: "Include a single vertical whitespace (empty line) before and none after MARK: comments." + severity: warning + view_controller_variable_naming: + included: ".*.swift" + regex: '(?:let|var) +\w*(?:vc|VC|Vc|viewC|viewController|ViewController) *=' + name: "View Controller Variable Naming" + message: "Always name your view controller variables with the suffix `ViewCtrl`." + severity: warning + whitespace_around_range_operators: + included: ".*.swift" + regex: '\w\.\.[<\.]\w' + name: "Whitespace around Range Operators" + message: "A range operator should be surrounded by a single whitespace." + severity: warning + whitespace_comment_start: + included: ".*.swift" + regex: '[^:#\]\}\)][^:#\]\}\)]\/\/[^\s\/]' + name: "Whitespace Comment Start" + message: "A comment should always start with a whitespace." + severity: warning diff --git a/Sources/AnyLint/AnyLint.swift b/Sources/AnyLint/AnyLint.swift index 934117e..367d91a 100644 --- a/Sources/AnyLint/AnyLint.swift +++ b/Sources/AnyLint/AnyLint.swift @@ -1,3 +1,3 @@ import Foundation -// TODO: [2020-03-07] not yet implemented +// TODO: [cg_2020-03-07] not yet implemented diff --git a/Sources/AnyLintCLI/AnyLintCLI.swift b/Sources/AnyLintCLI/AnyLintCLI.swift index 934117e..367d91a 100644 --- a/Sources/AnyLintCLI/AnyLintCLI.swift +++ b/Sources/AnyLintCLI/AnyLintCLI.swift @@ -1,3 +1,3 @@ import Foundation -// TODO: [2020-03-07] not yet implemented +// TODO: [cg_2020-03-07] not yet implemented diff --git a/Sources/AnyLintCLI/main.swift b/Sources/AnyLintCLI/main.swift new file mode 100644 index 0000000..0dc6595 --- /dev/null +++ b/Sources/AnyLintCLI/main.swift @@ -0,0 +1,3 @@ +import Foundation + +// TODO: [cg_2020-03-08] not yet implemented diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift index f8318d1..8120a15 100644 --- a/Tests/AnyLintCLITests/AnyLintCLITests.swift +++ b/Tests/AnyLintCLITests/AnyLintCLITests.swift @@ -1,8 +1,8 @@ -import XCTest @testable import AnyLintCLI +import XCTest final class AnyLintCLITests: XCTestCase { func testExample() { - // TODO: [2020-03-07] not yet implemented + // TODO: [cg_2020-03-07] not yet implemented } } diff --git a/Tests/AnyLintTests/AnyLintTests.swift b/Tests/AnyLintTests/AnyLintTests.swift index 4ea10b5..6627c33 100644 --- a/Tests/AnyLintTests/AnyLintTests.swift +++ b/Tests/AnyLintTests/AnyLintTests.swift @@ -1,8 +1,8 @@ -import XCTest @testable import AnyLint +import XCTest final class AnyLintTests: XCTestCase { func testExample() { - // TODO: [2020-03-07] not yet implemented + // TODO: [cg_2020-03-07] not yet implemented } } From 338f199d636c3fddba4402f9ccc6fb5c67e7832f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 9 Mar 2020 07:29:57 +0100 Subject: [PATCH 03/27] Fix Bitrise CI & Codacy badge links --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fd5e0a1..652ba3e 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ Lint anything by combining the power of Swift & regular expressions.

- - + Build Status - Code Quality - Coverage From 8bac331afe481e1d4e986358bcc55ce520e10e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 9 Mar 2020 07:32:23 +0100 Subject: [PATCH 04/27] [README.md] Fix duplicate heading --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 652ba3e..f51f899 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -# AnyLint - -Lint anything by combining the power of Swift & regular expressions. -

Date: Tue, 10 Mar 2020 09:09:25 +0100 Subject: [PATCH 05/27] Integrate SwiftCLI & design basic command line interface API --- .../contents.xcworkspacedata | 2 +- Package.resolved | 9 ++++ Package.swift | 3 +- Sources/AnyLintCLI/AnyLintCLI.swift | 3 -- .../AnyLintCLI/Commands/SingleCommand.swift | 50 +++++++++++++++++++ Sources/AnyLintCLI/Constants.swift | 7 +++ Sources/AnyLintCLI/Tasks/InitTask.swift | 18 +++++++ Sources/AnyLintCLI/Tasks/LintTask.swift | 11 ++++ Sources/AnyLintCLI/Tasks/Task.swift | 5 ++ Sources/AnyLintCLI/Tasks/VersionTask.swift | 10 ++++ Sources/AnyLintCLI/main.swift | 4 +- 11 files changed, 116 insertions(+), 6 deletions(-) delete mode 100644 Sources/AnyLintCLI/AnyLintCLI.swift create mode 100644 Sources/AnyLintCLI/Commands/SingleCommand.swift create mode 100644 Sources/AnyLintCLI/Constants.swift create mode 100644 Sources/AnyLintCLI/Tasks/InitTask.swift create mode 100644 Sources/AnyLintCLI/Tasks/LintTask.swift create mode 100644 Sources/AnyLintCLI/Tasks/Task.swift create mode 100644 Sources/AnyLintCLI/Tasks/VersionTask.swift diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata index 706eede..919434a 100644 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/Package.resolved b/Package.resolved index 16f18a7..00c15df 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "revision": "083707d9f9da65bd57b756294653ee0fc50d8662", "version": "3.1.0" } + }, + { + "package": "SwiftCLI", + "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", + "state": { + "branch": null, + "revision": "c72c4564f8c0a24700a59824880536aca45a4cae", + "version": "6.0.1" + } } ] }, diff --git a/Package.swift b/Package.swift index ca1d951..756138f 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/Flinesoft/HandySwift.git", from: "3.1.0"), + .package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.1"), ], targets: [ .target( @@ -21,7 +22,7 @@ let package = Package( ), .target( name: "AnyLintCLI", - dependencies: ["HandySwift"] + dependencies: ["HandySwift", "SwiftCLI"] ), .testTarget( name: "AnyLintCLITests", diff --git a/Sources/AnyLintCLI/AnyLintCLI.swift b/Sources/AnyLintCLI/AnyLintCLI.swift deleted file mode 100644 index 367d91a..0000000 --- a/Sources/AnyLintCLI/AnyLintCLI.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Foundation - -// TODO: [cg_2020-03-07] not yet implemented diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift new file mode 100644 index 0000000..b59b0e4 --- /dev/null +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -0,0 +1,50 @@ +import Foundation +import SwiftCLI + +class SingleCommand: Command { + // MARK: - Basics + var name: String = "anylint" + var shortDescription: String = "Lint anything by combining the power of Swift & regular expressions." + + // MARK: - Subcommands + @Flag("-v", "--version", description: "Print the current tool version") + var version: Bool + + @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(Constants.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 { + // version subcommand + if version { + VersionTask().perform() + exit(EXIT_SUCCESS) + } + + let configurationPaths = customPaths.isEmpty ? [Constants.defaultConfigurationPath] : customPaths + + // init subcommand + if let initTemplateName = initTemplateName { + guard let initTemplate = InitTask.Template(rawValue: initTemplateName) else { + // TODO: [cg_2020-03-10] replace print with more semantically weighted output that also makes CLI testable + print("Unknown default template '\(initTemplateName)' – use one of: [\(Constants.initTemplateCases)]") + exit(EXIT_FAILURE) + } + + for path in configurationPaths { + InitTask(path: path, template: initTemplate).perform() + } + exit(EXIT_SUCCESS) + } + + // lint command + for path in configurationPaths { + LintTask(path: path).perform() + } + exit(EXIT_SUCCESS) + } +} diff --git a/Sources/AnyLintCLI/Constants.swift b/Sources/AnyLintCLI/Constants.swift new file mode 100644 index 0000000..b855f0d --- /dev/null +++ b/Sources/AnyLintCLI/Constants.swift @@ -0,0 +1,7 @@ +import Foundation + +enum Constants { + static let defaultConfigurationPath: String = "AnyLint.swift" + static let currentVersion: String = "0.1.0" + static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") +} diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift new file mode 100644 index 0000000..ec3d1af --- /dev/null +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -0,0 +1,18 @@ +import Foundation + +struct InitTask { + enum Template: String, CaseIterable { + case blank + case ios + case android + } + + let path: String + let template: Template +} + +extension InitTask: Task { + func perform() { + // TODO: [cg_2020-03-10] not yet implemented + } +} diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift new file mode 100644 index 0000000..6a238c3 --- /dev/null +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -0,0 +1,11 @@ +import Foundation + +struct LintTask { + let path: String +} + +extension LintTask: Task { + func perform() { + // TODO: [cg_2020-03-10] not yet implemented + } +} diff --git a/Sources/AnyLintCLI/Tasks/Task.swift b/Sources/AnyLintCLI/Tasks/Task.swift new file mode 100644 index 0000000..7174104 --- /dev/null +++ b/Sources/AnyLintCLI/Tasks/Task.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol Task { + func perform() +} diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift new file mode 100644 index 0000000..d38d2df --- /dev/null +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -0,0 +1,10 @@ +import Foundation + +struct VersionTask {} + +extension VersionTask: Task { + func perform() { + // TODO: [cg_2020-03-10] replace print with more semantically weighted output that also makes CLI testable + print(Constants.currentVersion) + } +} diff --git a/Sources/AnyLintCLI/main.swift b/Sources/AnyLintCLI/main.swift index 0dc6595..d4cc92b 100644 --- a/Sources/AnyLintCLI/main.swift +++ b/Sources/AnyLintCLI/main.swift @@ -1,3 +1,5 @@ import Foundation +import SwiftCLI -// TODO: [cg_2020-03-08] not yet implemented +let singleCommand = CLI(singleCommand: SingleCommand()) +singleCommand.goAndExit() From edd5d4c6fab7a52d4d15374c50efb3dfecdb8308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Tue, 10 Mar 2020 09:22:07 +0100 Subject: [PATCH 06/27] Fix SwiftLint warnings --- .swiftlint.yml | 1 - Sources/AnyLintCLI/Tasks/VersionTask.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index eeb8dea..0a0572b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -34,7 +34,6 @@ opt_in_rules: - last_where - legacy_multiple - legacy_random -- let_var_whitespace - literal_expression_end_indentation - lower_acl_than_parent - missing_docs diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift index d38d2df..4b7676d 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -1,6 +1,6 @@ import Foundation -struct VersionTask {} +struct VersionTask { /* for extension purposes only */ } extension VersionTask: Task { func perform() { From 22af1197cb9feb51814f67d16f3ba4acd5dfdeb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Wed, 11 Mar 2020 08:06:10 +0100 Subject: [PATCH 07/27] Implement Logger with variable output formats --- .swiftlint.yml | 8 +- Package.resolved | 9 ++ Package.swift | 3 +- Sources/AnyLint/AnyLint.swift | 4 +- .../AnyLintCLI/Commands/SingleCommand.swift | 5 +- .../AnyLintCLI/{ => Globals}/Constants.swift | 6 +- Sources/AnyLintCLI/Globals/Logger.swift | 115 ++++++++++++++++++ Sources/AnyLintCLI/Globals/TestHelper.swift | 20 +++ Sources/AnyLintCLI/Tasks/VersionTask.swift | 3 +- Tests/AnyLintCLITests/AnyLintCLITests.swift | 1 - 10 files changed, 161 insertions(+), 13 deletions(-) rename Sources/AnyLintCLI/{ => Globals}/Constants.swift (67%) create mode 100644 Sources/AnyLintCLI/Globals/Logger.swift create mode 100644 Sources/AnyLintCLI/Globals/TestHelper.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 0a0572b..089fdf4 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -241,11 +241,11 @@ custom_rules: name: "Local l10n" message: "Don't name local variable `l10n` – use a property instead and further specify with `localL10n` if needed." severity: warning - log_prefix: + logger: included: ".*.swift" - regex: 'log\.(?:verbose|debug|info|warning|error)\("(?:verbose|debug|info|warning|error).*"\)' - name: "Logging Prefix" - message: "Don't use logging prefixes with log.verbose/debug/info/warning/error – done automatically." + regex: 'print\([^\n]+\)' + name: "Logger" + message: "Don't use `print` – use `log.message` instead." severity: warning multiline_guard_end: included: ".*.swift" diff --git a/Package.resolved b/Package.resolved index 00c15df..f02fdd2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,6 +10,15 @@ "version": "3.1.0" } }, + { + "package": "Rainbow", + "repositoryURL": "https://github.com/onevcat/Rainbow.git", + "state": { + "branch": null, + "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", + "version": "3.1.5" + } + }, { "package": "SwiftCLI", "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", diff --git a/Package.swift b/Package.swift index 756138f..a7fdcab 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/Flinesoft/HandySwift.git", from: "3.1.0"), + .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: [ @@ -22,7 +23,7 @@ let package = Package( ), .target( name: "AnyLintCLI", - dependencies: ["HandySwift", "SwiftCLI"] + dependencies: ["HandySwift", "Rainbow", "SwiftCLI"] ), .testTarget( name: "AnyLintCLITests", diff --git a/Sources/AnyLint/AnyLint.swift b/Sources/AnyLint/AnyLint.swift index 367d91a..3ff07c7 100644 --- a/Sources/AnyLint/AnyLint.swift +++ b/Sources/AnyLint/AnyLint.swift @@ -1,3 +1,5 @@ import Foundation -// TODO: [cg_2020-03-07] not yet implemented +enum AnyLint { + // TODO: [cg_2020-03-07] not yet implemented +} diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index b59b0e4..0701121 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -3,7 +3,7 @@ import SwiftCLI class SingleCommand: Command { // MARK: - Basics - var name: String = "anylint" + var name: String = Constants.commandName var shortDescription: String = "Lint anything by combining the power of Swift & regular expressions." // MARK: - Subcommands @@ -30,8 +30,7 @@ class SingleCommand: Command { // init subcommand if let initTemplateName = initTemplateName { guard let initTemplate = InitTask.Template(rawValue: initTemplateName) else { - // TODO: [cg_2020-03-10] replace print with more semantically weighted output that also makes CLI testable - print("Unknown default template '\(initTemplateName)' – use one of: [\(Constants.initTemplateCases)]") + log.message("Unknown default template '\(initTemplateName)' – use one of: [\(Constants.initTemplateCases)]", level: .error) exit(EXIT_FAILURE) } diff --git a/Sources/AnyLintCLI/Constants.swift b/Sources/AnyLintCLI/Globals/Constants.swift similarity index 67% rename from Sources/AnyLintCLI/Constants.swift rename to Sources/AnyLintCLI/Globals/Constants.swift index b855f0d..ab91d8e 100644 --- a/Sources/AnyLintCLI/Constants.swift +++ b/Sources/AnyLintCLI/Globals/Constants.swift @@ -1,7 +1,11 @@ import Foundation +var log = Logger(outputType: .console) + enum Constants { - static let defaultConfigurationPath: String = "AnyLint.swift" static let currentVersion: String = "0.1.0" + static let commandName: String = "anylint" + static let defaultConfigurationPath: String = "AnyLint.swift" static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") + static let toolName: String = "AnyLint" } diff --git a/Sources/AnyLintCLI/Globals/Logger.swift b/Sources/AnyLintCLI/Globals/Logger.swift new file mode 100644 index 0000000..3629e5d --- /dev/null +++ b/Sources/AnyLintCLI/Globals/Logger.swift @@ -0,0 +1,115 @@ +import Foundation +import Rainbow + +// swiftlint:disable logger + +final class Logger { + /// The print level type. + 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 type. + enum OutputType { + /// Output is targeted to a console to be read by developers. + case console + + /// Output is targeted to Xcode. Native support for Xcode Warnings & Errors. + case xcode + + /// Output is targeted for unit tests. Collect into globally accessible TestHelper. + case test + } + + let outputType: OutputType + + 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. + /// - file: The file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. + /// - line: The line within the file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. + func message(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { + switch outputType { + case .console: + consoleMessage(message, level: level, file: file, line: line) + + case .xcode: + xcodeMessage(message, level: level, file: file, line: line) + + case .test: + TestHelper.shared.consoleOutputs.append((message, level, file, line)) + } + } + + private func consoleMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { + let location = locationInfo(file: file, line: line)?.replacingOccurrences(of: FileManager.default.currentDirectoryPath, with: ".") + let message = location != nil ? [location!, message].joined(separator: " ") : message + + switch level { + case .success: + print(formattedCurrentDateTime(), "✅ ", message.lightGreen) + + case .info: + print(formattedCurrentDateTime(), "ℹ️ ", message.lightBlue) + + case .warning: + print(formattedCurrentDateTime(), "⚠️ ", message.yellow) + + case .error: + print(formattedCurrentDateTime(), "❌ ", message.lightRed) + } + } + + private func formattedCurrentDateTime() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + let dateTime = dateFormatter.string(from: Date()) + return "\(dateTime):" + } + + private func xcodeMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { + if let location = locationInfo(file: file, line: line) { + print(location, "\(level.rawValue): \(Constants.toolName): ", message) + } else { + print("\(level.rawValue): \(Constants.toolName): ", message) + } + } + + private func locationInfo(file: String?, line: Int?) -> String? { + guard let file = file else { return nil } + guard let line = line else { return "\(file): " } + return "\(file):\(line): " + } +} diff --git a/Sources/AnyLintCLI/Globals/TestHelper.swift b/Sources/AnyLintCLI/Globals/TestHelper.swift new file mode 100644 index 0000000..c15b7eb --- /dev/null +++ b/Sources/AnyLintCLI/Globals/TestHelper.swift @@ -0,0 +1,20 @@ +import Foundation + +/// A helper class for Unit Testing only. Only put data in here when `isStartedByUnitTests` is set to true. +/// Never read other data in framework than that property. +final class TestHelper { + typealias ConsoleOutput = (message: String, level: Logger.PrintLevel, file: String?, line: Int?) + + static let shared = TestHelper() + + /// Set to `true` within unit tests (in `setup()`). Defaults to `false`. + var isStartedByUnitTests: Bool = false + + /// Use only in Unit Tests. + var consoleOutputs: [ConsoleOutput] = [] + + /// Deletes all data collected until now. + func reset() { + consoleOutputs = [] + } +} diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift index 4b7676d..6d341eb 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -4,7 +4,6 @@ struct VersionTask { /* for extension purposes only */ } extension VersionTask: Task { func perform() { - // TODO: [cg_2020-03-10] replace print with more semantically weighted output that also makes CLI testable - print(Constants.currentVersion) + log.message(Constants.currentVersion, level: .info) } } diff --git a/Tests/AnyLintCLITests/AnyLintCLITests.swift b/Tests/AnyLintCLITests/AnyLintCLITests.swift index 8120a15..5be114c 100644 --- a/Tests/AnyLintCLITests/AnyLintCLITests.swift +++ b/Tests/AnyLintCLITests/AnyLintCLITests.swift @@ -1,4 +1,3 @@ -@testable import AnyLintCLI import XCTest final class AnyLintCLITests: XCTestCase { From 404f123a7113af44ca6c5375a3731e6664c4e66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 12 Mar 2020 00:33:30 +0100 Subject: [PATCH 08/27] Implement init and lint command basics --- AnyLint.swift | 4 ++ .../AnyLintCLI/Commands/SingleCommand.swift | 14 ++++--- .../AndroidTemplate.swift | 13 ++++++ .../BlankTemplate.swift | 13 ++++++ .../ConfigurationTemplate.swift | 5 +++ .../ConfigurationTemplates/IOSTemplate.swift | 13 ++++++ Sources/AnyLintCLI/Globals/Constants.swift | 3 +- .../Globals/Extensions/StringExt.swift | 30 ++++++++++++++ Sources/AnyLintCLI/Globals/Logger.swift | 12 +++--- Sources/AnyLintCLI/Tasks/InitTask.swift | 40 +++++++++++++++++-- Sources/AnyLintCLI/Tasks/LintTask.swift | 35 ++++++++++++++-- Sources/AnyLintCLI/Tasks/Task.swift | 5 --- Sources/AnyLintCLI/Tasks/TaskHandler.swift | 5 +++ Sources/AnyLintCLI/Tasks/VersionTask.swift | 4 +- 14 files changed, 168 insertions(+), 28 deletions(-) create mode 100755 AnyLint.swift create mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift create mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift create mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift create mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift create mode 100644 Sources/AnyLintCLI/Globals/Extensions/StringExt.swift delete mode 100644 Sources/AnyLintCLI/Tasks/Task.swift create mode 100644 Sources/AnyLintCLI/Tasks/TaskHandler.swift diff --git a/AnyLint.swift b/AnyLint.swift new file mode 100755 index 0000000..1da8398 --- /dev/null +++ b/AnyLint.swift @@ -0,0 +1,4 @@ +#!/usr/local/bin/swift-sh +import AnyLint // @Flinesoft ~> 0.1.0 + +// TODO: [cg_2020-03-11] not yet implemented diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index 0701121..2e81efb 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -21,11 +21,13 @@ class SingleCommand: Command { func execute() throws { // version subcommand if version { - VersionTask().perform() + try VersionTask().perform() exit(EXIT_SUCCESS) } - let configurationPaths = customPaths.isEmpty ? [Constants.defaultConfigurationPath] : customPaths + let configurationPaths = customPaths.isEmpty + ? [fileManager.currentDirectoryPath.appendingPathComponent(Constants.defaultConfigFileName)] + : customPaths // init subcommand if let initTemplateName = initTemplateName { @@ -34,15 +36,15 @@ class SingleCommand: Command { exit(EXIT_FAILURE) } - for path in configurationPaths { - InitTask(path: path, template: initTemplate).perform() + for configPath in configurationPaths { + try InitTask(configFilePath: configPath, template: initTemplate).perform() } exit(EXIT_SUCCESS) } // lint command - for path in configurationPaths { - LintTask(path: path).perform() + for configPath in configurationPaths { + try LintTask(configFilePath: configPath).perform() } exit(EXIT_SUCCESS) } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift new file mode 100644 index 0000000..58956a7 --- /dev/null +++ b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift @@ -0,0 +1,13 @@ +import Foundation + +// swiftlint:disable trailing_whitespace + +enum AndroidTemplate: ConfigurationTemplate { + static let fileContents: String = """ + #!/usr/local/bin/swift-sh + import AnyLint // @Flinesoft ~> \(Constants.currentVersion) + + // TODO: [cg_2020-03-11] not yet implemented + + """ +} diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift new file mode 100644 index 0000000..56b079d --- /dev/null +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -0,0 +1,13 @@ +import Foundation + +// swiftlint:disable trailing_whitespace + +enum BlankTemplate: ConfigurationTemplate { + static let fileContents: String = """ + #!/usr/local/bin/swift-sh + import AnyLint // @Flinesoft ~> \(Constants.currentVersion) + + // TODO: [cg_2020-03-11] not yet implemented + + """ +} diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift new file mode 100644 index 0000000..2c1e2df --- /dev/null +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol ConfigurationTemplate { + static var fileContents: String { get } +} diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift new file mode 100644 index 0000000..f80f9c1 --- /dev/null +++ b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift @@ -0,0 +1,13 @@ +import Foundation + +// swiftlint:disable trailing_whitespace + +enum IOSTemplate: ConfigurationTemplate { + static let fileContents: String = """ + #!/usr/local/bin/swift-sh + import AnyLint // @Flinesoft ~> \(Constants.currentVersion) + + // TODO: [cg_2020-03-11] not yet implemented + + """ +} diff --git a/Sources/AnyLintCLI/Globals/Constants.swift b/Sources/AnyLintCLI/Globals/Constants.swift index ab91d8e..43b9174 100644 --- a/Sources/AnyLintCLI/Globals/Constants.swift +++ b/Sources/AnyLintCLI/Globals/Constants.swift @@ -1,11 +1,12 @@ import Foundation +let fileManager = FileManager.default var log = Logger(outputType: .console) enum Constants { static let currentVersion: String = "0.1.0" static let commandName: String = "anylint" - static let defaultConfigurationPath: String = "AnyLint.swift" + static let defaultConfigFileName: String = "AnyLint.swift" static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") static let toolName: String = "AnyLint" } diff --git a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift new file mode 100644 index 0000000..1fc3ba6 --- /dev/null +++ b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift @@ -0,0 +1,30 @@ +import Foundation + +extension String { + var absolutePath: String { + guard let url = URL(string: self) else { + log.message("Could not convert path '\(self)' to type URL.", level: .error) + exit(EXIT_FAILURE) + } + + return url.absoluteString + } + + var parentDirectoryPath: String { + guard let url = URL(string: self) else { + log.message("Could not convert path '\(self)' to type URL.", level: .error) + exit(EXIT_FAILURE) + } + + return url.deletingLastPathComponent().absoluteString + } + + func appendingPathComponent(_ pathComponent: String) -> String { + guard let pathUrl = URL(string: self) else { + log.message("Could not convert path '\(self)' to type URL.", level: .error) + exit(EXIT_FAILURE) + } + + return pathUrl.appendingPathComponent(pathComponent).absoluteString + } +} diff --git a/Sources/AnyLintCLI/Globals/Logger.swift b/Sources/AnyLintCLI/Globals/Logger.swift index 3629e5d..304e034 100644 --- a/Sources/AnyLintCLI/Globals/Logger.swift +++ b/Sources/AnyLintCLI/Globals/Logger.swift @@ -79,22 +79,22 @@ final class Logger { switch level { case .success: - print(formattedCurrentDateTime(), "✅ ", message.lightGreen) + print(formattedCurrentTime(), "✅ ", message.lightGreen) case .info: - print(formattedCurrentDateTime(), "ℹ️ ", message.lightBlue) + print(formattedCurrentTime(), "ℹ️ ", message.lightBlue) case .warning: - print(formattedCurrentDateTime(), "⚠️ ", message.yellow) + print(formattedCurrentTime(), "⚠️ ", message.yellow) case .error: - print(formattedCurrentDateTime(), "❌ ", message.lightRed) + print(formattedCurrentTime(), "❌ ", message.lightRed) } } - private func formattedCurrentDateTime() -> String { + private func formattedCurrentTime() -> String { let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + dateFormatter.dateFormat = "HH:mm:ss.SSS" let dateTime = dateFormatter.string(from: Date()) return "\(dateTime):" } diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index ec3d1af..deee3eb 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -1,18 +1,50 @@ import Foundation +import SwiftCLI struct InitTask { enum Template: String, CaseIterable { case blank case ios case android + + var configFileContents: String { + switch self { + case .blank: + return BlankTemplate.fileContents + + case .android: + return AndroidTemplate.fileContents + + case .ios: + return IOSTemplate.fileContents + } + } } - let path: String + let configFilePath: String let template: Template } -extension InitTask: Task { - func perform() { - // TODO: [cg_2020-03-10] not yet implemented +extension InitTask: TaskHandler { + func perform() throws { + guard !fileManager.fileExists(atPath: configFilePath) else { + log.message("Configuration file already exists at path '\(configFilePath)'.", level: .error) + exit(EXIT_FAILURE) + } + + 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 6a238c3..b86feca 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -1,11 +1,38 @@ import Foundation +import SwiftCLI struct LintTask { - let path: String + let configFilePath: String } -extension LintTask: Task { - func perform() { - // TODO: [cg_2020-03-10] not yet implemented +extension LintTask: TaskHandler { + func perform() throws { + guard fileManager.fileExists(atPath: configFilePath) else { + log.message( + "No configuration file found at \(configFilePath) – consider running `\(Constants.commandName) --init` with a template.", + level: .error + ) + exit(EXIT_FAILURE) + } + + if !fileManager.isExecutableFile(atPath: configFilePath) { + try Task.run(bash: "chmod +x '\(configFilePath)'") + } + + do { + try Task.run(bash: "which swift-sh") + } catch is RunError { + log.message("swift-sh not installed – please follow instructions on https://github.com/mxcl/swift-sh#installation to install.", level: .error) + exit(EXIT_FAILURE) + } + + do { + log.message("Start linting using config file at \(configFilePath) ...", level: .info) + try Task.run(bash: "\(configFilePath.absolutePath)") + log.message("Successfully linted without errors using config file at \(configFilePath). Congrats! 🎉", level: .success) + } catch is RunError { + log.message("Linting failed using config file at \(configFilePath).", level: .error) + exit(EXIT_FAILURE) + } } } diff --git a/Sources/AnyLintCLI/Tasks/Task.swift b/Sources/AnyLintCLI/Tasks/Task.swift deleted file mode 100644 index 7174104..0000000 --- a/Sources/AnyLintCLI/Tasks/Task.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -protocol Task { - func perform() -} diff --git a/Sources/AnyLintCLI/Tasks/TaskHandler.swift b/Sources/AnyLintCLI/Tasks/TaskHandler.swift new file mode 100644 index 0000000..9986c03 --- /dev/null +++ b/Sources/AnyLintCLI/Tasks/TaskHandler.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol TaskHandler { + func perform() throws +} diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift index 6d341eb..d4f4764 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -2,8 +2,8 @@ import Foundation struct VersionTask { /* for extension purposes only */ } -extension VersionTask: Task { - func perform() { +extension VersionTask: TaskHandler { + func perform() throws { log.message(Constants.currentVersion, level: .info) } } From 084791aae60acf8d618bdcbcc177bbfba3338a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 12 Mar 2020 07:57:38 +0100 Subject: [PATCH 09/27] Create configuration file for this project & fix issues --- Sources/AnyLintCLI/Globals/Constants.swift | 2 +- Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift | 7 +++++++ Sources/AnyLintCLI/Globals/Extensions/StringExt.swift | 2 +- AnyLint.swift => lint.swift | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift rename AnyLint.swift => lint.swift (65%) diff --git a/Sources/AnyLintCLI/Globals/Constants.swift b/Sources/AnyLintCLI/Globals/Constants.swift index 43b9174..2023ffe 100644 --- a/Sources/AnyLintCLI/Globals/Constants.swift +++ b/Sources/AnyLintCLI/Globals/Constants.swift @@ -6,7 +6,7 @@ var log = Logger(outputType: .console) enum Constants { static let currentVersion: String = "0.1.0" static let commandName: String = "anylint" - static let defaultConfigFileName: String = "AnyLint.swift" + static let defaultConfigFileName: String = "lint.swift" static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") static let toolName: String = "AnyLint" } diff --git a/Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift b/Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift new file mode 100644 index 0000000..c825739 --- /dev/null +++ b/Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift @@ -0,0 +1,7 @@ +import Foundation + +extension FileManager { + var currentDirectoryUrl: URL { + URL(string: currentDirectoryPath)! + } +} diff --git a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift index 1fc3ba6..9002c89 100644 --- a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift +++ b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift @@ -2,7 +2,7 @@ import Foundation extension String { var absolutePath: String { - guard let url = URL(string: self) else { + guard let url = URL(string: self, relativeTo: fileManager.currentDirectoryUrl) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) exit(EXIT_FAILURE) } diff --git a/AnyLint.swift b/lint.swift similarity index 65% rename from AnyLint.swift rename to lint.swift index 1da8398..675bfb4 100755 --- a/AnyLint.swift +++ b/lint.swift @@ -1,4 +1,4 @@ #!/usr/local/bin/swift-sh -import AnyLint // @Flinesoft ~> 0.1.0 +import AnyLint // . // TODO: [cg_2020-03-11] not yet implemented From c48303a19107cf098a9e25764d815159d8f554f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 13 Mar 2020 08:33:25 +0100 Subject: [PATCH 10/27] Implement basic stubs for AnyLint library --- .swiftlint.yml | 4 +- Package.swift | 9 ++- Sources/AnyLint/AnyLint.swift | 5 -- Sources/AnyLint/CheckInfo.swift | 29 +++++++ Sources/AnyLint/Lint.swift | 79 +++++++++++++++++++ Sources/AnyLint/Severity.swift | 13 +++ Sources/AnyLint/Statistics.swift | 18 +++++ .../ViolationTypes/FileContentViolation.swift | 20 +++++ .../AnyLint/ViolationTypes/Violation.swift | 29 +++++++ .../AnyLintCLI/Commands/SingleCommand.swift | 20 +++-- .../AndroidTemplate.swift | 7 +- .../BlankTemplate.swift | 7 +- .../ConfigurationTemplate.swift | 2 +- .../ConfigurationTemplates/IOSTemplate.swift | 11 +-- .../{Constants.swift => CLIConstants.swift} | 8 +- .../Globals/Extensions/StringExt.swift | 1 + Sources/AnyLintCLI/Globals/TestHelper.swift | 20 ----- .../AnyLintCLI/Globals/ValidateOrFail.swift | 29 +++++++ Sources/AnyLintCLI/Tasks/EditTask.swift | 17 ++++ Sources/AnyLintCLI/Tasks/InitTask.swift | 7 +- Sources/AnyLintCLI/Tasks/LintTask.swift | 23 +++--- Sources/AnyLintCLI/Tasks/VersionTask.swift | 1 + Sources/Utility/Constants.swift | 16 ++++ .../Globals => Utility}/Logger.swift | 22 +++--- Sources/Utility/TestHelper.swift | 22 ++++++ Tests/UtilityTests/UtilityTests.swift | 7 ++ 26 files changed, 348 insertions(+), 78 deletions(-) delete mode 100644 Sources/AnyLint/AnyLint.swift create mode 100644 Sources/AnyLint/CheckInfo.swift create mode 100644 Sources/AnyLint/Lint.swift create mode 100644 Sources/AnyLint/Severity.swift create mode 100644 Sources/AnyLint/Statistics.swift create mode 100644 Sources/AnyLint/ViolationTypes/FileContentViolation.swift create mode 100644 Sources/AnyLint/ViolationTypes/Violation.swift rename Sources/AnyLintCLI/Globals/{Constants.swift => CLIConstants.swift} (56%) delete mode 100644 Sources/AnyLintCLI/Globals/TestHelper.swift create mode 100644 Sources/AnyLintCLI/Globals/ValidateOrFail.swift create mode 100644 Sources/AnyLintCLI/Tasks/EditTask.swift create mode 100644 Sources/Utility/Constants.swift rename Sources/{AnyLintCLI/Globals => Utility}/Logger.swift (79%) create mode 100644 Sources/Utility/TestHelper.swift create mode 100644 Tests/UtilityTests/UtilityTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 089fdf4..5a68e87 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -108,7 +108,9 @@ identifier_name: - db - to -line_length: 160 +line_length: + warning: 160 + ignores_comments: true nesting: type_level: 3 diff --git a/Package.swift b/Package.swift index a7fdcab..1590acb 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: [ .target( name: "AnyLint", - dependencies: ["HandySwift"] + dependencies: ["HandySwift", "Utility"] ), .testTarget( name: "AnyLintTests", @@ -23,11 +23,16 @@ let package = Package( ), .target( name: "AnyLintCLI", - dependencies: ["HandySwift", "Rainbow", "SwiftCLI"] + dependencies: ["HandySwift", "Rainbow", "SwiftCLI", "Utility"] ), .testTarget( name: "AnyLintCLITests", dependencies: ["AnyLintCLI"] ), + .target( + name: "Utility", + dependencies: ["Rainbow"] + ), + .testTarget(name: "UtilityTests") ] ) diff --git a/Sources/AnyLint/AnyLint.swift b/Sources/AnyLint/AnyLint.swift deleted file mode 100644 index 3ff07c7..0000000 --- a/Sources/AnyLint/AnyLint.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -enum AnyLint { - // TODO: [cg_2020-03-07] not yet implemented -} diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift new file mode 100644 index 0000000..8735d13 --- /dev/null +++ b/Sources/AnyLint/CheckInfo.swift @@ -0,0 +1,29 @@ +import Foundation +import HandySwift +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 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 + + func consoleDescription(match: Regex.Match?) -> String { + if let match = match { + return "" // TODO: [cg_2020-03-12] not yet implemented + } else { + return "\(id) – \(hint)" + } + } +} + +extension CheckInfo: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift new file mode 100644 index 0000000..9ccb6d7 --- /dev/null +++ b/Sources/AnyLint/Lint.swift @@ -0,0 +1,79 @@ +import Foundation +import HandySwift + +/// 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'. + /// - includedFileRegexes: An array of regexes defining which files should be incuded in the check. Will check all files matching any of the given regexes. + /// - excludedFileRegexes: 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 `contentRegex` to use for autocorrection. + /// - triggeringExamples: An array of example contents where the `contentRegex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. + /// - nonTriggeringExamples: An array of examples contents where the `contentRegex` is expected not to trigger. + public static func checkFileContents( + checkInfo: CheckInfo, + regex: Regex, + includedFileRegexes: [Regex], + excludedFileRegexes: [Regex] = [], + autoCorrectReplacement: String? = nil, + triggeringExamples: [String] = [], + nonTriggeringExamples: [String] = [] + ) { + var violations: [Violation] = [] + + // TODO: [cg_2020-03-12] not yet implemented + + 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'. + /// - includedFileRegexes: Defines which files should be incuded in check. Checks all files matching any of the given regexes. + /// - excludedFileRegexes: 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 `contentRegex` to use for autocorrection. + /// - triggeringExamples: An array of example contents where the `contentRegex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. + /// - nonTriggeringExamples: An array of examples contents where the `contentRegex` is expected not to trigger. + public static func checkFilePaths( + checkInfo: CheckInfo, + regex: Regex, + includedFileRegexes: [Regex], + excludedFileRegexes: [Regex] = [], + autoCorrectReplacement: String? = nil, + triggeringExamples: [String] = [], + nonTriggeringExamples: [String] = [] + ) { + var violations: [Violation] = [] + + // TODO: [cg_2020-03-12] not yet implemented + + Statistics.shared.found(violations: violations, in: checkInfo) + } + + /// Checks the last commit message. + /// + /// - Parameters: + /// - checkInfo: The info object providing some general information on the lint check. + /// - regex: The regex to use for matching the commit message. + public static func checkLastCommitMessage(checkInfo: CheckInfo, regex: Regex) { + var violations: [Violation] = [] + + // TODO: [cg_2020-03-12] not yet implemented + + 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: () -> [Violation]) { + Statistics.shared.found(violations: customClosure(), in: checkInfo) + } +} diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift new file mode 100644 index 0000000..1b7980b --- /dev/null +++ b/Sources/AnyLint/Severity.swift @@ -0,0 +1,13 @@ +import Foundation + +/// Defines the severity of a lint check. +public enum Severity { + /// 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 +} diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift new file mode 100644 index 0000000..018f8ba --- /dev/null +++ b/Sources/AnyLint/Statistics.swift @@ -0,0 +1,18 @@ +import Foundation +import Utility + +final class Statistics { + static let shared = Statistics() + + var executedChecks: [CheckInfo] = [] + var allViolations: [Violation] = [] + var violationsPerCheck: [CheckInfo: [Violation]] = [:] + + private init() {} + + func found(violations: [Violation], in check: CheckInfo) { + executedChecks.append(check) + allViolations.append(contentsOf: violations) + violationsPerCheck[check] = violations + } +} diff --git a/Sources/AnyLint/ViolationTypes/FileContentViolation.swift b/Sources/AnyLint/ViolationTypes/FileContentViolation.swift new file mode 100644 index 0000000..05fcdf7 --- /dev/null +++ b/Sources/AnyLint/ViolationTypes/FileContentViolation.swift @@ -0,0 +1,20 @@ +import Foundation + +open class FileContentViolation: Violation { + /// The file path. + public let filePath: String + + /// The line number of the violation. + public let lineNum: Int + + /// The character within the violations line. + public let charInLine: Int + + public init(checkInfo: CheckInfo, filePath: String, lineNum: Int, charInLine: Int) { + self.filePath = filePath + self.lineNum = lineNum + self.charInLine = charInLine + + super.init(checkInfo: checkInfo) + } +} diff --git a/Sources/AnyLint/ViolationTypes/Violation.swift b/Sources/AnyLint/ViolationTypes/Violation.swift new file mode 100644 index 0000000..1a54120 --- /dev/null +++ b/Sources/AnyLint/ViolationTypes/Violation.swift @@ -0,0 +1,29 @@ +import Foundation +import HandySwift +import Utility + +/// A violation found in a check. +open class Violation { + /// The info about the chack that caused this violation. + public let checkInfo: CheckInfo + + /// Create a new violation. + public init(checkInfo: CheckInfo) { + self.checkInfo = checkInfo + } + + func logMessage(match: Regex.Match?) { + let message = "\(checkInfo.consoleDescription(match: match))" + + switch checkInfo.severity { + case .info: + log.message(message, level: .info) + + case .warning: + log.message(message, level: .warning) + + case .error: + log.message(message, level: .error) + } + } +} diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index 2e81efb..45b5014 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -1,16 +1,17 @@ import Foundation import SwiftCLI +import Utility class SingleCommand: Command { // MARK: - Basics - var name: String = Constants.commandName + var name: String = CLIConstants.commandName var shortDescription: String = "Lint anything by combining the power of Swift & regular expressions." // MARK: - Subcommands @Flag("-v", "--version", description: "Print the current tool version") var version: Bool - @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(Constants.initTemplateCases)]") + @Key("-i", "--init", description: "Configure AnyLint with a default template. Has to be one of: [\(CLIConstants.initTemplateCases)]") var initTemplateName: String? // MARK: - Options @@ -26,13 +27,13 @@ class SingleCommand: Command { } let configurationPaths = customPaths.isEmpty - ? [fileManager.currentDirectoryPath.appendingPathComponent(Constants.defaultConfigFileName)] + ? [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: [\(Constants.initTemplateCases)]", level: .error) + log.message("Unknown default template '\(initTemplateName)' – use one of: [\(CLIConstants.initTemplateCases)]", level: .error) exit(EXIT_FAILURE) } @@ -42,10 +43,15 @@ class SingleCommand: Command { exit(EXIT_SUCCESS) } - // lint command + // lint main command + var anyConfigFileFailed = false for configPath in configurationPaths { - try LintTask(configFilePath: configPath).perform() + do { + try LintTask(configFilePath: configPath).perform() + } catch LintTask.LintError.configFileFailed { + anyConfigFileFailed = true + } } - exit(EXIT_SUCCESS) + exit(anyConfigFileFailed ? EXIT_FAILURE : EXIT_SUCCESS) } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift index 58956a7..42705d7 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift @@ -1,13 +1,16 @@ import Foundation +import Utility // swiftlint:disable trailing_whitespace enum AndroidTemplate: ConfigurationTemplate { - static let fileContents: String = """ - #!/usr/local/bin/swift-sh + static func fileContents() -> String { + """ + #!/usr/local/bin/\(CLIConstants.swiftShCommand) import AnyLint // @Flinesoft ~> \(Constants.currentVersion) // TODO: [cg_2020-03-11] not yet implemented """ + } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 56b079d..18a608d 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -1,13 +1,16 @@ import Foundation +import Utility // swiftlint:disable trailing_whitespace enum BlankTemplate: ConfigurationTemplate { - static let fileContents: String = """ - #!/usr/local/bin/swift-sh + static func fileContents() -> String { + """ + #!/usr/local/bin/\(CLIConstants.swiftShCommand) import AnyLint // @Flinesoft ~> \(Constants.currentVersion) // TODO: [cg_2020-03-11] not yet implemented """ + } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index 2c1e2df..294600d 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -1,5 +1,5 @@ import Foundation protocol ConfigurationTemplate { - static var fileContents: String { get } + static func fileContents() -> String } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift index f80f9c1..961e273 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift @@ -1,13 +1,14 @@ import Foundation - -// swiftlint:disable trailing_whitespace +import Utility enum IOSTemplate: ConfigurationTemplate { - static let fileContents: String = """ - #!/usr/local/bin/swift-sh + static func fileContents() -> String { + """ + #!/usr/local/bin/\(CLIConstants.swiftShCommand) import AnyLint // @Flinesoft ~> \(Constants.currentVersion) // TODO: [cg_2020-03-11] not yet implemented - + """ + } } diff --git a/Sources/AnyLintCLI/Globals/Constants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift similarity index 56% rename from Sources/AnyLintCLI/Globals/Constants.swift rename to Sources/AnyLintCLI/Globals/CLIConstants.swift index 2023ffe..e8890e9 100644 --- a/Sources/AnyLintCLI/Globals/Constants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -1,12 +1,8 @@ import Foundation -let fileManager = FileManager.default -var log = Logger(outputType: .console) - -enum Constants { - static let currentVersion: String = "0.1.0" +enum CLIConstants { static let commandName: String = "anylint" static let defaultConfigFileName: String = "lint.swift" static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") - static let toolName: String = "AnyLint" + static let swiftShCommand: String = "swift-sh" } diff --git a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift index 9002c89..f3de5b3 100644 --- a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift +++ b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift @@ -1,4 +1,5 @@ import Foundation +import Utility extension String { var absolutePath: String { diff --git a/Sources/AnyLintCLI/Globals/TestHelper.swift b/Sources/AnyLintCLI/Globals/TestHelper.swift deleted file mode 100644 index c15b7eb..0000000 --- a/Sources/AnyLintCLI/Globals/TestHelper.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -/// A helper class for Unit Testing only. Only put data in here when `isStartedByUnitTests` is set to true. -/// Never read other data in framework than that property. -final class TestHelper { - typealias ConsoleOutput = (message: String, level: Logger.PrintLevel, file: String?, line: Int?) - - static let shared = TestHelper() - - /// Set to `true` within unit tests (in `setup()`). Defaults to `false`. - var isStartedByUnitTests: Bool = false - - /// Use only in Unit Tests. - var consoleOutputs: [ConsoleOutput] = [] - - /// Deletes all data collected until now. - func reset() { - consoleOutputs = [] - } -} diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift new file mode 100644 index 0000000..4e68038 --- /dev/null +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -0,0 +1,29 @@ +import Foundation +import SwiftCLI +import Utility + +enum ValidateOrFail { + /// Fails if swift-sh is not installed. Returns the install path if it is installed. + @discardableResult + static func swiftShInstalled() throws -> String { + do { + return try Task.capture(bash: "which \(CLIConstants.swiftShCommand)").stdout + } catch is CaptureError { + log.message( + "\(CLIConstants.swiftShCommand) not installed – please follow instructions on https://github.com/mxcl/swift-sh#installation to install.", + level: .error + ) + exit(EXIT_FAILURE) + } + } + + 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 + ) + exit(EXIT_FAILURE) + } + } +} diff --git a/Sources/AnyLintCLI/Tasks/EditTask.swift b/Sources/AnyLintCLI/Tasks/EditTask.swift new file mode 100644 index 0000000..553036f --- /dev/null +++ b/Sources/AnyLintCLI/Tasks/EditTask.swift @@ -0,0 +1,17 @@ +import Foundation +import SwiftCLI +import Utility + +struct EditTask { + let configFilePath: String +} + +extension EditTask: TaskHandler { + func perform() throws { + try ValidateOrFail.configFileExists(at: configFilePath) + try ValidateOrFail.swiftShInstalled() + + log.message("Opening config file at \(configFilePath) in Xcode to edit ...", level: .info) + try Task.run(bash: "\(CLIConstants.swiftShCommand) edit '\(configFilePath)'") + } +} diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index deee3eb..ab247bd 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -1,5 +1,6 @@ import Foundation import SwiftCLI +import Utility struct InitTask { enum Template: String, CaseIterable { @@ -10,13 +11,13 @@ struct InitTask { var configFileContents: String { switch self { case .blank: - return BlankTemplate.fileContents + return BlankTemplate.fileContents() case .android: - return AndroidTemplate.fileContents + return AndroidTemplate.fileContents() case .ios: - return IOSTemplate.fileContents + return IOSTemplate.fileContents() } } } diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index b86feca..9cf9794 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -1,30 +1,25 @@ import Foundation import SwiftCLI +import Utility struct LintTask { let configFilePath: String } extension LintTask: TaskHandler { + enum LintError: Error { + case configFileFailed + } + + /// - Throws: `LintError.configFileFailed` if running a configuration file fails func perform() throws { - guard fileManager.fileExists(atPath: configFilePath) else { - log.message( - "No configuration file found at \(configFilePath) – consider running `\(Constants.commandName) --init` with a template.", - level: .error - ) - exit(EXIT_FAILURE) - } + try ValidateOrFail.configFileExists(at: configFilePath) if !fileManager.isExecutableFile(atPath: configFilePath) { try Task.run(bash: "chmod +x '\(configFilePath)'") } - do { - try Task.run(bash: "which swift-sh") - } catch is RunError { - log.message("swift-sh not installed – please follow instructions on https://github.com/mxcl/swift-sh#installation to install.", level: .error) - exit(EXIT_FAILURE) - } + try ValidateOrFail.swiftShInstalled() do { log.message("Start linting using config file at \(configFilePath) ...", level: .info) @@ -32,7 +27,7 @@ extension LintTask: TaskHandler { log.message("Successfully linted without errors using config file at \(configFilePath). Congrats! 🎉", level: .success) } catch is RunError { log.message("Linting failed using config file at \(configFilePath).", level: .error) - exit(EXIT_FAILURE) + throw LintError.configFileFailed } } } diff --git a/Sources/AnyLintCLI/Tasks/VersionTask.swift b/Sources/AnyLintCLI/Tasks/VersionTask.swift index d4f4764..e043f26 100644 --- a/Sources/AnyLintCLI/Tasks/VersionTask.swift +++ b/Sources/AnyLintCLI/Tasks/VersionTask.swift @@ -1,4 +1,5 @@ import Foundation +import Utility struct VersionTask { /* for extension purposes only */ } diff --git a/Sources/Utility/Constants.swift b/Sources/Utility/Constants.swift new file mode 100644 index 0000000..d8b5041 --- /dev/null +++ b/Sources/Utility/Constants.swift @@ -0,0 +1,16 @@ +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.1.0" + + /// The name of this tool. + public static let toolName: String = "AnyLint" +} diff --git a/Sources/AnyLintCLI/Globals/Logger.swift b/Sources/Utility/Logger.swift similarity index 79% rename from Sources/AnyLintCLI/Globals/Logger.swift rename to Sources/Utility/Logger.swift index 304e034..cf26824 100644 --- a/Sources/AnyLintCLI/Globals/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -3,9 +3,10 @@ import Rainbow // swiftlint:disable logger -final class Logger { +/// Helper to log output to console or elsewhere. +public final class Logger { /// The print level type. - enum PrintLevel: String { + public enum PrintLevel: String { /// Print success information. case success @@ -36,7 +37,7 @@ final class Logger { } /// The output type. - enum OutputType { + public enum OutputType { /// Output is targeted to a console to be read by developers. case console @@ -60,7 +61,7 @@ final class Logger { /// - level: The level of the print statement. /// - file: The file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. /// - line: The line within the file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. - func message(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { + public func message(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { switch outputType { case .console: consoleMessage(message, level: level, file: file, line: line) @@ -73,8 +74,8 @@ final class Logger { } } - private func consoleMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { - let location = locationInfo(file: file, line: line)?.replacingOccurrences(of: FileManager.default.currentDirectoryPath, with: ".") + private func consoleMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil, charInLine: Int? = nil) { + let location = locationInfo(file: file, line: line, charInLine: charInLine)?.replacingOccurrences(of: fileManager.currentDirectoryPath, with: ".") let message = location != nil ? [location!, message].joined(separator: " ") : message switch level { @@ -99,17 +100,18 @@ final class Logger { return "\(dateTime):" } - private func xcodeMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { - if let location = locationInfo(file: file, line: line) { + private func xcodeMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil, charInLine: Int? = nil) { + if let location = locationInfo(file: file, line: line, charInLine: charInLine) { print(location, "\(level.rawValue): \(Constants.toolName): ", message) } else { print("\(level.rawValue): \(Constants.toolName): ", message) } } - private func locationInfo(file: String?, line: Int?) -> String? { + private func locationInfo(file: String?, line: Int?, charInLine: Int?) -> String? { guard let file = file else { return nil } guard let line = line else { return "\(file): " } - return "\(file):\(line): " + guard let charInLine = charInLine else { return "\(file):\(line): " } + return "\(file):\(line):\(charInLine): " } } diff --git a/Sources/Utility/TestHelper.swift b/Sources/Utility/TestHelper.swift new file mode 100644 index 0000000..b1fcefd --- /dev/null +++ b/Sources/Utility/TestHelper.swift @@ -0,0 +1,22 @@ +import Foundation + +/// A helper class for Unit Testing only. Only put data in here when `isStartedByUnitTests` is set to true. +/// Never read other data in framework than that property. +public final class TestHelper { + /// The console output data. + public typealias ConsoleOutput = (message: String, level: Logger.PrintLevel, file: String?, line: Int?) + + /// The shared `TestHelper` object. + public static let shared = TestHelper() + + /// Set to `true` within unit tests (in `setup()`). Defaults to `false`. + public var isStartedByUnitTests: Bool = false + + /// Use only in Unit Tests. + public var consoleOutputs: [ConsoleOutput] = [] + + /// Deletes all data collected until now. + public func reset() { + consoleOutputs = [] + } +} diff --git a/Tests/UtilityTests/UtilityTests.swift b/Tests/UtilityTests/UtilityTests.swift new file mode 100644 index 0000000..a4764fb --- /dev/null +++ b/Tests/UtilityTests/UtilityTests.swift @@ -0,0 +1,7 @@ +import XCTest + +final class UtilityTests: XCTestCase { + func testExample() { + // TODO: [cg_2020-03-12] not yet implemented + } +} From cab03c9c558ba9bae10933e986f4dc4a40c2b8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 13 Mar 2020 09:22:40 +0100 Subject: [PATCH 11/27] Implement basic file contents check with stubs --- Sources/AnyLint/Extensions/StringExt.swift | 9 +++ Sources/AnyLint/FilesSearch.swift | 9 +++ Sources/AnyLint/Lint.swift | 85 +++++++++++++++++----- 3 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 Sources/AnyLint/Extensions/StringExt.swift create mode 100644 Sources/AnyLint/FilesSearch.swift diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift new file mode 100644 index 0000000..a81a642 --- /dev/null +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -0,0 +1,9 @@ +import Foundation + +extension String { + typealias LocationInfo = (line: Int, charInLine: Int) + + func locationInfo(of index: String.Index) -> LocationInfo { + (line: 0, charInLine: 0) // TODO: [cg_2020-03-13] not yet implemented + } +} diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift new file mode 100644 index 0000000..edae509 --- /dev/null +++ b/Sources/AnyLint/FilesSearch.swift @@ -0,0 +1,9 @@ +import Foundation +import HandySwift + +/// Helper to search for files and filter using Regexes. +public enum FilesSearch { + static func allFiles(within path: String, includeFilters: [Regex], excludeFilters: [Regex] = []) -> [String] { + [] // TODO: [cg_2020-03-13] not yet implemented + } +} diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 9ccb6d7..a5f9dfb 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -1,5 +1,6 @@ import Foundation import HandySwift +import Utility /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { @@ -8,23 +9,49 @@ public enum Lint { /// - 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'. - /// - includedFileRegexes: An array of regexes defining which files should be incuded in the check. Will check all files matching any of the given regexes. - /// - excludedFileRegexes: 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 `contentRegex` to use for autocorrection. - /// - triggeringExamples: An array of example contents where the `contentRegex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. - /// - nonTriggeringExamples: An array of examples contents where the `contentRegex` 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. + /// - 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. public static func checkFileContents( checkInfo: CheckInfo, regex: Regex, - includedFileRegexes: [Regex], - excludedFileRegexes: [Regex] = [], + includeFilters: [Regex] = [], + excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, - triggeringExamples: [String] = [], - nonTriggeringExamples: [String] = [] + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [] ) { + // TODO: [cg_2020-03-13] validate matching and non-matching examples first + var violations: [Violation] = [] + let filePathsToCheck: [String] = FilesSearch.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) - // TODO: [cg_2020-03-12] not yet implemented + for filePath in filePathsToCheck { + if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { + for match in regex.matches(in: fileContents) { + // TODO: [cg_2020-03-13] use capture group named 'pointer' if exists + let locationInfo = fileContents.locationInfo(of: match.range.lowerBound) + + // TODO: [cg_2020-03-13] autocorrect if autocorrection is available + violations.append( + FileContentViolation( + checkInfo: checkInfo, + filePath: filePath, + lineNum: locationInfo.line, + charInLine: locationInfo.charInLine + ) + ) + } + } 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.found(violations: violations, in: checkInfo) } @@ -34,21 +61,30 @@ public enum Lint { /// - 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'. - /// - includedFileRegexes: Defines which files should be incuded in check. Checks all files matching any of the given regexes. - /// - excludedFileRegexes: 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 `contentRegex` to use for autocorrection. - /// - triggeringExamples: An array of example contents where the `contentRegex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. - /// - nonTriggeringExamples: An array of examples contents where the `contentRegex` 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. + /// - 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. + /// - 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, - includedFileRegexes: [Regex], - excludedFileRegexes: [Regex] = [], + includeFilters: [Regex] = [], + excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, - triggeringExamples: [String] = [], - nonTriggeringExamples: [String] = [] + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [], + violateIfNoMatchesFound: Bool = false ) { + // TODO: [cg_2020-03-13] validate matching and non-matching examples first + var violations: [Violation] = [] + let filePathsToCheck: [String] = FilesSearch.allFiles( + within: fileManager.currentDirectoryPath, + includeFilters: includeFilters, + excludeFilters: excludeFilters + ) // TODO: [cg_2020-03-12] not yet implemented @@ -60,7 +96,16 @@ public enum Lint { /// - Parameters: /// - checkInfo: The info object providing some general information on the lint check. /// - regex: The regex to use for matching the commit message. - public static func checkLastCommitMessage(checkInfo: CheckInfo, regex: Regex) { + /// - matchingExamples: An array of example messages where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. + /// - nonMatchingExamples: An array of example messages where the `regex` is expected not to trigger. + public static func checkLastCommitMessage( + checkInfo: CheckInfo, + regex: Regex, + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [] + ) { + // TODO: [cg_2020-03-13] validate matching and non-matching examples first + var violations: [Violation] = [] // TODO: [cg_2020-03-12] not yet implemented From 8ba78e68cb2a81956d47c482c8991c192541365a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Mar 2020 04:28:34 +0100 Subject: [PATCH 12/27] Implement most logic without tests --- .swiftlint.yml | 6 -- Package.swift | 2 +- Sources/AnyLint/CheckInfo.swift | 8 -- Sources/AnyLint/Checkers/Checker.swift | 5 ++ .../Checkers/FileContentsChecker.swift | 40 +++++++++ .../AnyLint/Checkers/FilePathsChecker.swift | 47 ++++++++++ .../Checkers/LastCommitMessageChecker.swift | 13 +++ Sources/AnyLint/Extensions/StringExt.swift | 9 +- Sources/AnyLint/FilesSearch.swift | 55 +++++++++++- Sources/AnyLint/Lint.swift | 88 ++++++++++++------- Sources/AnyLint/Severity.swift | 16 +++- Sources/AnyLint/Statistics.swift | 24 ++++- .../ViolationTypes/FileContentViolation.swift | 20 ----- .../AnyLint/ViolationTypes/Violation.swift | 30 ++++--- .../AndroidTemplate.swift | 4 +- .../BlankTemplate.swift | 4 +- .../ConfigurationTemplates/IOSTemplate.swift | 4 +- Sources/AnyLintCLI/Globals/CLIConstants.swift | 2 +- .../AnyLintCLI/Globals/ValidateOrFail.swift | 11 +-- Sources/AnyLintCLI/Tasks/EditTask.swift | 4 +- Sources/AnyLintCLI/Tasks/LintTask.swift | 4 +- .../Utility/Extensions/CollectionExt.swift | 8 ++ .../Extensions/FileManagerExt.swift | 3 +- Sources/Utility/Extensions/RegexExt.swift | 13 +++ lint.swift | 2 + 25 files changed, 318 insertions(+), 104 deletions(-) create mode 100644 Sources/AnyLint/Checkers/Checker.swift create mode 100644 Sources/AnyLint/Checkers/FileContentsChecker.swift create mode 100644 Sources/AnyLint/Checkers/FilePathsChecker.swift create mode 100644 Sources/AnyLint/Checkers/LastCommitMessageChecker.swift delete mode 100644 Sources/AnyLint/ViolationTypes/FileContentViolation.swift create mode 100644 Sources/Utility/Extensions/CollectionExt.swift rename Sources/{AnyLintCLI/Globals => Utility}/Extensions/FileManagerExt.swift (54%) create mode 100644 Sources/Utility/Extensions/RegexExt.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 5a68e87..49876de 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -261,12 +261,6 @@ custom_rules: name: "Multiline Guard Start" message: "Always start a multiline guard via `guard` then a line break and all expressions indented." severity: warning - multiple_closure_params: - included: ".*.swift" - regex: '\n *(?:[^\.\n=]+\.)+[^\(\s]+\([^\{\n]+\{[^\}\n]+\}\)\s*\{' - name: "Multiple Closure Params" - message: "Don't use multiple in-line closures – save one or more of them to variables instead." - severity: warning none_case_enum: included: ".*.swift" regex: 'enum\s+[^\{]+\{(?:\s*\/\/[^\n]*)*(?:\s*case\s+[^\n]+)*\s*case\s+none[^\S]' diff --git a/Package.swift b/Package.swift index 1590acb..151dd37 100644 --- a/Package.swift +++ b/Package.swift @@ -31,7 +31,7 @@ let package = Package( ), .target( name: "Utility", - dependencies: ["Rainbow"] + dependencies: ["HandySwift", "Rainbow"] ), .testTarget(name: "UtilityTests") ] diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index 8735d13..a7c6e22 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -12,14 +12,6 @@ public struct CheckInfo { /// The severity level for the report in case the check fails. public let severity: Severity - - func consoleDescription(match: Regex.Match?) -> String { - if let match = match { - return "" // TODO: [cg_2020-03-12] not yet implemented - } else { - return "\(id) – \(hint)" - } - } } extension CheckInfo: Hashable { diff --git a/Sources/AnyLint/Checkers/Checker.swift b/Sources/AnyLint/Checkers/Checker.swift new file mode 100644 index 0000000..368abef --- /dev/null +++ b/Sources/AnyLint/Checkers/Checker.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol Checker { + func performCheck() -> [Violation] +} diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift new file mode 100644 index 0000000..a76a8e0 --- /dev/null +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -0,0 +1,40 @@ +import Foundation +import HandySwift +import Utility + +struct FileContentsChecker { + let checkInfo: CheckInfo + let regex: Regex + let filePathsToCheck: [String] +} + +extension FileContentsChecker: Checker { + func performCheck() -> [Violation] { + var violations: [Violation] = [] + + for filePath in filePathsToCheck { + if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { + for match in regex.matches(in: fileContents) { + // TODO: [cg_2020-03-13] use capture group named 'pointer' if exists + let locationInfo = fileContents.locationInfo(of: match.range.lowerBound) + + // TODO: [cg_2020-03-13] autocorrect if autocorrection is available + violations.append( + Violation( + checkInfo: checkInfo, + filePath: filePath, + locationInfo: locationInfo + ) + ) + } + } 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 + ) + } + } + + return violations + } +} diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift new file mode 100644 index 0000000..32c34e8 --- /dev/null +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -0,0 +1,47 @@ +import Foundation +import HandySwift +import Utility + +struct FilePathsChecker { + let checkInfo: CheckInfo + let regex: Regex + let filePathsToCheck: [String] + let violateIfNoMatchesFound: Bool +} + +extension FilePathsChecker: Checker { + func performCheck() -> [Violation] { + var violations: [Violation] = [] + + if violateIfNoMatchesFound { + let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count + if matchingFilePathsCount <= 0 { + violations.append( + Violation( + checkInfo: checkInfo, + filePath: nil, + locationInfo: nil + ) + ) + } + } else { + for filePath in filePathsToCheck { + for match in regex.matches(in: filePath) { + // TODO: [cg_2020-03-13] use capture group named 'pointer' if exists + let locationInfo = filePath.locationInfo(of: match.range.lowerBound) + + // TODO: [cg_2020-03-13] autocorrect if autocorrection is available + violations.append( + Violation( + checkInfo: checkInfo, + filePath: filePath, + locationInfo: locationInfo + ) + ) + } + } + } + + return violations + } +} diff --git a/Sources/AnyLint/Checkers/LastCommitMessageChecker.swift b/Sources/AnyLint/Checkers/LastCommitMessageChecker.swift new file mode 100644 index 0000000..3565588 --- /dev/null +++ b/Sources/AnyLint/Checkers/LastCommitMessageChecker.swift @@ -0,0 +1,13 @@ +import Foundation +import HandySwift + +struct LastCommitMessageChecker { + let checkInfo: CheckInfo + let regex: Regex +} + +extension LastCommitMessageChecker: Checker { + func performCheck() -> [Violation] { + [] // TODO: [cg_2020-03-14] not yet implemented + } +} diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index a81a642..57a69dc 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -1,9 +1,14 @@ import Foundation extension String { - 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) func locationInfo(of index: String.Index) -> LocationInfo { - (line: 0, charInLine: 0) // TODO: [cg_2020-03-13] not yet implemented + let prefix = self[startIndex ..< index] + let prefixLines = prefix.split(separator: "\n") + guard let lastPrefixLine = prefixLines.last else { return (line: 0, charInLine: 0) } + + return (line: prefixLines.count, charInLine: lastPrefixLine.count) } } diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index edae509..41b6104 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -1,9 +1,62 @@ import Foundation import HandySwift +import Utility /// Helper to search for files and filter using Regexes. public enum FilesSearch { static func allFiles(within path: String, includeFilters: [Regex], excludeFilters: [Regex] = []) -> [String] { - [] // TODO: [cg_2020-03-13] not yet implemented + guard let url = URL(string: path, relativeTo: fileManager.currentDirectoryUrl) else { + log.message("Could not convert path '\(path)' to type URL.", level: .error) + exit(EXIT_FAILURE) + } + + guard let enumerator = fileManager.enumerator( + at: url, + includingPropertiesForKeys: [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey], + options: [], + errorHandler: nil + ) else { + log.message("Couldn't create enumerator for path '\(path)'.", level: .error) + exit(EXIT_FAILURE) + } + + var filePaths: [String] = [] + + for case let fileUrl as URL in enumerator { + // skip if no include filter applies + guard includeFilters.contains(where: { $0.matches(fileUrl.relativePath) }) else { + enumerator.skipDescendants() + continue + } + + // skip if any exclude filter applies + if excludeFilters.contains(where: { $0.matches(fileUrl.relativePath) }) { + enumerator.skipDescendants() + continue + } + + // TODO: [cg_2020-03-15] make sure not to skip any hidden directories, that were explicitly specified in includeFilters + + 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) + exit(EXIT_FAILURE) + } + + // skip hidden files and directories + if isHiddenFilePath { + enumerator.skipDescendants() + continue + } + + if isRegularFilePath { + filePaths.append(fileUrl.relativePath) + } + } + + return filePaths } } diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index a5f9dfb..e100dd1 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -17,42 +17,22 @@ public enum Lint { public static func checkFileContents( checkInfo: CheckInfo, regex: Regex, - includeFilters: [Regex] = [], + includeFilters: [Regex] = [#".*"#], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, matchingExamples: [String] = [], nonMatchingExamples: [String] = [] ) { - // TODO: [cg_2020-03-13] validate matching and non-matching examples first + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - var violations: [Violation] = [] let filePathsToCheck: [String] = FilesSearch.allFiles( within: fileManager.currentDirectoryPath, includeFilters: includeFilters, excludeFilters: excludeFilters ) - for filePath in filePathsToCheck { - if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { - for match in regex.matches(in: fileContents) { - // TODO: [cg_2020-03-13] use capture group named 'pointer' if exists - let locationInfo = fileContents.locationInfo(of: match.range.lowerBound) - - // TODO: [cg_2020-03-13] autocorrect if autocorrection is available - violations.append( - FileContentViolation( - checkInfo: checkInfo, - filePath: filePath, - lineNum: locationInfo.line, - charInLine: locationInfo.charInLine - ) - ) - } - } 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) - } - } - + let violations = FileContentsChecker(checkInfo: checkInfo, regex: regex, filePathsToCheck: filePathsToCheck).performCheck() Statistics.shared.found(violations: violations, in: checkInfo) } @@ -70,23 +50,28 @@ public enum Lint { public static func checkFilePaths( checkInfo: CheckInfo, regex: Regex, - includeFilters: [Regex] = [], + includeFilters: [Regex] = [#".*"#], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, matchingExamples: [String] = [], nonMatchingExamples: [String] = [], violateIfNoMatchesFound: Bool = false ) { - // TODO: [cg_2020-03-13] validate matching and non-matching examples first + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - var violations: [Violation] = [] let filePathsToCheck: [String] = FilesSearch.allFiles( within: fileManager.currentDirectoryPath, includeFilters: includeFilters, excludeFilters: excludeFilters ) - // TODO: [cg_2020-03-12] not yet implemented + let violations = FilePathsChecker( + checkInfo: checkInfo, + regex: regex, + filePathsToCheck: filePathsToCheck, + violateIfNoMatchesFound: violateIfNoMatchesFound + ).performCheck() Statistics.shared.found(violations: violations, in: checkInfo) } @@ -104,12 +89,10 @@ public enum Lint { matchingExamples: [String] = [], nonMatchingExamples: [String] = [] ) { - // TODO: [cg_2020-03-13] validate matching and non-matching examples first - - var violations: [Violation] = [] - - // TODO: [cg_2020-03-12] not yet implemented + validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) + validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + let violations = LastCommitMessageChecker(checkInfo: checkInfo, regex: regex).performCheck() Statistics.shared.found(violations: violations, in: checkInfo) } @@ -121,4 +104,43 @@ public enum Lint { public static func customCheck(checkInfo: CheckInfo, customClosure: () -> [Violation]) { Statistics.shared.found(violations: customClosure(), 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(failOnWarnings: Bool = false) { + Statistics.shared.logSummary() + + if Statistics.shared.violationsBySeverity[.error]!.isFilled { + exit(EXIT_FAILURE) + } else if failOnWarnings && Statistics.shared.violationsBySeverity[.warning]!.isFilled { + exit(EXIT_FAILURE) + } else { + exit(EXIT_SUCCESS) + } + } + + static func validate(regex: Regex, matchesForEach matchingExamples: [String], checkInfo: CheckInfo) { + for example in matchingExamples { + if !regex.matches(example) { + // TODO: [cg_2020-03-14] check position of ↘ is the matching line and char. + log.message( + "Couldn't find a match for regex '\(regex)' in check '\(checkInfo.id)' within matching example:\n\(example)", + level: .error + ) + exit(EXIT_FAILURE) + } + } + } + + static func validate(regex: Regex, doesNotMatchAny nonMatchingExamples: [String], checkInfo: CheckInfo) { + for example in nonMatchingExamples { + if regex.matches(example) { + // TODO: [cg_2020-03-14] check position of ↘ is the matching line and char. + log.message( + "Unexpectedly found a match for regex '\(regex)' in check '\(checkInfo.id)' within non-matching example:\n\(example)", + level: .error + ) + exit(EXIT_FAILURE) + } + } + } } diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift index 1b7980b..c3dfffb 100644 --- a/Sources/AnyLint/Severity.swift +++ b/Sources/AnyLint/Severity.swift @@ -1,7 +1,8 @@ import Foundation +import Utility /// Defines the severity of a lint check. -public enum Severity { +public enum Severity: Int, CaseIterable { /// Use for checks that are mostly informational and not necessarily problematic. case info @@ -10,4 +11,17 @@ public enum Severity { /// 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 + } + } } diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index 018f8ba..d9365b3 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -5,14 +5,34 @@ final class Statistics { static let shared = Statistics() var executedChecks: [CheckInfo] = [] - var allViolations: [Violation] = [] var violationsPerCheck: [CheckInfo: [Violation]] = [:] + var violationsBySeverity: [Severity: [Violation]] = [.info: [], .warning: [], .error: []] + + var maxViolationSeverity: Severity? { + violationsBySeverity.keys.max { $0.rawValue < $1.rawValue } + } private init() {} func found(violations: [Violation], in check: CheckInfo) { executedChecks.append(check) - allViolations.append(contentsOf: violations) violationsPerCheck[check] = violations + violationsBySeverity[check.severity]!.append(contentsOf: violations) + } + + func logSummary() { + if executedChecks.isEmpty { + log.message("No checks found to perform.", level: .warning) + } else if violationsBySeverity.isEmpty { + log.message("Performed \(executedChecks.count) checks without any violations.", level: .info) + } else { + let errors = "\(violationsBySeverity[.error]!.count) errors" + let warnings = "\(violationsBySeverity[.warning]!.count) warnings" + + log.message( + "Performed \(executedChecks.count) checks and found \(errors) & \(warnings).", + level: maxViolationSeverity!.logLevel + ) + } } } diff --git a/Sources/AnyLint/ViolationTypes/FileContentViolation.swift b/Sources/AnyLint/ViolationTypes/FileContentViolation.swift deleted file mode 100644 index 05fcdf7..0000000 --- a/Sources/AnyLint/ViolationTypes/FileContentViolation.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -open class FileContentViolation: Violation { - /// The file path. - public let filePath: String - - /// The line number of the violation. - public let lineNum: Int - - /// The character within the violations line. - public let charInLine: Int - - public init(checkInfo: CheckInfo, filePath: String, lineNum: Int, charInLine: Int) { - self.filePath = filePath - self.lineNum = lineNum - self.charInLine = charInLine - - super.init(checkInfo: checkInfo) - } -} diff --git a/Sources/AnyLint/ViolationTypes/Violation.swift b/Sources/AnyLint/ViolationTypes/Violation.swift index 1a54120..f39db3b 100644 --- a/Sources/AnyLint/ViolationTypes/Violation.swift +++ b/Sources/AnyLint/ViolationTypes/Violation.swift @@ -3,27 +3,29 @@ import HandySwift import Utility /// A violation found in a check. -open class Violation { +public struct Violation { /// The info about the chack that caused this violation. public let checkInfo: CheckInfo - /// Create a new violation. - public init(checkInfo: CheckInfo) { - self.checkInfo = checkInfo - } + /// The file path the violation is related to. + public let filePath: String? - func logMessage(match: Regex.Match?) { - let message = "\(checkInfo.consoleDescription(match: match))" + /// The info about the exact location of the violation within the file. Will be ignored if no `filePath` specified. + public let locationInfo: String.LocationInfo? - switch checkInfo.severity { - case .info: - log.message(message, level: .info) + func logMessage() { + let checkInfoMessage = "[\(checkInfo.id)] \(checkInfo.hint)" - case .warning: - log.message(message, level: .warning) + guard let filePath = filePath else { + log.message(checkInfoMessage, level: checkInfo.severity.logLevel) + return + } - case .error: - log.message(message, level: .error) + guard let locationInfo = locationInfo else { + log.message("\(filePath): \(checkInfoMessage)", level: checkInfo.severity.logLevel) + return } + + log.message("\(filePath):\(locationInfo.line):\(locationInfo.charInLine): \(checkInfoMessage)", level: checkInfo.severity.logLevel) } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift index 42705d7..85717b3 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift @@ -6,10 +6,12 @@ import Utility enum AndroidTemplate: ConfigurationTemplate { static func fileContents() -> String { """ - #!/usr/local/bin/\(CLIConstants.swiftShCommand) + #!\(CLIConstants.swiftShPath) import AnyLint // @Flinesoft ~> \(Constants.currentVersion) // TODO: [cg_2020-03-11] not yet implemented + + Lint.logSummaryAndExit() """ } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 18a608d..421694f 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -6,10 +6,12 @@ import Utility enum BlankTemplate: ConfigurationTemplate { static func fileContents() -> String { """ - #!/usr/local/bin/\(CLIConstants.swiftShCommand) + #!\(CLIConstants.swiftShPath) import AnyLint // @Flinesoft ~> \(Constants.currentVersion) // TODO: [cg_2020-03-11] not yet implemented + + Lint.logSummaryAndExit() """ } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift index 961e273..2fe8501 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift @@ -4,11 +4,13 @@ import Utility enum IOSTemplate: ConfigurationTemplate { static func fileContents() -> String { """ - #!/usr/local/bin/\(CLIConstants.swiftShCommand) + #!\(CLIConstants.swiftShPath) import AnyLint // @Flinesoft ~> \(Constants.currentVersion) // TODO: [cg_2020-03-11] not yet implemented + Lint.logSummaryAndExit() + """ } } diff --git a/Sources/AnyLintCLI/Globals/CLIConstants.swift b/Sources/AnyLintCLI/Globals/CLIConstants.swift index e8890e9..07111e3 100644 --- a/Sources/AnyLintCLI/Globals/CLIConstants.swift +++ b/Sources/AnyLintCLI/Globals/CLIConstants.swift @@ -4,5 +4,5 @@ enum CLIConstants { static let commandName: String = "anylint" static let defaultConfigFileName: String = "lint.swift" static let initTemplateCases: String = InitTask.Template.allCases.map { $0.rawValue }.joined(separator: ", ") - static let swiftShCommand: String = "swift-sh" + static let swiftShPath: String = "/usr/local/bin/swift-sh" } diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index 4e68038..49d17b7 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -3,14 +3,11 @@ import SwiftCLI import Utility enum ValidateOrFail { - /// Fails if swift-sh is not installed. Returns the install path if it is installed. - @discardableResult - static func swiftShInstalled() throws -> String { - do { - return try Task.capture(bash: "which \(CLIConstants.swiftShCommand)").stdout - } catch is CaptureError { + /// Fails if swift-sh is not installed. + static func swiftShInstalled() { + guard fileManager.fileExists(atPath: CLIConstants.swiftShPath) else { log.message( - "\(CLIConstants.swiftShCommand) not installed – please follow instructions on https://github.com/mxcl/swift-sh#installation to install.", + "swift-sh not installed – please follow instructions on https://github.com/mxcl/swift-sh#installation to install.", level: .error ) exit(EXIT_FAILURE) diff --git a/Sources/AnyLintCLI/Tasks/EditTask.swift b/Sources/AnyLintCLI/Tasks/EditTask.swift index 553036f..f1f3017 100644 --- a/Sources/AnyLintCLI/Tasks/EditTask.swift +++ b/Sources/AnyLintCLI/Tasks/EditTask.swift @@ -9,9 +9,9 @@ struct EditTask { extension EditTask: TaskHandler { func perform() throws { try ValidateOrFail.configFileExists(at: configFilePath) - try ValidateOrFail.swiftShInstalled() + ValidateOrFail.swiftShInstalled() log.message("Opening config file at \(configFilePath) in Xcode to edit ...", level: .info) - try Task.run(bash: "\(CLIConstants.swiftShCommand) edit '\(configFilePath)'") + try Task.run(bash: "\(CLIConstants.swiftShPath) edit '\(configFilePath)'") } } diff --git a/Sources/AnyLintCLI/Tasks/LintTask.swift b/Sources/AnyLintCLI/Tasks/LintTask.swift index 9cf9794..c4f6703 100644 --- a/Sources/AnyLintCLI/Tasks/LintTask.swift +++ b/Sources/AnyLintCLI/Tasks/LintTask.swift @@ -19,12 +19,12 @@ extension LintTask: TaskHandler { try Task.run(bash: "chmod +x '\(configFilePath)'") } - try ValidateOrFail.swiftShInstalled() + ValidateOrFail.swiftShInstalled() do { log.message("Start linting using config file at \(configFilePath) ...", level: .info) try Task.run(bash: "\(configFilePath.absolutePath)") - log.message("Successfully linted without errors using config file at \(configFilePath). Congrats! 🎉", level: .success) + log.message("Linting successful using config file at \(configFilePath). Congrats! 🎉", level: .success) } catch is RunError { log.message("Linting failed using config file at \(configFilePath).", level: .error) throw LintError.configFileFailed diff --git a/Sources/Utility/Extensions/CollectionExt.swift b/Sources/Utility/Extensions/CollectionExt.swift new file mode 100644 index 0000000..7d9a099 --- /dev/null +++ b/Sources/Utility/Extensions/CollectionExt.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Collection { + /// A Boolean value indicating whether the collection is not empty. + public var isFilled: Bool { + !isEmpty + } +} diff --git a/Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift b/Sources/Utility/Extensions/FileManagerExt.swift similarity index 54% rename from Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift rename to Sources/Utility/Extensions/FileManagerExt.swift index c825739..2d014a4 100644 --- a/Sources/AnyLintCLI/Globals/Extensions/FileManagerExt.swift +++ b/Sources/Utility/Extensions/FileManagerExt.swift @@ -1,7 +1,8 @@ import Foundation extension FileManager { - var currentDirectoryUrl: URL { + /// The current directory `URL`. + public var currentDirectoryUrl: URL { URL(string: currentDirectoryPath)! } } diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/Utility/Extensions/RegexExt.swift new file mode 100644 index 0000000..992bbe1 --- /dev/null +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -0,0 +1,13 @@ +import Foundation +import HandySwift + +extension Regex: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + do { + self = try Regex(value) + } catch { + log.message("Failed to convert String literal '\(value)' to type Regex.", level: .error) + exit(EXIT_FAILURE) + } + } +} diff --git a/lint.swift b/lint.swift index 675bfb4..23e8133 100755 --- a/lint.swift +++ b/lint.swift @@ -2,3 +2,5 @@ import AnyLint // . // TODO: [cg_2020-03-11] not yet implemented + +Lint.logSummaryAndExit() From afd6dc2a31a7cabd1f4dd87afb10bd64a921f1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Mar 2020 08:05:15 +0100 Subject: [PATCH 13/27] Write tests for FileSearch, Statistics and Violation types --- .gitignore | 1 + .swiftlint.yml | 1 - Package.swift | 5 +- Sources/AnyLint/Extensions/URLExt.swift | 8 ++ Sources/AnyLint/FilesSearch.swift | 30 +++---- Sources/AnyLint/Statistics.swift | 7 ++ .../{ViolationTypes => }/Violation.swift | 6 ++ Sources/Utility/Logger.swift | 62 +++++++------- Sources/Utility/TestHelper.swift | 11 ++- Tests/AnyLintTests/AnyLintTests.swift | 8 -- .../Checkers/FileContentsCheckerTests.swift | 14 +++ .../Checkers/FilePathsCheckerTests.swift | 14 +++ .../LastCommitMessageCheckerTests.swift | 14 +++ Tests/AnyLintTests/FilesSearchTests.swift | 38 +++++++++ Tests/AnyLintTests/LintTests.swift | 38 +++++++++ Tests/AnyLintTests/StatisticsTests.swift | 85 +++++++++++++++++++ Tests/AnyLintTests/ViolationTests.swift | 32 +++++++ .../Extensions/RegexExtTests.swift | 10 +++ Tests/UtilityTests/LoggerTests.swift | 31 +++++++ Tests/UtilityTests/UtilityTests.swift | 7 -- 20 files changed, 353 insertions(+), 69 deletions(-) create mode 100644 Sources/AnyLint/Extensions/URLExt.swift rename Sources/AnyLint/{ViolationTypes => }/Violation.swift (82%) delete mode 100644 Tests/AnyLintTests/AnyLintTests.swift create mode 100644 Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift create mode 100644 Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift create mode 100644 Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift create mode 100644 Tests/AnyLintTests/FilesSearchTests.swift create mode 100644 Tests/AnyLintTests/LintTests.swift create mode 100644 Tests/AnyLintTests/StatisticsTests.swift create mode 100644 Tests/AnyLintTests/ViolationTests.swift create mode 100644 Tests/UtilityTests/Extensions/RegexExtTests.swift create mode 100644 Tests/UtilityTests/LoggerTests.swift delete mode 100644 Tests/UtilityTests/UtilityTests.swift diff --git a/.gitignore b/.gitignore index 95c4320..cdcb390 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /Packages /*.xcodeproj xcuserdata/ +/tmp diff --git a/.swiftlint.yml b/.swiftlint.yml index 49876de..184cd5a 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -48,7 +48,6 @@ opt_in_rules: - object_literal - operator_usage_whitespace - optional_enum_case_matching -- overridden_super_call - override_in_extension - pattern_matching_keywords - prefer_self_type_over_type_of_self diff --git a/Package.swift b/Package.swift index 151dd37..3a17ac1 100644 --- a/Package.swift +++ b/Package.swift @@ -33,6 +33,9 @@ let package = Package( name: "Utility", dependencies: ["HandySwift", "Rainbow"] ), - .testTarget(name: "UtilityTests") + .testTarget( + name: "UtilityTests", + dependencies: ["Utility"] + ) ] ) diff --git a/Sources/AnyLint/Extensions/URLExt.swift b/Sources/AnyLint/Extensions/URLExt.swift new file mode 100644 index 0000000..2bdb1a6 --- /dev/null +++ b/Sources/AnyLint/Extensions/URLExt.swift @@ -0,0 +1,8 @@ +import Foundation +import Utility + +extension URL { + var relativePathFromCurrent: String { + String(path.replacingOccurrences(of: fileManager.currentDirectoryPath, with: "").dropFirst()) + } +} diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index 41b6104..0c56697 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -23,20 +23,6 @@ public enum FilesSearch { var filePaths: [String] = [] for case let fileUrl as URL in enumerator { - // skip if no include filter applies - guard includeFilters.contains(where: { $0.matches(fileUrl.relativePath) }) else { - enumerator.skipDescendants() - continue - } - - // skip if any exclude filter applies - if excludeFilters.contains(where: { $0.matches(fileUrl.relativePath) }) { - enumerator.skipDescendants() - continue - } - - // TODO: [cg_2020-03-15] make sure not to skip any hidden directories, that were explicitly specified in includeFilters - guard let resourceValues = try? fileUrl.resourceValues(forKeys: [URLResourceKey.isRegularFileKey, URLResourceKey.isHiddenKey]), let isHiddenFilePath = resourceValues.isHidden, @@ -46,14 +32,24 @@ public enum FilesSearch { exit(EXIT_FAILURE) } + // 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 isHiddenFilePath { - enumerator.skipDescendants() + if !isRegularFilePath { + enumerator.skipDescendants() + } continue } - if isRegularFilePath { - filePaths.append(fileUrl.relativePath) + if isRegularFilePath, includeFilters.contains(where: { $0.matches(fileUrl.relativePathFromCurrent) }) { + filePaths.append(fileUrl.relativePathFromCurrent) } } diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index d9365b3..e0e95b2 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -20,6 +20,13 @@ final class Statistics { violationsBySeverity[check.severity]!.append(contentsOf: violations) } + /// Use for unit testing only. + func reset() { + executedChecks = [] + violationsPerCheck = [:] + violationsBySeverity = [.info: [], .warning: [], .error: []] + } + func logSummary() { if executedChecks.isEmpty { log.message("No checks found to perform.", level: .warning) diff --git a/Sources/AnyLint/ViolationTypes/Violation.swift b/Sources/AnyLint/Violation.swift similarity index 82% rename from Sources/AnyLint/ViolationTypes/Violation.swift rename to Sources/AnyLint/Violation.swift index f39db3b..5398b21 100644 --- a/Sources/AnyLint/ViolationTypes/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -13,6 +13,12 @@ public struct Violation { /// The info about the exact location of the violation within the file. Will be ignored if no `filePath` specified. public let locationInfo: String.LocationInfo? + init(checkInfo: CheckInfo, filePath: String? = nil, locationInfo: String.LocationInfo? = nil) { + self.checkInfo = checkInfo + self.filePath = filePath + self.locationInfo = locationInfo + } + func logMessage() { let checkInfoMessage = "[\(checkInfo.id)] \(checkInfo.hint)" diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index cf26824..b5b7562 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -41,13 +41,29 @@ public final class Logger { /// Output is targeted to a console to be read by developers. case console - /// Output is targeted to Xcode. Native support for Xcode Warnings & Errors. - case xcode - /// Output is targeted for unit tests. Collect into globally accessible TestHelper. case test } + /// The exit status. + public enum ExitStatus { + /// Successfully finished task. + case success + + /// Failed to finish task. + case failure + + var statusCode: Int32 { + switch self { + case .success: + return EXIT_SUCCESS + + case .failure: + return EXIT_FAILURE + } + } + } + let outputType: OutputType init(outputType: OutputType) { @@ -59,25 +75,28 @@ public final class Logger { /// - 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. - /// - file: The file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. - /// - line: The line within the file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. - public func message(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { + public func message(_ message: String, level: PrintLevel) { switch outputType { case .console: - consoleMessage(message, level: level, file: file, line: line) - - case .xcode: - xcodeMessage(message, level: level, file: file, line: line) + consoleMessage(message, level: level) case .test: - TestHelper.shared.consoleOutputs.append((message, level, file, line)) + TestHelper.shared.consoleOutputs.append((message, level)) } } - private func consoleMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil, charInLine: Int? = nil) { - let location = locationInfo(file: file, line: line, charInLine: charInLine)?.replacingOccurrences(of: fileManager.currentDirectoryPath, with: ".") - let message = location != nil ? [location!, message].joined(separator: " ") : message + /// Exits the current program with the given status. + public func exit(status: ExitStatus) { + switch outputType { + case .console: + Darwin.exit(status.statusCode) + + case .test: + TestHelper.shared.exitStatus = status + } + } + private func consoleMessage(_ message: String, level: PrintLevel) { switch level { case .success: print(formattedCurrentTime(), "✅ ", message.lightGreen) @@ -99,19 +118,4 @@ public final class Logger { let dateTime = dateFormatter.string(from: Date()) return "\(dateTime):" } - - private func xcodeMessage(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil, charInLine: Int? = nil) { - if let location = locationInfo(file: file, line: line, charInLine: charInLine) { - print(location, "\(level.rawValue): \(Constants.toolName): ", message) - } else { - print("\(level.rawValue): \(Constants.toolName): ", message) - } - } - - private func locationInfo(file: String?, line: Int?, charInLine: Int?) -> String? { - guard let file = file else { return nil } - guard let line = line else { return "\(file): " } - guard let charInLine = charInLine else { return "\(file):\(line): " } - return "\(file):\(line):\(charInLine): " - } } diff --git a/Sources/Utility/TestHelper.swift b/Sources/Utility/TestHelper.swift index b1fcefd..d35eee1 100644 --- a/Sources/Utility/TestHelper.swift +++ b/Sources/Utility/TestHelper.swift @@ -1,20 +1,19 @@ import Foundation -/// A helper class for Unit Testing only. Only put data in here when `isStartedByUnitTests` is set to true. -/// Never read other data in framework than that property. +/// A helper class for Unit Testing only. public final class TestHelper { /// The console output data. - public typealias ConsoleOutput = (message: String, level: Logger.PrintLevel, file: String?, line: Int?) + public typealias ConsoleOutput = (message: String, level: Logger.PrintLevel) /// The shared `TestHelper` object. public static let shared = TestHelper() - /// Set to `true` within unit tests (in `setup()`). Defaults to `false`. - public var isStartedByUnitTests: Bool = false - /// 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 = [] diff --git a/Tests/AnyLintTests/AnyLintTests.swift b/Tests/AnyLintTests/AnyLintTests.swift deleted file mode 100644 index 6627c33..0000000 --- a/Tests/AnyLintTests/AnyLintTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -@testable import AnyLint -import XCTest - -final class AnyLintTests: XCTestCase { - func testExample() { - // TODO: [cg_2020-03-07] not yet implemented - } -} diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift new file mode 100644 index 0000000..6ed1a35 --- /dev/null +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -0,0 +1,14 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class FileContentsCheckerTests: XCTestCase { + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testPerformCheck() { + // TODO: [cg_2020-03-15] not yet implemented + } +} diff --git a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift new file mode 100644 index 0000000..b8c9788 --- /dev/null +++ b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift @@ -0,0 +1,14 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class FilePathsCheckerTests: XCTestCase { + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testPerformCheck() { + // TODO: [cg_2020-03-15] not yet implemented + } +} diff --git a/Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift b/Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift new file mode 100644 index 0000000..a1f9d7f --- /dev/null +++ b/Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift @@ -0,0 +1,14 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class LastCommitMessageCheckerTests: XCTestCase { + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testPerformCheck() { + // TODO: [cg_2020-03-15] not yet implemented + } +} diff --git a/Tests/AnyLintTests/FilesSearchTests.swift b/Tests/AnyLintTests/FilesSearchTests.swift new file mode 100644 index 0000000..ef42dc9 --- /dev/null +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -0,0 +1,38 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class FilesSearchTests: XCTestCase { + private let tempDir: String = "AnyLintTempTests" + + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testAllFilesWithinPath() { + try? FileManager.default.createDirectory(atPath: "\(tempDir)/Sources", withIntermediateDirectories: true, attributes: nil) + FileManager.default.createFile(atPath: "\(tempDir)/Hello.swift", contents: nil, attributes: nil) + FileManager.default.createFile(atPath: "\(tempDir)/World.swift", contents: nil, attributes: nil) + FileManager.default.createFile(atPath: "\(tempDir)/.hidden_file", contents: nil, attributes: nil) + + try? FileManager.default.createDirectory(atPath: "\(tempDir)/.hidden_dir", withIntermediateDirectories: true, attributes: nil) + FileManager.default.createFile(atPath: "\(tempDir)/.hidden_dir/unhidden_file", contents: nil, attributes: nil) + + let includeFilterFilePaths = FilesSearch.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [#"AnyLintTempTests/.*"#], + excludeFilters: [] + ) + XCTAssertEqual(includeFilterFilePaths, ["\(tempDir)/Hello.swift", "\(tempDir)/World.swift"]) + + let excludeFilterFilePaths = FilesSearch.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [#"AnyLintTempTests/.*"#], + excludeFilters: [#"World"#] + ) + XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Hello.swift"]) + + try? FileManager.default.removeItem(atPath: tempDir) + } +} diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift new file mode 100644 index 0000000..ad6510b --- /dev/null +++ b/Tests/AnyLintTests/LintTests.swift @@ -0,0 +1,38 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class LintTests: XCTestCase { + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + } + + func testCheckFileContents() { + // TODO: [cg_2020-03-15] not yet implemented + } + + func testCheckFilePaths() { + // TODO: [cg_2020-03-15] not yet implemented + } + + func testCheckLastCommitMessage() { + // TODO: [cg_2020-03-15] not yet implemented + } + + func testCustomCheck() { + // TODO: [cg_2020-03-15] not yet implemented + } + + func testLogSummaryAndExit() { + // TODO: [cg_2020-03-15] not yet implemented + } + + func testValidateRegexMatchesForEach() { + // TODO: [cg_2020-03-15] not yet implemented + } + + func testValidateRegexDoesNotMatchAny() { + // TODO: [cg_2020-03-15] not yet implemented + } +} diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift new file mode 100644 index 0000000..d59f108 --- /dev/null +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -0,0 +1,85 @@ +@testable import AnyLint +@testable import Utility +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() { + Statistics.shared.logSummary() + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "No checks found to perform.") + + 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), Violation(checkInfo: checkInfo2)], + in: CheckInfo(id: "id2", hint: "hint2", severity: .warning) + ) + + let checkInfo3 = CheckInfo(id: "id3", hint: "hint3", severity: .error) + Statistics.shared.found( + violations: [Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3)], + in: CheckInfo(id: "id3", hint: "hint3", severity: .error) + ) + + Statistics.shared.logSummary() + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .error) + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Performed 3 checks and found 3 errors & 2 warnings.") + } +} diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift new file mode 100644 index 0000000..dbb9f98 --- /dev/null +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -0,0 +1,32 @@ +@testable import AnyLint +@testable import Utility +import XCTest + +final class ViolationTests: XCTestCase { + override func setUp() { + log = Logger(outputType: .test) + TestHelper.shared.reset() + Statistics.shared.reset() + } + + func testLogMessage() { + let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) + Violation(checkInfo: checkInfo).logMessage() + + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "[demo_check] Make sure to always check the demo.") + + Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift").logMessage() + + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .warning) + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Temp/Souces/Hello.swift: [demo_check] Make sure to always check the demo.") + + Violation(checkInfo: checkInfo, filePath: "Temp/Souces/World.swift", locationInfo: String.LocationInfo(line: 5, charInLine: 15)).logMessage() + + XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 3) + XCTAssertEqual(TestHelper.shared.consoleOutputs[2].level, .warning) + XCTAssertEqual(TestHelper.shared.consoleOutputs[2].message, "Temp/Souces/World.swift:5:15: [demo_check] Make sure to always check the demo.") + } +} diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/UtilityTests/Extensions/RegexExtTests.swift new file mode 100644 index 0000000..ef0ad54 --- /dev/null +++ b/Tests/UtilityTests/Extensions/RegexExtTests.swift @@ -0,0 +1,10 @@ +import HandySwift +@testable import Utility +import XCTest + +final class RegexExtTests: XCTestCase { + func testStringLiteralInit() { + let regex: Regex = #".*"# + XCTAssertEqual(regex.description, #"Regex<".*">"#) + } +} diff --git a/Tests/UtilityTests/LoggerTests.swift b/Tests/UtilityTests/LoggerTests.swift new file mode 100644 index 0000000..3496482 --- /dev/null +++ b/Tests/UtilityTests/LoggerTests.swift @@ -0,0 +1,31 @@ +@testable import Utility +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/UtilityTests/UtilityTests.swift b/Tests/UtilityTests/UtilityTests.swift deleted file mode 100644 index a4764fb..0000000 --- a/Tests/UtilityTests/UtilityTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -final class UtilityTests: XCTestCase { - func testExample() { - // TODO: [cg_2020-03-12] not yet implemented - } -} From 12fcd6313943cd849fef06b9bc4417c0139af31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 15 Mar 2020 08:53:47 +0100 Subject: [PATCH 14/27] Implement Lint tests --- Sources/AnyLint/FilesSearch.swift | 9 ++- Sources/AnyLint/Lint.swift | 10 ++-- .../AnyLintCLI/Commands/SingleCommand.swift | 7 ++- .../Globals/Extensions/StringExt.swift | 9 ++- .../AnyLintCLI/Globals/ValidateOrFail.swift | 6 +- Sources/AnyLintCLI/Tasks/InitTask.swift | 3 +- Sources/Utility/Extensions/RegexExt.swift | 3 +- Sources/Utility/TestHelper.swift | 1 + Tests/AnyLintTests/LintTests.swift | 55 ++++++++++++------- 9 files changed, 65 insertions(+), 38 deletions(-) diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index 0c56697..40e679e 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -7,7 +7,8 @@ public enum FilesSearch { static func allFiles(within path: String, includeFilters: [Regex], excludeFilters: [Regex] = []) -> [String] { guard let url = URL(string: path, relativeTo: fileManager.currentDirectoryUrl) else { log.message("Could not convert path '\(path)' to type URL.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return [] // only reachable in unit tests } guard let enumerator = fileManager.enumerator( @@ -17,7 +18,8 @@ public enum FilesSearch { errorHandler: nil ) else { log.message("Couldn't create enumerator for path '\(path)'.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return [] // only reachable in unit tests } var filePaths: [String] = [] @@ -29,7 +31,8 @@ public enum FilesSearch { let isRegularFilePath = resourceValues.isRegularFile else { log.message("Could not read resource values for file at \(fileUrl.path)", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return [] // only reachable in unit tests } // skip if any exclude filter applies diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index e100dd1..e6f2f68 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -110,11 +110,11 @@ public enum Lint { Statistics.shared.logSummary() if Statistics.shared.violationsBySeverity[.error]!.isFilled { - exit(EXIT_FAILURE) + log.exit(status: .failure) } else if failOnWarnings && Statistics.shared.violationsBySeverity[.warning]!.isFilled { - exit(EXIT_FAILURE) + log.exit(status: .failure) } else { - exit(EXIT_SUCCESS) + log.exit(status: .success) } } @@ -126,7 +126,7 @@ public enum Lint { "Couldn't find a match for regex '\(regex)' in check '\(checkInfo.id)' within matching example:\n\(example)", level: .error ) - exit(EXIT_FAILURE) + log.exit(status: .failure) } } } @@ -139,7 +139,7 @@ public enum Lint { "Unexpectedly found a match for regex '\(regex)' in check '\(checkInfo.id)' within non-matching example:\n\(example)", level: .error ) - exit(EXIT_FAILURE) + log.exit(status: .failure) } } } diff --git a/Sources/AnyLintCLI/Commands/SingleCommand.swift b/Sources/AnyLintCLI/Commands/SingleCommand.swift index 45b5014..160f2d5 100644 --- a/Sources/AnyLintCLI/Commands/SingleCommand.swift +++ b/Sources/AnyLintCLI/Commands/SingleCommand.swift @@ -23,7 +23,7 @@ class SingleCommand: Command { // version subcommand if version { try VersionTask().perform() - exit(EXIT_SUCCESS) + log.exit(status: .success) } let configurationPaths = customPaths.isEmpty @@ -34,13 +34,14 @@ class SingleCommand: Command { 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) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return // only reachable in unit tests } for configPath in configurationPaths { try InitTask(configFilePath: configPath, template: initTemplate).perform() } - exit(EXIT_SUCCESS) + log.exit(status: .success) } // lint main command diff --git a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift index f3de5b3..3e609b6 100644 --- a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift +++ b/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift @@ -5,7 +5,8 @@ extension String { var absolutePath: String { guard let url = URL(string: self, relativeTo: fileManager.currentDirectoryUrl) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return "" // only reachable in unit tests } return url.absoluteString @@ -14,7 +15,8 @@ extension String { var parentDirectoryPath: String { guard let url = URL(string: self) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return "" // only reachable in unit tests } return url.deletingLastPathComponent().absoluteString @@ -23,7 +25,8 @@ extension String { func appendingPathComponent(_ pathComponent: String) -> String { guard let pathUrl = URL(string: self) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return "" // only reachable in unit tests } return pathUrl.appendingPathComponent(pathComponent).absoluteString diff --git a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift index 49d17b7..f83a5cb 100644 --- a/Sources/AnyLintCLI/Globals/ValidateOrFail.swift +++ b/Sources/AnyLintCLI/Globals/ValidateOrFail.swift @@ -10,7 +10,8 @@ enum ValidateOrFail { "swift-sh not installed – please follow instructions on https://github.com/mxcl/swift-sh#installation to install.", level: .error ) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return // only reachable in unit tests } } @@ -20,7 +21,8 @@ enum ValidateOrFail { "No configuration file found at \(configFilePath) – consider running `\(CLIConstants.commandName) --init` with a template.", level: .error ) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return // only reachable in unit tests } } } diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index ab247bd..385bb87 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -30,7 +30,8 @@ extension InitTask: TaskHandler { func perform() throws { guard !fileManager.fileExists(atPath: configFilePath) else { log.message("Configuration file already exists at path '\(configFilePath)'.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + return // only reachable in unit tests } log.message("Making sure config file directory exists ...", level: .info) diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/Utility/Extensions/RegexExt.swift index 992bbe1..ca5b669 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -7,7 +7,8 @@ extension Regex: ExpressibleByStringLiteral { self = try Regex(value) } catch { log.message("Failed to convert String literal '\(value)' to type Regex.", level: .error) - exit(EXIT_FAILURE) + log.exit(status: .failure) + exit(EXIT_FAILURE) // only reachable in unit tests } } } diff --git a/Sources/Utility/TestHelper.swift b/Sources/Utility/TestHelper.swift index d35eee1..1b72080 100644 --- a/Sources/Utility/TestHelper.swift +++ b/Sources/Utility/TestHelper.swift @@ -17,5 +17,6 @@ public final class TestHelper { /// Deletes all data collected until now. public func reset() { consoleOutputs = [] + exitStatus = nil } } diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index ad6510b..623d7b7 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -1,4 +1,5 @@ @testable import AnyLint +import HandySwift @testable import Utility import XCTest @@ -8,31 +9,45 @@ final class LintTests: XCTestCase { TestHelper.shared.reset() } - func testCheckFileContents() { - // TODO: [cg_2020-03-15] not yet implemented - } - - func testCheckFilePaths() { - // TODO: [cg_2020-03-15] not yet implemented - } - - func testCheckLastCommitMessage() { - // TODO: [cg_2020-03-15] not yet implemented - } + func testValidateRegexMatchesForEach() { + XCTAssertNil(TestHelper.shared.exitStatus) - func testCustomCheck() { - // TODO: [cg_2020-03-15] not yet implemented - } + let regex: Regex = #"foo[0-9]?bar"# + let checkInfo = CheckInfo(id: "foo_bar", hint: "do bar", severity: .warning) - func testLogSummaryAndExit() { - // TODO: [cg_2020-03-15] not yet implemented - } + Lint.validate( + regex: regex, + matchesForEach: ["foo1bar", "foobar", "myfoo4barbeque"], + checkInfo: checkInfo + ) + XCTAssertNil(TestHelper.shared.exitStatus) - func testValidateRegexMatchesForEach() { - // TODO: [cg_2020-03-15] not yet implemented + Lint.validate( + regex: regex, + matchesForEach: ["foo1bar", "FooBar", "myfoo4barbeque"], + checkInfo: checkInfo + ) + XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } func testValidateRegexDoesNotMatchAny() { - // TODO: [cg_2020-03-15] not yet implemented + 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) } } From 87df782f52de7c231255920b19046f9a1125ebcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 16 Mar 2020 08:41:50 +0100 Subject: [PATCH 15/27] Implement basic tests for file contents & paths checkers --- .gitignore | 2 +- .../AnyLint/Checkers/FilePathsChecker.swift | 25 ++------ Sources/AnyLint/Extensions/StringExt.swift | 5 +- .../Checkers/FileContentsCheckerTests.swift | 26 +++++++- .../Checkers/FilePathsCheckerTests.swift | 63 ++++++++++++++++++- .../Extensions/XCTestCaseExt.swift | 25 ++++++++ Tests/AnyLintTests/FilesSearchTests.swift | 46 +++++++------- 7 files changed, 144 insertions(+), 48 deletions(-) create mode 100644 Tests/AnyLintTests/Extensions/XCTestCaseExt.swift diff --git a/.gitignore b/.gitignore index cdcb390..0e11e71 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ /Packages /*.xcodeproj xcuserdata/ -/tmp +/AnyLintTempTests diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index 32c34e8..08cff8c 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -17,28 +17,15 @@ extension FilePathsChecker: Checker { let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count if matchingFilePathsCount <= 0 { violations.append( - Violation( - checkInfo: checkInfo, - filePath: nil, - locationInfo: nil - ) + Violation(checkInfo: checkInfo, filePath: nil, locationInfo: nil) ) } } else { - for filePath in filePathsToCheck { - for match in regex.matches(in: filePath) { - // TODO: [cg_2020-03-13] use capture group named 'pointer' if exists - let locationInfo = filePath.locationInfo(of: match.range.lowerBound) - - // TODO: [cg_2020-03-13] autocorrect if autocorrection is available - violations.append( - Violation( - checkInfo: checkInfo, - filePath: filePath, - locationInfo: locationInfo - ) - ) - } + for filePath in filePathsToCheck where regex.matches(filePath) { + // TODO: [cg_2020-03-13] autocorrect if autocorrection is available + violations.append( + Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil) + ) } } diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 57a69dc..a5d8982 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -7,8 +7,9 @@ extension String { func locationInfo(of index: String.Index) -> LocationInfo { let prefix = self[startIndex ..< index] let prefixLines = prefix.split(separator: "\n") - guard let lastPrefixLine = prefixLines.last else { return (line: 0, charInLine: 0) } + guard let lastPrefixLine = prefixLines.last else { return (line: 1, charInLine: 1) } - return (line: prefixLines.count, charInLine: lastPrefixLine.count) + let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 + return (line: prefixLines.count + 1, charInLine: charInLine) } } diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index 6ed1a35..7302a7b 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -9,6 +9,30 @@ final class FileContentsCheckerTests: XCTestCase { } func testPerformCheck() { - // TODO: [cg_2020-03-15] not yet implemented + 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 = FileContentsChecker( + checkInfo: checkInfo, + regex: #"(let|var) \w+=\w+"#, + filePathsToCheck: filePathsToCheck + ).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) + } } } diff --git a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift index b8c9788..da642a7 100644 --- a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift @@ -9,6 +9,67 @@ final class FilePathsCheckerTests: XCTestCase { } func testPerformCheck() { - // TODO: [cg_2020-03-15] not yet implemented + withTemporaryFiles( + [ + (subpath: "Sources/Hello.swift", contents: ""), + (subpath: "Sources/World.swift", contents: ""), + ] + ) { filePathsToCheck in + let violations = sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + XCTAssertEqual(violations.count, 0) + } + + withTemporaryFiles([(subpath: "Sources/World.swift", contents: "")]) { filePathsToCheck in + let violations = sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + + XCTAssertEqual(violations.count, 1) + + XCTAssertEqual(violations[0].checkInfo, sayHelloCheck()) + XCTAssertNil(violations[0].filePath) + XCTAssertNil(violations[0].locationInfo) + XCTAssertNil(violations[0].locationInfo) + } + + withTemporaryFiles( + [ + (subpath: "Sources/Hello.swift", contents: ""), + (subpath: "Sources/World.swift", contents: ""), + ] + ) { filePathsToCheck in + let violations = noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() + + XCTAssertEqual(violations.count, 1) + + XCTAssertEqual(violations[0].checkInfo, noWorldCheck()) + XCTAssertEqual(violations[0].filePath, "\(tempDir)/Sources/World.swift") + XCTAssertNil(violations[0].locationInfo) + XCTAssertNil(violations[0].locationInfo) + } + } + + private func sayHelloChecker(filePathsToCheck: [String]) -> FilePathsChecker { + FilePathsChecker( + checkInfo: sayHelloCheck(), + regex: #".*Hello\.swift"#, + filePathsToCheck: filePathsToCheck, + 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, + 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 new file mode 100644 index 0000000..a59d9d8 --- /dev/null +++ b/Tests/AnyLintTests/Extensions/XCTestCaseExt.swift @@ -0,0 +1,25 @@ +@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 index ef42dc9..9d60838 100644 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -1,38 +1,36 @@ @testable import AnyLint +import HandySwift @testable import Utility import XCTest final class FilesSearchTests: XCTestCase { - private let tempDir: String = "AnyLintTempTests" - override func setUp() { log = Logger(outputType: .test) TestHelper.shared.reset() } func testAllFilesWithinPath() { - try? FileManager.default.createDirectory(atPath: "\(tempDir)/Sources", withIntermediateDirectories: true, attributes: nil) - FileManager.default.createFile(atPath: "\(tempDir)/Hello.swift", contents: nil, attributes: nil) - FileManager.default.createFile(atPath: "\(tempDir)/World.swift", contents: nil, attributes: nil) - FileManager.default.createFile(atPath: "\(tempDir)/.hidden_file", contents: nil, attributes: nil) - - try? FileManager.default.createDirectory(atPath: "\(tempDir)/.hidden_dir", withIntermediateDirectories: true, attributes: nil) - FileManager.default.createFile(atPath: "\(tempDir)/.hidden_dir/unhidden_file", contents: nil, attributes: nil) - - let includeFilterFilePaths = FilesSearch.allFiles( - within: FileManager.default.currentDirectoryPath, - includeFilters: [#"AnyLintTempTests/.*"#], - excludeFilters: [] - ) - XCTAssertEqual(includeFilterFilePaths, ["\(tempDir)/Hello.swift", "\(tempDir)/World.swift"]) - - let excludeFilterFilePaths = FilesSearch.allFiles( - within: FileManager.default.currentDirectoryPath, - includeFilters: [#"AnyLintTempTests/.*"#], - excludeFilters: [#"World"#] - ) - XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Hello.swift"]) + 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.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [try Regex("\(tempDir)/.*")], + excludeFilters: [] + ) + XCTAssertEqual(includeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift", "\(tempDir)/Sources/World.swift"]) - try? FileManager.default.removeItem(atPath: tempDir) + let excludeFilterFilePaths = FilesSearch.allFiles( + within: FileManager.default.currentDirectoryPath, + includeFilters: [try Regex("\(tempDir)/.*")], + excludeFilters: ["World"] + ) + XCTAssertEqual(excludeFilterFilePaths, ["\(tempDir)/Sources/Hello.swift"]) + } } } From dbe9223f2342248d667febeca8bee222a9052506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 16 Mar 2020 09:20:42 +0100 Subject: [PATCH 16/27] Remove commit message checker in favor of fit-commit --- .../AnyLint/Checkers/LastCommitMessageChecker.swift | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 Sources/AnyLint/Checkers/LastCommitMessageChecker.swift diff --git a/Sources/AnyLint/Checkers/LastCommitMessageChecker.swift b/Sources/AnyLint/Checkers/LastCommitMessageChecker.swift deleted file mode 100644 index 3565588..0000000 --- a/Sources/AnyLint/Checkers/LastCommitMessageChecker.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import HandySwift - -struct LastCommitMessageChecker { - let checkInfo: CheckInfo - let regex: Regex -} - -extension LastCommitMessageChecker: Checker { - func performCheck() -> [Violation] { - [] // TODO: [cg_2020-03-14] not yet implemented - } -} From c2f31756d00412f188b0f5f3821f17780070e7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Mon, 16 Mar 2020 13:35:23 +0100 Subject: [PATCH 17/27] Remove check last commit leftovers --- Sources/AnyLint/Lint.swift | 20 ------------------- .../LastCommitMessageCheckerTests.swift | 14 ------------- 2 files changed, 34 deletions(-) delete mode 100644 Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index e6f2f68..ce22843 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -76,26 +76,6 @@ public enum Lint { Statistics.shared.found(violations: violations, in: checkInfo) } - /// Checks the last commit message. - /// - /// - Parameters: - /// - checkInfo: The info object providing some general information on the lint check. - /// - regex: The regex to use for matching the commit message. - /// - matchingExamples: An array of example messages where the `regex` is expected to trigger. Optionally, the expected pointer position can be marked with ↘. - /// - nonMatchingExamples: An array of example messages where the `regex` is expected not to trigger. - public static func checkLastCommitMessage( - checkInfo: CheckInfo, - regex: Regex, - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [] - ) { - validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) - validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) - - let violations = LastCommitMessageChecker(checkInfo: checkInfo, regex: regex).performCheck() - Statistics.shared.found(violations: violations, in: checkInfo) - } - /// Run custom logic as checks. /// /// - Parameters: diff --git a/Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift b/Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift deleted file mode 100644 index a1f9d7f..0000000 --- a/Tests/AnyLintTests/Checkers/LastCommitMessageCheckerTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -@testable import AnyLint -@testable import Utility -import XCTest - -final class LastCommitMessageCheckerTests: XCTestCase { - override func setUp() { - log = Logger(outputType: .test) - TestHelper.shared.reset() - } - - func testPerformCheck() { - // TODO: [cg_2020-03-15] not yet implemented - } -} From 1310a8b9b90ae821f7351295e0809407b16de05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Wed, 18 Mar 2020 08:52:51 +0100 Subject: [PATCH 18/27] Remove HandySwift dependency --- Package.resolved | 9 - Package.swift | 7 +- Sources/AnyLint/CheckInfo.swift | 1 - .../Checkers/FileContentsChecker.swift | 1 - .../AnyLint/Checkers/FilePathsChecker.swift | 1 - Sources/AnyLint/FilesSearch.swift | 1 - Sources/AnyLint/Lint.swift | 1 - Sources/AnyLint/Violation.swift | 1 - Sources/Utility/Extensions/RegexExt.swift | 1 - Sources/Utility/Regex.swift | 262 ++++++++++++++++++ Tests/AnyLintTests/FilesSearchTests.swift | 1 - Tests/AnyLintTests/LintTests.swift | 1 - .../Extensions/RegexExtTests.swift | 1 - 13 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 Sources/Utility/Regex.swift diff --git a/Package.resolved b/Package.resolved index f02fdd2..aa38a47 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "HandySwift", - "repositoryURL": "https://github.com/Flinesoft/HandySwift.git", - "state": { - "branch": null, - "revision": "083707d9f9da65bd57b756294653ee0fc50d8662", - "version": "3.1.0" - } - }, { "package": "Rainbow", "repositoryURL": "https://github.com/onevcat/Rainbow.git", diff --git a/Package.swift b/Package.swift index 3a17ac1..c0ad908 100644 --- a/Package.swift +++ b/Package.swift @@ -8,14 +8,13 @@ let package = Package( .executable(name: "anylint", targets: ["AnyLintCLI"]), ], dependencies: [ - .package(url: "https://github.com/Flinesoft/HandySwift.git", from: "3.1.0"), .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: ["HandySwift", "Utility"] + dependencies: ["Utility"] ), .testTarget( name: "AnyLintTests", @@ -23,7 +22,7 @@ let package = Package( ), .target( name: "AnyLintCLI", - dependencies: ["HandySwift", "Rainbow", "SwiftCLI", "Utility"] + dependencies: ["Rainbow", "SwiftCLI", "Utility"] ), .testTarget( name: "AnyLintCLITests", @@ -31,7 +30,7 @@ let package = Package( ), .target( name: "Utility", - dependencies: ["HandySwift", "Rainbow"] + dependencies: ["Rainbow"] ), .testTarget( name: "UtilityTests", diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index a7c6e22..58c8a90 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift import Utility /// Provides some basic information needed in each lint check. diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index a76a8e0..5eebf53 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift import Utility struct FileContentsChecker { diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index 08cff8c..0b4350a 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift import Utility struct FilePathsChecker { diff --git a/Sources/AnyLint/FilesSearch.swift b/Sources/AnyLint/FilesSearch.swift index 40e679e..179dcf8 100644 --- a/Sources/AnyLint/FilesSearch.swift +++ b/Sources/AnyLint/FilesSearch.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift import Utility /// Helper to search for files and filter using Regexes. diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index ce22843..970cc90 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift import Utility /// The linter type providing APIs for checking anything using regular expressions. diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index 5398b21..723db5a 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift import Utility /// A violation found in a check. diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/Utility/Extensions/RegexExt.swift index ca5b669..17226ef 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -1,5 +1,4 @@ import Foundation -import HandySwift extension Regex: ExpressibleByStringLiteral { public init(stringLiteral value: String) { diff --git a/Sources/Utility/Regex.swift b/Sources/Utility/Regex.swift new file mode 100644 index 0000000..158d87c --- /dev/null +++ b/Sources/Utility/Regex.swift @@ -0,0 +1,262 @@ +// Originally from: https://github.com/sharplet/Regex & https://github.com/Flinesoft/HandySwift (modified). + +import Foundation + +/// `Regex` is a swifty regex engine built on top of the NSRegularExpression api. +public struct Regex { + // MARK: - Properties + private let regularExpression: NSRegularExpression + + // 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 = []) throws { + regularExpression = try NSRegularExpression( + pattern: pattern, + options: options.toNSRegularExpressionOptions + ) + } + + // MARK: - Methods: Matching + /// Returns `true` if the regex matches `string`, otherwise returns `false`. + /// + /// - parameter string: The string to test. + /// + /// - returns: `true` if the regular expression matches, otherwise `false`. + public func matches(_ string: String) -> Bool { + firstMatch(in: string) != nil + } + + /// If the regex matches `string`, returns a `Match` describing the + /// first matched string and any captures. If there are no matches, returns + /// `nil`. + /// + /// - parameter string: The string to match against. + /// + /// - returns: An optional `Match` describing the first match, or `nil`. + public func firstMatch(in string: String) -> Match? { + let firstMatch = regularExpression + .firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) + .map { Match(result: $0, in: string) } + return firstMatch + } + + /// If the regex matches `string`, returns an array of `Match`, describing + /// every match inside `string`. If there are no matches, returns an empty + /// array. + /// + /// - parameter string: The string to match against. + /// + /// - returns: An array of `Match` describing every match in `string`. + public func matches(in string: String) -> [Match] { + let matches = regularExpression + .matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) + .map { Match(result: $0, in: string) } + return matches + } + + // MARK: Replacing + /// Returns a new string where each substring matched by `regex` is replaced + /// with `template`. + /// + /// The template string may be a literal string, or include template variables: + /// the variable `$0` will be replaced with the entire matched substring, `$1` + /// with the first capture group, etc. + /// + /// For example, to include the literal string "$1" in the replacement string, + /// you must escape the "$": `\$1`. + /// + /// - parameters: + /// - regex: A regular expression to match against `self`. + /// - template: A template string used to replace matches. + /// - count: The maximum count of matches to replace, beginning with the first match. + /// + /// - returns: A string with all matches of `regex` replaced by `template`. + public func replacingMatches(in input: String, with template: String, count: Int? = nil) -> String { + var output = input + let matches = self.matches(in: input) + let rangedMatches = Array(matches[0 ..< min(matches.count, count ?? .max)]) + for match in rangedMatches.reversed() { + let replacement = match.string(applyingTemplate: template) + output.replaceSubrange(match.range, with: replacement) + } + + return output + } +} + +// MARK: - CustomStringConvertible +extension Regex: CustomStringConvertible { + /// Returns a string describing the regex using its pattern string. + public var description: String { + "Regex<\"\(regularExpression.pattern)\">" + } +} + +// 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 + } +} + +// MARK: - Hashable +extension Regex: Hashable { + /// Manages hashing of the `Regex` instance. + public func hash(into hasher: inout Hasher) { + hasher.combine(regularExpression) + } +} + +// MARK: - Options +extension Regex { + /// `Options` defines alternate behaviours of regular expressions when matching. + public struct Options: OptionSet { + // MARK: - Properties + /// Ignores the case of letters when matching. + public static let ignoreCase = Options(rawValue: 1) + + /// Ignore any metacharacters in the pattern, treating every character as + /// a literal. + public static let ignoreMetacharacters = Options(rawValue: 1 << 1) + + /// By default, "^" matches the beginning of the string and "$" matches the + /// end of the string, ignoring any newlines. With this option, "^" will + /// the beginning of each line, and "$" will match the end of each line. + public static let anchorsMatchLines = Options(rawValue: 1 << 2) + + /// Usually, "." matches all characters except newlines (\n). Using this, + /// options will allow "." to match newLines + public static let dotMatchesLineSeparators = Options(rawValue: 1 << 3) + + /// The raw value of the `OptionSet` + public let rawValue: Int + + /// Transform an instance of `Regex.Options` into the equivalent `NSRegularExpression.Options`. + /// + /// - returns: The equivalent `NSRegularExpression.Options`. + var toNSRegularExpressionOptions: NSRegularExpression.Options { + var options = NSRegularExpression.Options() + if contains(.ignoreCase) { options.insert(.caseInsensitive) } + if contains(.ignoreMetacharacters) { options.insert(.ignoreMetacharacters) } + if contains(.anchorsMatchLines) { options.insert(.anchorsMatchLines) } + if contains(.dotMatchesLineSeparators) { options.insert(.dotMatchesLineSeparators) } + return options + } + + // MARK: - Initializers + /// The raw value init for the `OptionSet` + public init(rawValue: Int) { + self.rawValue = rawValue + } + } +} + +// 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 + if let captureRange = captureRange { + return String(describing: self.baseString[captureRange]) + } + + return nil + } + }() + + private let result: NSTextCheckingResult + + private 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/Tests/AnyLintTests/FilesSearchTests.swift b/Tests/AnyLintTests/FilesSearchTests.swift index 9d60838..115f919 100644 --- a/Tests/AnyLintTests/FilesSearchTests.swift +++ b/Tests/AnyLintTests/FilesSearchTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -import HandySwift @testable import Utility import XCTest diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index 623d7b7..7d0444d 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -1,5 +1,4 @@ @testable import AnyLint -import HandySwift @testable import Utility import XCTest diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/UtilityTests/Extensions/RegexExtTests.swift index ef0ad54..9c90a31 100644 --- a/Tests/UtilityTests/Extensions/RegexExtTests.swift +++ b/Tests/UtilityTests/Extensions/RegexExtTests.swift @@ -1,4 +1,3 @@ -import HandySwift @testable import Utility import XCTest From a8c7d87728e650afbea5f45d13f7a0405dc1e937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Wed, 18 Mar 2020 09:50:10 +0100 Subject: [PATCH 19/27] Improve logging & fix AnyLint issue --- Sources/AnyLint/CheckInfo.swift | 7 +++++++ Sources/AnyLint/Extensions/StringExt.swift | 4 ++++ Sources/AnyLint/Lint.swift | 8 +++++++- Sources/AnyLint/Statistics.swift | 14 +++++++------- Sources/AnyLint/Violation.swift | 3 ++- Sources/Utility/Logger.swift | 2 +- Tests/AnyLintTests/StatisticsTests.swift | 2 +- Tests/AnyLintTests/ViolationTests.swift | 7 ++++--- 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index 58c8a90..ed93cce 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -11,6 +11,13 @@ public struct CheckInfo { /// 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) { + self.id = id + self.hint = hint + self.severity = severity + } } extension CheckInfo: Hashable { diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index a5d8982..c15b77a 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -1,4 +1,8 @@ import Foundation +import Utility + +/// `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. diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 970cc90..cb58dbe 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -32,6 +32,8 @@ public enum Lint { ) let violations = FileContentsChecker(checkInfo: checkInfo, regex: regex, filePathsToCheck: filePathsToCheck).performCheck() + + violations.forEach { $0.logMessage() } Statistics.shared.found(violations: violations, in: checkInfo) } @@ -72,6 +74,7 @@ public enum Lint { violateIfNoMatchesFound: violateIfNoMatchesFound ).performCheck() + violations.forEach { $0.logMessage() } Statistics.shared.found(violations: violations, in: checkInfo) } @@ -81,7 +84,10 @@ public enum Lint { /// - 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: () -> [Violation]) { - Statistics.shared.found(violations: customClosure(), in: checkInfo) + let violations = customClosure() + + violations.forEach { $0.logMessage() } + Statistics.shared.found(violations: violations, in: checkInfo) } /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index e0e95b2..ab981aa 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -9,7 +9,7 @@ final class Statistics { var violationsBySeverity: [Severity: [Violation]] = [.info: [], .warning: [], .error: []] var maxViolationSeverity: Severity? { - violationsBySeverity.keys.max { $0.rawValue < $1.rawValue } + violationsBySeverity.keys.filter { !violationsBySeverity[$0]!.isEmpty }.max { $0.rawValue < $1.rawValue } } private init() {} @@ -30,16 +30,16 @@ final class Statistics { func logSummary() { if executedChecks.isEmpty { log.message("No checks found to perform.", level: .warning) - } else if violationsBySeverity.isEmpty { - log.message("Performed \(executedChecks.count) checks without any violations.", level: .info) - } else { - let errors = "\(violationsBySeverity[.error]!.count) errors" - let warnings = "\(violationsBySeverity[.warning]!.count) warnings" + } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { + let errors = "\(violationsBySeverity[.error]!.count) error(s)" + let warnings = "\(violationsBySeverity[.warning]!.count) warning(s)" log.message( - "Performed \(executedChecks.count) checks and found \(errors) & \(warnings).", + "Performed \(executedChecks.count) check(s) and found \(errors) & \(warnings).", level: maxViolationSeverity!.logLevel ) + } else { + log.message("Performed \(executedChecks.count) check(s) without any violations.", level: .success) } } } diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index 723db5a..bb845ee 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -1,4 +1,5 @@ import Foundation +import Rainbow import Utility /// A violation found in a check. @@ -19,7 +20,7 @@ public struct Violation { } func logMessage() { - let checkInfoMessage = "[\(checkInfo.id)] \(checkInfo.hint)" + let checkInfoMessage = "\("[\(checkInfo.id)]".bold) \(checkInfo.hint)" guard let filePath = filePath else { log.message(checkInfoMessage, level: checkInfo.severity.logLevel) diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index b5b7562..89a6dca 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -99,7 +99,7 @@ public final class Logger { private func consoleMessage(_ message: String, level: PrintLevel) { switch level { case .success: - print(formattedCurrentTime(), "✅ ", message.lightGreen) + print(formattedCurrentTime(), "✅ ", message.green) case .info: print(formattedCurrentTime(), "ℹ️ ", message.lightBlue) diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index d59f108..551e2b8 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -80,6 +80,6 @@ final class StatisticsTests: XCTestCase { Statistics.shared.logSummary() XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .error) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Performed 3 checks and found 3 errors & 2 warnings.") + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Performed 3 check(s) and found 3 error(s) & 2 warning(s).") } } diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift index dbb9f98..846a02d 100644 --- a/Tests/AnyLintTests/ViolationTests.swift +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -1,4 +1,5 @@ @testable import AnyLint +import Rainbow @testable import Utility import XCTest @@ -15,18 +16,18 @@ final class ViolationTests: XCTestCase { XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "[demo_check] Make sure to always check the demo.") + XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "\("[demo_check]".bold) Make sure to always check the demo.") Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift").logMessage() XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Temp/Souces/Hello.swift: [demo_check] Make sure to always check the demo.") + XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Temp/Souces/Hello.swift: \("[demo_check]".bold) Make sure to always check the demo.") Violation(checkInfo: checkInfo, filePath: "Temp/Souces/World.swift", locationInfo: String.LocationInfo(line: 5, charInLine: 15)).logMessage() XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 3) XCTAssertEqual(TestHelper.shared.consoleOutputs[2].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[2].message, "Temp/Souces/World.swift:5:15: [demo_check] Make sure to always check the demo.") + XCTAssertEqual(TestHelper.shared.consoleOutputs[2].message, "Temp/Souces/World.swift:5:15: \("[demo_check]".bold) Make sure to always check the demo.") } } From 722761c20e6c0bd3902552aee9b80ac49e3de058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Wed, 18 Mar 2020 16:00:44 +0100 Subject: [PATCH 20/27] Improve log output & add autocorrect examples --- Sources/AnyLint/Extensions/StringExt.swift | 5 +++ Sources/AnyLint/Lint.swift | 43 ++++++++++++++-------- Sources/AnyLint/Statistics.swift | 18 +++++++++ Sources/AnyLint/Violation.swift | 18 ++------- Tests/AnyLintTests/LintTests.swift | 6 +++ Tests/AnyLintTests/StatisticsTests.swift | 37 ++++++++++++++++--- Tests/AnyLintTests/StringExtTests.swift | 18 +++++++++ Tests/AnyLintTests/ViolationTests.swift | 25 +++++-------- lint.swift | 43 +++++++++++++++++++++- 9 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 Tests/AnyLintTests/StringExtTests.swift diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index c15b77a..2338232 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -8,6 +8,11 @@ extension String { /// Info about the exact location of a character in a given file. public typealias LocationInfo = (line: Int, charInLine: Int) + /// Removes any newlines between capture groups in regexes. + public func removeNewlinesBetweenCaptureGroups() -> String { + components(separatedBy: ")\n(").joined(separator: ")(") + } + func locationInfo(of index: String.Index) -> LocationInfo { let prefix = self[startIndex ..< index] let prefixLines = prefix.split(separator: "\n") diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index cb58dbe..7f0f0ba 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -3,28 +3,37 @@ import Utility /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { + /// Example String tuples with a `before` and `after` autocorrection matching String. + public typealias AutoCorrectExample = (before: String, after: String) + /// 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. - /// - 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. + /// - autoCorrectExamples: An array of example tuples with a `before` and an `after` String object to check if autocorrection works properly. public static func checkFileContents( checkInfo: CheckInfo, regex: Regex, + matchingExamples: [String] = [], + nonMatchingExamples: [String] = [], includeFilters: [Regex] = [#".*"#], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [] + autoCorrectExamples: [AutoCorrectExample] = [] ) { validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + if let autoCorrectReplacement = autoCorrectReplacement { + validateAutocorrectsAll(examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement) + } + let filePathsToCheck: [String] = FilesSearch.allFiles( within: fileManager.currentDirectoryPath, includeFilters: includeFilters, @@ -32,8 +41,6 @@ public enum Lint { ) let violations = FileContentsChecker(checkInfo: checkInfo, regex: regex, filePathsToCheck: filePathsToCheck).performCheck() - - violations.forEach { $0.logMessage() } Statistics.shared.found(violations: violations, in: checkInfo) } @@ -42,25 +49,31 @@ public enum Lint { /// - 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. - /// - 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. + /// - autoCorrectExamples: An array of example tuples 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, - matchingExamples: [String] = [], - nonMatchingExamples: [String] = [], + autoCorrectExamples: [AutoCorrectExample] = [], violateIfNoMatchesFound: Bool = false ) { validate(regex: regex, matchesForEach: matchingExamples, checkInfo: checkInfo) validate(regex: regex, doesNotMatchAny: nonMatchingExamples, checkInfo: checkInfo) + if let autoCorrectReplacement = autoCorrectReplacement { + validateAutocorrectsAll(examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement) + } + let filePathsToCheck: [String] = FilesSearch.allFiles( within: fileManager.currentDirectoryPath, includeFilters: includeFilters, @@ -74,7 +87,6 @@ public enum Lint { violateIfNoMatchesFound: violateIfNoMatchesFound ).performCheck() - violations.forEach { $0.logMessage() } Statistics.shared.found(violations: violations, in: checkInfo) } @@ -84,10 +96,7 @@ public enum Lint { /// - 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: () -> [Violation]) { - let violations = customClosure() - - violations.forEach { $0.logMessage() } - Statistics.shared.found(violations: violations, in: checkInfo) + Statistics.shared.found(violations: customClosure(), in: checkInfo) } /// Logs the summary of all detected violations and exits successfully on no violations or with a failure, if any violations. @@ -128,4 +137,8 @@ public enum Lint { } } } + + static func validateAutocorrectsAll(examples: [AutoCorrectExample], regex: Regex, autocorrectReplacement: String) { + // TODO: [cg_2020-03-18] not yet implemented + } } diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index ab981aa..d653c4c 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -31,6 +31,24 @@ final class Statistics { if executedChecks.isEmpty { log.message("No checks found to perform.", level: .warning) } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { + for check in executedChecks { + if let checkViolations = violationsPerCheck[check], checkViolations.isFilled { + let violationLocationMessages = checkViolations.compactMap { $0.locationMessage() } + + if violationLocationMessages.isFilled { + log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:", level: check.severity.logLevel) + let numerationDigits = String(violationLocationMessages.count).count + + for (index, locationMessage) in violationLocationMessages.enumerated() { + let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) + log.message("> \(violationNumString). " + locationMessage, level: check.severity.logLevel) + } + } 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)" diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index bb845ee..a3c95e0 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -19,19 +19,9 @@ public struct Violation { self.locationInfo = locationInfo } - func logMessage() { - let checkInfoMessage = "\("[\(checkInfo.id)]".bold) \(checkInfo.hint)" - - guard let filePath = filePath else { - log.message(checkInfoMessage, level: checkInfo.severity.logLevel) - return - } - - guard let locationInfo = locationInfo else { - log.message("\(filePath): \(checkInfoMessage)", level: checkInfo.severity.logLevel) - return - } - - log.message("\(filePath):\(locationInfo.line):\(locationInfo.charInLine): \(checkInfoMessage)", level: checkInfo.severity.logLevel) + func locationMessage() -> String? { + guard let filePath = filePath else { return nil } + guard let locationInfo = locationInfo else { return filePath } + return "\(filePath):\(locationInfo.line):\(locationInfo.charInLine)" } } diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index 7d0444d..5e2d276 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -49,4 +49,10 @@ final class LintTests: XCTestCase { ) XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } + + func testValidateAutocorrectsAllExamples() { + XCTAssertNil(TestHelper.shared.exitStatus) + + // TODO: [cg_2020-03-18] not yet implemented + } } diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 551e2b8..08b333f 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -1,4 +1,5 @@ @testable import AnyLint +import Rainbow @testable import Utility import XCTest @@ -59,6 +60,8 @@ final class StatisticsTests: XCTestCase { 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)], @@ -67,19 +70,43 @@ final class StatisticsTests: XCTestCase { let checkInfo2 = CheckInfo(id: "id2", hint: "hint2", severity: .warning) Statistics.shared.found( - violations: [Violation(checkInfo: checkInfo2), Violation(checkInfo: checkInfo2)], + 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), Violation(checkInfo: checkInfo3), Violation(checkInfo: checkInfo3)], + 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.logSummary() - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .error) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Performed 3 check(s) and found 3 error(s) & 2 warning(s).") + + XCTAssertEqual( + TestHelper.shared.consoleOutputs.map { $0.level }, + [.info, .warning, .warning, .warning, .error, .error, .error, .error, .error] + ) + + XCTAssertEqual( + TestHelper.shared.consoleOutputs.map { $0.message }, + [ + "\("[id1]".bold) Found 1 violation(s).", + "\("[id2]".bold) Found 2 violation(s) at:", + "> 1. Hogwarts/Harry.swift", + "> 2. Hogwarts/Albus.swift", + "\("[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", + "Performed 3 check(s) and found 3 error(s) & 2 warning(s).", + ] + ) } } diff --git a/Tests/AnyLintTests/StringExtTests.swift b/Tests/AnyLintTests/StringExtTests.swift new file mode 100644 index 0000000..d8d5584 --- /dev/null +++ b/Tests/AnyLintTests/StringExtTests.swift @@ -0,0 +1,18 @@ +@testable import AnyLint +import Rainbow +@testable import Utility +import XCTest + +final class StringExtTests: XCTestCase { + func testRemoveNewlinesBetweenCaptureGroups() { + XCTAssertEqual( + """ + A + (?B) + (?C) + D + """.removeNewlinesBetweenCaptureGroups(), + "A\n(?B)(?C)\nD" + ) + } +} diff --git a/Tests/AnyLintTests/ViolationTests.swift b/Tests/AnyLintTests/ViolationTests.swift index 846a02d..96e4a7d 100644 --- a/Tests/AnyLintTests/ViolationTests.swift +++ b/Tests/AnyLintTests/ViolationTests.swift @@ -10,24 +10,19 @@ final class ViolationTests: XCTestCase { Statistics.shared.reset() } - func testLogMessage() { + func testLocationMessage() { let checkInfo = CheckInfo(id: "demo_check", hint: "Make sure to always check the demo.", severity: .warning) - Violation(checkInfo: checkInfo).logMessage() + XCTAssertNil(Violation(checkInfo: checkInfo).locationMessage()) - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[0].message, "\("[demo_check]".bold) Make sure to always check the demo.") + let fileViolation = Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift") + XCTAssertEqual(fileViolation.locationMessage(), "Temp/Souces/Hello.swift") - Violation(checkInfo: checkInfo, filePath: "Temp/Souces/Hello.swift").logMessage() + let locationInfoViolation = Violation( + checkInfo: checkInfo, + filePath: "Temp/Souces/World.swift", + locationInfo: String.LocationInfo(line: 5, charInLine: 15) + ) - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 2) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[1].message, "Temp/Souces/Hello.swift: \("[demo_check]".bold) Make sure to always check the demo.") - - Violation(checkInfo: checkInfo, filePath: "Temp/Souces/World.swift", locationInfo: String.LocationInfo(line: 5, charInLine: 15)).logMessage() - - XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 3) - XCTAssertEqual(TestHelper.shared.consoleOutputs[2].level, .warning) - XCTAssertEqual(TestHelper.shared.consoleOutputs[2].message, "Temp/Souces/World.swift:5:15: \("[demo_check]".bold) Make sure to always check the demo.") + XCTAssertEqual(locationInfoViolation.locationMessage(), "Temp/Souces/World.swift:5:15") } } diff --git a/lint.swift b/lint.swift index 23e8133..99132ed 100755 --- a/lint.swift +++ b/lint.swift @@ -1,6 +1,47 @@ #!/usr/local/bin/swift-sh import AnyLint // . -// TODO: [cg_2020-03-11] not yet implemented +// MARK: - Reusables +let swiftSourceFiles: Regex = #"Sources/.*\.swift"# +let swiftTestFiles: Regex = #"Tests/.*\.swift"# + +// MARK: - File Content Checks +Lint.checkFileContents( + checkInfo: CheckInfo( + id: "closure_params_parantheses", + hint: "Don't use parantheses around non-typed parameters in a closure.", + severity: .error + ), + regex: try Regex( + #""" + (?\{\s*) + (?\() + (?(?!self)[^):]+) + (?\)) + (?\s*in) + """#.removeNewlinesBetweenCaptureGroups() + ), + matchingExamples: ["closure = { (param) in", "func do() { (param) in"], + nonMatchingExamples: ["closure { (self) in", "func do() { (self) in"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$prefix$parameters$suffix", + autoCorrectExamples: [ + (before: "closure = { (param) in", after: "closure { param in"), + (before: "func do() { (param) in", after: "func do() { param in"), + ] +) + +// MARK: - File Path Checks +Lint.checkFilePaths( + checkInfo: CheckInfo( + id: "readme", + hint: "Each project should have a README.md file, explaining how to use or contribute to the project.", + severity: .error + ), + regex: #"^README\.md$"#, + matchingExamples: ["README.md"], + nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], + violateIfNoMatchesFound: true +) Lint.logSummaryAndExit() From 807c9f16eb6596b8713b372acf5be7307cb4d22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 19 Mar 2020 09:59:09 +0100 Subject: [PATCH 21/27] Complete autocorrection check & support named capture groups --- .swiftlint.yml | 249 --------------------- Sources/AnyLint/Extensions/StringExt.swift | 5 - Sources/AnyLint/Lint.swift | 23 +- Sources/AnyLint/Statistics.swift | 2 + Sources/Utility/Extensions/RegexExt.swift | 12 + Sources/Utility/Logger.swift | 2 - Sources/Utility/Regex.swift | 4 + Tests/AnyLintTests/LintTests.swift | 60 ++++- Tests/AnyLintTests/RegexExtTests.swift | 18 ++ Tests/AnyLintTests/StatisticsTests.swift | 11 +- Tests/AnyLintTests/StringExtTests.swift | 18 -- lint.swift | 37 +-- 12 files changed, 144 insertions(+), 297 deletions(-) create mode 100644 Tests/AnyLintTests/RegexExtTests.swift delete mode 100644 Tests/AnyLintTests/StringExtTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 184cd5a..6409aac 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -119,252 +119,3 @@ trailing_comma: trailing_whitespace: ignores_comments: false - -# Custom Rules -custom_rules: - class_name_suffix_collection_view_controller: - included: ".*.swift" - regex: 'class +\w+(?]+>)? *: +\w+CollectionViewController' - name: "Class Name Suffix View Controller" - message: "All `CollectionViewController` subclasses should end on `CollectionViewController`." - severity: warning - class_name_suffix_table_view_controller: - included: ".*.swift" - regex: 'class +\w+(?]+>)? *: +\w+TableViewController' - name: "Class Name Suffix View Controller" - message: "All `TableViewController` subclasses should end on `TableViewController`." - severity: warning - class_name_suffix_view_controller: - included: ".*.swift" - regex: 'class +\w+(?]+>)? *: +\w+ViewController' - name: "Class Name Suffix View Controller" - message: "All `ViewController` subclasses should end on `ViewController`." - severity: warning - closure_params_parantheses: - included: ".*.swift" - regex: '\{\s*\((?!self)[^):]+\)\s*in' - name: "Unnecessary Closure Params Parantheses" - message: "Don't use parantheses around non-typed parameters in a closure." - severity: warning - comment_type_note: - included: ".*.swift" - regex: '// *(?:WORKAROUND|HACK|WARNING)[:\\s]' - name: "Comment Type NOTE" - message: "Use a '// NOTE:' comment instead." - severity: warning - comment_type_refactor: - included: ".*.swift" - regex: '// *(?:TODO|NOTE)[:\\s][^\n]*(?:refactor|REFACTOR|Refactor)' - name: "Comment Type REFACTOR" - message: "Use a '// REFACTOR:' comment instead." - severity: warning - comment_type_todo: - included: ".*.swift" - regex: '// *(?:BUG|MOCK|FIXME|RELEASE|TEST)[:\\s]' - name: "Comment Type TODO" - message: "Use a '// TODO:' comment instead." - severity: warning - controller_class_name_suffix: - included: ".*.swift" - regex: 'class +\w+(?\w+)(?:<[^\>]+>)? *\{.*static let `default`(?:: *\k)? *= *\k\(.*(?<=private) init\(' - name: "Singleton Default Private Init" - message: "Singletons with a `default` object (pseudo-singletons) should not declare init methods as private." - severity: warning - singleton_shared_final: - included: ".*.swift" - regex: '(?\w+)(?:<[^\>]+>)? *\{.*static let shared(?:: *\k)? *= *\k\(' - name: "Singleton Shared Final" - message: "Singletons with a single object (`shared`) should be marked as final." - severity: warning - singleton_shared_private_init: - included: ".*.swift" - regex: 'class +(?\w+)(?:<[^\>]+>)? *\{.*static let shared(?:: *\k)? *= *\k\(.*(?<= |\t|public|internal) init\(' - name: "Singleton Shared Private Init" - message: "Singletons with a single object (`shared`) should declare their init method(s) as private." - severity: warning - singleton_shared_single_object: - included: ".*.swift" - regex: 'class +(?\w+)(?:<[^\>]+>)? *\{.*(?:static let shared(?:: *\k)? *= *\k\(.*static let \w+(?:: *\k)? *= *\k\(|static let \w+(?:: *\k)? *= *\k\(.*static let shared(?:: *\k)? *= *\k\()' - name: "Singleton Shared Single Object" - message: "Singletons with a `shared` object (real Singletons) should not have other static let properties. Use `default` instead (if needed)." - severity: warning - switch_associated_value_style: - included: ".*.swift" - regex: 'case\s+[^\(][^\n]*(?:\(let |[^\)], let)' - name: "Switch Associated Value Style" - message: "Always put the `let` in front of case – even if only one associated value captured." - severity: warning - todo_format: - included: ".*.swift" - regex: '\/\/ TODO: [^\n]{0,14}\n|\/\/ TODO: \[\S{1,12}\]|\/\/ TODO: [^\[]|\/\/ TODO: \[.{13}[^\]]|\/\/ TODO: \[[^a-z]{2}|\/\/ TODO: \[.{2}[^_]|\/\/ TODO: \[.{7}[^-]|\/\/ TODO: \[.{10}[^-]' - name: "Todo Date" - message: "All TODOs should have a format with creator credentials & date of their creation documented like this: `// TODO: [cg_YYYY-MM-DD] `." - severity: warning - todo_uppercase: - included: ".*.swift" - regex: '\/\/ ?tODO|\/\/ ?ToDO|\/\/ ?TOdO|\/\/ ?TODo|\/\/ ?todo|\/\/ ?Todo|\/\/ ?ToDo|\/\/ ?toDo' - name: "Todo Uppercase" - message: "All TODOs should be all-uppercased like this: `// TODO: [cg_YYYY-MM-DD] `." - severity: warning - todo_whitespacing: - included: ".*.swift" - regex: '\/\/TODO|\/\/ TODO\s|\/\/ TODO:[^ ]|\/\/ TODO: |\/\/ TODO: \[[^\s]{0,10}\][^ ]' - name: "Todo Whitespace" - message: "All TODOs should exactly start like this (mind the whitespacing): `// TODO: [cg_YYYY-MM-DD] `." - severity: warning - tuple_index: - included: ".*.swift" - regex: '(\$\d|\w*[^\d \(\[\{])\.\d' - name: "Tuple Index" - message: "Prevent unwraping tuples by their index – define a typealias with named components instead." - severity: warning - unnecessary_case_break: - included: ".*.swift" - regex: '(case |default)(?:[^\n\}]+\n){2,}\s*break *\n|\n *\n *break(?:\n *\n|\n *\})' - name: "Unnecessary Case Break" - message: "Don't use break in switch cases – Swift breaks by default." - severity: warning - unnecessary_nil_assignment: - included: ".*.swift" - regex: 'var \S+\s*:\s*[^\s]+\?\s*=\s*nil' - name: "Unnecessary Nil Assignment" - message: "Don't assign nil as a value when defining an optional type – it's nil by default." - severity: warning - vertical_whitespaces_around_mark: - included: ".*.swift" - regex: '\/\/\s*MARK:[^\n]*(\n\n)|(\n\n\n)[ \t]*\/\/\s*MARK:|[^\s{]\n[^\n\/]*\/\/\s*MARK:' - name: "Vertical Whitespaces Around MARK:" - message: "Include a single vertical whitespace (empty line) before and none after MARK: comments." - severity: warning - view_controller_variable_naming: - included: ".*.swift" - regex: '(?:let|var) +\w*(?:vc|VC|Vc|viewC|viewController|ViewController) *=' - name: "View Controller Variable Naming" - message: "Always name your view controller variables with the suffix `ViewCtrl`." - severity: warning - whitespace_around_range_operators: - included: ".*.swift" - regex: '\w\.\.[<\.]\w' - name: "Whitespace around Range Operators" - message: "A range operator should be surrounded by a single whitespace." - severity: warning - whitespace_comment_start: - included: ".*.swift" - regex: '[^:#\]\}\)][^:#\]\}\)]\/\/[^\s\/]' - name: "Whitespace Comment Start" - message: "A comment should always start with a whitespace." - severity: warning diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index 2338232..c15b77a 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -8,11 +8,6 @@ extension String { /// Info about the exact location of a character in a given file. public typealias LocationInfo = (line: Int, charInLine: Int) - /// Removes any newlines between capture groups in regexes. - public func removeNewlinesBetweenCaptureGroups() -> String { - components(separatedBy: ")\n(").joined(separator: ")(") - } - func locationInfo(of index: String.Index) -> LocationInfo { let prefix = self[startIndex ..< index] let prefixLines = prefix.split(separator: "\n") diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index 7f0f0ba..bd7de24 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -139,6 +139,27 @@ public enum Lint { } static func validateAutocorrectsAll(examples: [AutoCorrectExample], regex: Regex, autocorrectReplacement: String) { - // TODO: [cg_2020-03-18] not yet implemented + for (before, after) in examples { + let autocorrected = regex.replacingMatches( + in: before, + with: numerizedNamedCaptureRefs(in: autocorrectReplacement, relatedRegex: regex) + ) + if autocorrected != after { + log.message( + "Autocorrecting example '\(before)' did not result in expected output. Expected '\(after)' but got '\(autocorrected)' instead.", + level: .error + ) + log.exit(status: .failure) + } + } + } + + /// Numerizes references to named capture groups to work around missing named capture group replacement in `NSRegularExpression` APIs. + static func numerizedNamedCaptureRefs(in replacementString: String, relatedRegex: Regex) -> String { + let captureGroupNameRegex = Regex(#"\(\?\<([a-zA-Z0-9_-]+)\>[^\)]+\)"#) + let captureGroupNames: [String] = captureGroupNameRegex.matches(in: relatedRegex.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/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index d653c4c..5a8621e 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -46,6 +46,8 @@ final class Statistics { } 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) } } diff --git a/Sources/Utility/Extensions/RegexExt.swift b/Sources/Utility/Extensions/RegexExt.swift index 17226ef..f0d6640 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -11,3 +11,15 @@ extension Regex: ExpressibleByStringLiteral { } } } + +extension Regex: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, String)...) { + do { + self = try Regex(elements.reduce(into: "") { result, element in result.append("(?<\(element.0)>\(element.1))") }) + } 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 + } + } +} diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index 89a6dca..b3d2ac3 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -1,8 +1,6 @@ import Foundation import Rainbow -// swiftlint:disable logger - /// Helper to log output to console or elsewhere. public final class Logger { /// The print level type. diff --git a/Sources/Utility/Regex.swift b/Sources/Utility/Regex.swift index 158d87c..9eeea31 100644 --- a/Sources/Utility/Regex.swift +++ b/Sources/Utility/Regex.swift @@ -7,6 +7,9 @@ public struct Regex { // MARK: - Properties private let regularExpression: NSRegularExpression + /// The regex patterns string. + public let pattern: String + // MARK: - Initializers /// Create a `Regex` based on a pattern string. /// @@ -20,6 +23,7 @@ public struct Regex { /// /// - throws: A value of `ErrorType` describing the invalid regular expression. public init(_ pattern: String, options: Options = []) throws { + self.pattern = pattern regularExpression = try NSRegularExpression( pattern: pattern, options: options.toNSRegularExpressionOptions diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index 5e2d276..704baa8 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -50,9 +50,65 @@ final class LintTests: XCTestCase { XCTAssertEqual(TestHelper.shared.exitStatus, .failure) } - func testValidateAutocorrectsAllExamples() { + func testValidateAutocorrectsAllExamplesWithAnonymousGroups() { XCTAssertNil(TestHelper.shared.exitStatus) - // TODO: [cg_2020-03-18] not yet implemented + let anonymousCaptureRegex = try? Regex(#"([^\.]+)(\.)([^\.]+)(\.)([^\.]+)"#) + + Lint.validateAutocorrectsAll( + examples: [ + (before: "prefix.content.suffix", after: "suffix.content.prefix"), + (before: "forums.swift.org", after: "org.swift.forums"), + ], + regex: anonymousCaptureRegex!, + autocorrectReplacement: "$5$2$3$4$1" + ) + + XCTAssertNil(TestHelper.shared.exitStatus) + + Lint.validateAutocorrectsAll( + examples: [ + (before: "prefix.content.suffix", after: "suffix.content.prefix"), + (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( + examples: [ + (before: "prefix.content.suffix", after: "suffix.content.prefix"), + (before: "forums.swift.org", after: "org.swift.forums"), + ], + regex: namedCaptureRegex, + autocorrectReplacement: "$suffix$separator1$content$separator2$prefix" + ) + + XCTAssertNil(TestHelper.shared.exitStatus) + + Lint.validateAutocorrectsAll( + examples: [ + (before: "prefix.content.suffix", after: "suffix.content.prefix"), + (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 new file mode 100644 index 0000000..b729aa7 --- /dev/null +++ b/Tests/AnyLintTests/RegexExtTests.swift @@ -0,0 +1,18 @@ +@testable import AnyLint +@testable import Utility +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 index 08b333f..6504c1d 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -91,20 +91,23 @@ final class StatisticsTests: XCTestCase { XCTAssertEqual( TestHelper.shared.consoleOutputs.map { $0.level }, - [.info, .warning, .warning, .warning, .error, .error, .error, .error, .error] + [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] ) XCTAssertEqual( TestHelper.shared.consoleOutputs.map { $0.message }, [ - "\("[id1]".bold) Found 1 violation(s).", - "\("[id2]".bold) Found 2 violation(s) at:", + "[id1] Found 1 violation(s).", + ">> Hint: hint1", + "[id2] Found 2 violation(s) at:", "> 1. Hogwarts/Harry.swift", "> 2. Hogwarts/Albus.swift", - "\("[id3]".bold) Found 3 violation(s) at:", + ">> Hint: hint2", + "[id3] 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", "Performed 3 check(s) and found 3 error(s) & 2 warning(s).", ] ) diff --git a/Tests/AnyLintTests/StringExtTests.swift b/Tests/AnyLintTests/StringExtTests.swift deleted file mode 100644 index d8d5584..0000000 --- a/Tests/AnyLintTests/StringExtTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -@testable import AnyLint -import Rainbow -@testable import Utility -import XCTest - -final class StringExtTests: XCTestCase { - func testRemoveNewlinesBetweenCaptureGroups() { - XCTAssertEqual( - """ - A - (?B) - (?C) - D - """.removeNewlinesBetweenCaptureGroups(), - "A\n(?B)(?C)\nD" - ) - } -} diff --git a/lint.swift b/lint.swift index 99132ed..d09c838 100755 --- a/lint.swift +++ b/lint.swift @@ -8,26 +8,31 @@ let swiftTestFiles: Regex = #"Tests/.*\.swift"# // MARK: - File Content Checks Lint.checkFileContents( checkInfo: CheckInfo( - id: "closure_params_parantheses", - hint: "Don't use parantheses around non-typed parameters in a closure.", + id: "empty_method_body", + hint: "Don't use whitespace or newlines for the body of empty methods – use empty bodies like in `func doSomething() {}` instead.", severity: .error ), - regex: try Regex( - #""" - (?\{\s*) - (?\() - (?(?!self)[^):]+) - (?\)) - (?\s*in) - """#.removeNewlinesBetweenCaptureGroups() - ), - matchingExamples: ["closure = { (param) in", "func do() { (param) in"], - nonMatchingExamples: ["closure { (self) in", "func do() { (self) in"], + regex: ["declaration": #"(init|func [^\(\s]+)\((.|\n)*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], + matchingExamples: [ + "init() { }", + "init() {\n\n}", + "init(\n x: Int,\n y: Int\n) { }", + "func foo2bar() { }", + "func foo2bar(x: Int, y: Int) { }", + "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", + ], + nonMatchingExamples: ["init() { /* comment */ }", "init() {}", "func foo2bar() {}", "func foo2bar(x: Int, y: Int) {}"], includeFilters: [swiftSourceFiles, swiftTestFiles], - autoCorrectReplacement: "$prefix$parameters$suffix", + autoCorrectReplacement: "$declaration {}", autoCorrectExamples: [ - (before: "closure = { (param) in", after: "closure { param in"), - (before: "func do() { (param) in", after: "func do() { param in"), + (before: "init() { }", after: "init() {}"), + (before: "init(x: Int, y: Int) { }", after: "init(x: Int, y: Int) {}"), + (before: "init()\n{\n \n}", after: "init() {}"), + (before: "init(\n x: Int,\n y: Int\n) {\n \n}", after: "init(\n x: Int,\n y: Int\n) {}"), + (before: "func foo2bar() { }", after: "func foo2bar() {}"), + (before: "func foo2bar(x: Int, y: Int) { }", after: "func foo2bar(x: Int, y: Int) {}"), + (before: "func foo2bar()\n{\n \n}", after: "func foo2bar() {}"), + (before: "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", after: "func foo2bar(\n x: Int,\n y: Int\n) {}"), ] ) From 05ee7f7744036bba52e40cfdaab60986db2ea9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Thu, 19 Mar 2020 15:12:04 +0100 Subject: [PATCH 22/27] Implement autocorrection for file contents & paths --- Sources/AnyLint/AutoCorrection.swift | 25 ++++++ Sources/AnyLint/Checkers/Checker.swift | 2 +- .../Checkers/FileContentsChecker.swift | 25 +++++- .../AnyLint/Checkers/FilePathsChecker.swift | 17 +++- Sources/AnyLint/Extensions/StringExt.swift | 4 + Sources/AnyLint/Lint.swift | 79 +++++++++++++------ Sources/AnyLint/Statistics.swift | 23 ++++-- Sources/AnyLint/Violation.swift | 6 +- .../Utility/Extensions/FileManagerExt.swift | 40 ++++++++++ Sources/Utility/Extensions/RegexExt.swift | 16 ++++ .../Extensions/StringExt.swift | 10 ++- Sources/Utility/Logger.swift | 2 +- .../Checkers/FileContentsCheckerTests.swift | 5 +- .../Checkers/FilePathsCheckerTests.swift | 8 +- Tests/AnyLintTests/LintTests.swift | 16 ++-- lint.swift | 38 ++++++--- 16 files changed, 246 insertions(+), 70 deletions(-) create mode 100644 Sources/AnyLint/AutoCorrection.swift rename Sources/{AnyLintCLI/Globals => Utility}/Extensions/StringExt.swift (74%) diff --git a/Sources/AnyLint/AutoCorrection.swift b/Sources/AnyLint/AutoCorrection.swift new file mode 100644 index 0000000..51eadee --- /dev/null +++ b/Sources/AnyLint/AutoCorrection.swift @@ -0,0 +1,25 @@ +import Foundation + +/// Information about an autocorrection. +public struct AutoCorrection { + /// 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] { + [ + "Autocorrection applied (before >>> after):", + "> ✗ \(before.showNewlines())", + ">>>", + "> ✓ \(after.showNewlines())", + ] + } + + /// Initializes an autocorrection. + public init(before: String, after: String) { + self.before = before + self.after = after + } +} diff --git a/Sources/AnyLint/Checkers/Checker.swift b/Sources/AnyLint/Checkers/Checker.swift index 368abef..d0c0f56 100644 --- a/Sources/AnyLint/Checkers/Checker.swift +++ b/Sources/AnyLint/Checkers/Checker.swift @@ -1,5 +1,5 @@ import Foundation protocol Checker { - func performCheck() -> [Violation] + func performCheck() throws -> [Violation] } diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index 5eebf53..cf83b39 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -5,27 +5,44 @@ struct FileContentsChecker { let checkInfo: CheckInfo let regex: Regex let filePathsToCheck: [String] + let autoCorrectReplacement: String? } extension FileContentsChecker: Checker { - func performCheck() -> [Violation] { + func performCheck() throws -> [Violation] { var violations: [Violation] = [] for filePath in filePathsToCheck { if let fileData = fileManager.contents(atPath: filePath), let fileContents = String(data: fileData, encoding: .utf8) { - for match in regex.matches(in: fileContents) { + var newFileContents: String = fileContents + + for match in regex.matches(in: fileContents).reversed() { // TODO: [cg_2020-03-13] use capture group named 'pointer' if exists let locationInfo = fileContents.locationInfo(of: match.range.lowerBound) + let appliedAutoCorrection: AutoCorrection? = { + guard let autoCorrectReplacement = autoCorrectReplacement else { return nil } + + let newMatchString = regex.replaceAllCaptures(in: match.string, with: autoCorrectReplacement) + newFileContents.replaceSubrange(match.range, with: newMatchString) + + return AutoCorrection(before: match.string, after: newMatchString) + }() + // TODO: [cg_2020-03-13] autocorrect if autocorrection is available violations.append( Violation( checkInfo: checkInfo, filePath: filePath, - locationInfo: locationInfo + locationInfo: locationInfo, + appliedAutoCorrection: appliedAutoCorrection ) ) } + + 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.", @@ -34,6 +51,6 @@ extension FileContentsChecker: Checker { } } - return violations + return violations.reversed() } } diff --git a/Sources/AnyLint/Checkers/FilePathsChecker.swift b/Sources/AnyLint/Checkers/FilePathsChecker.swift index 0b4350a..8fcbfe3 100644 --- a/Sources/AnyLint/Checkers/FilePathsChecker.swift +++ b/Sources/AnyLint/Checkers/FilePathsChecker.swift @@ -5,25 +5,34 @@ struct FilePathsChecker { let checkInfo: CheckInfo let regex: Regex let filePathsToCheck: [String] + let autoCorrectReplacement: String? let violateIfNoMatchesFound: Bool } extension FilePathsChecker: Checker { - func performCheck() -> [Violation] { + func performCheck() throws -> [Violation] { var violations: [Violation] = [] if violateIfNoMatchesFound { let matchingFilePathsCount = filePathsToCheck.filter { regex.matches($0) }.count if matchingFilePathsCount <= 0 { violations.append( - Violation(checkInfo: checkInfo, filePath: nil, locationInfo: nil) + Violation(checkInfo: checkInfo, filePath: nil, locationInfo: nil, appliedAutoCorrection: nil) ) } } else { for filePath in filePathsToCheck where regex.matches(filePath) { - // TODO: [cg_2020-03-13] autocorrect if autocorrection is available + 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) + }() + violations.append( - Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil) + Violation(checkInfo: checkInfo, filePath: filePath, locationInfo: nil, appliedAutoCorrection: appliedAutoCorrection) ) } } diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index c15b77a..c73455f 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -16,4 +16,8 @@ extension String { let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 return (line: prefixLines.count + 1, charInLine: charInLine) } + + func showNewlines() -> String { + replacingOccurrences(of: "\n", with: #"\n"#).replacingOccurrences(of: "\t", with: #"\t"#) + } } diff --git a/Sources/AnyLint/Lint.swift b/Sources/AnyLint/Lint.swift index bd7de24..eef8d61 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -3,9 +3,6 @@ import Utility /// The linter type providing APIs for checking anything using regular expressions. public enum Lint { - /// Example String tuples with a `before` and `after` autocorrection matching String. - public typealias AutoCorrectExample = (before: String, after: String) - /// Checks the contents of files. /// /// - Parameters: @@ -16,7 +13,7 @@ public enum Lint { /// - 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 tuples with a `before` and an `after` String object to check if autocorrection works properly. + /// - autoCorrectExamples: An array of example structs with a `before` and an `after` String object to check if autocorrection works properly. public static func checkFileContents( checkInfo: CheckInfo, regex: Regex, @@ -25,10 +22,16 @@ public enum Lint { includeFilters: [Regex] = [#".*"#], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, - autoCorrectExamples: [AutoCorrectExample] = [] - ) { + autoCorrectExamples: [AutoCorrection] = [] + ) 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(examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement) @@ -40,7 +43,12 @@ public enum Lint { excludeFilters: excludeFilters ) - let violations = FileContentsChecker(checkInfo: checkInfo, regex: regex, filePathsToCheck: filePathsToCheck).performCheck() + let violations = try FileContentsChecker( + checkInfo: checkInfo, + regex: regex, + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement + ).performCheck() Statistics.shared.found(violations: violations, in: checkInfo) } @@ -54,7 +62,7 @@ public enum Lint { /// - 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 tuples with a `before` and an `after` String object to check if autocorrection works properly. + /// - 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, @@ -64,11 +72,17 @@ public enum Lint { includeFilters: [Regex] = [#".*"#], excludeFilters: [Regex] = [], autoCorrectReplacement: String? = nil, - autoCorrectExamples: [AutoCorrectExample] = [], + 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(examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement) @@ -80,10 +94,11 @@ public enum Lint { excludeFilters: excludeFilters ) - let violations = FilePathsChecker( + let violations = try FilePathsChecker( checkInfo: checkInfo, regex: regex, filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: autoCorrectReplacement, violateIfNoMatchesFound: violateIfNoMatchesFound ).performCheck() @@ -138,15 +153,15 @@ public enum Lint { } } - static func validateAutocorrectsAll(examples: [AutoCorrectExample], regex: Regex, autocorrectReplacement: String) { - for (before, after) in examples { - let autocorrected = regex.replacingMatches( - in: before, - with: numerizedNamedCaptureRefs(in: autocorrectReplacement, relatedRegex: regex) - ) - if autocorrected != after { + static func validateAutocorrectsAll(examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { + for autocorrect in examples { + let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) + if autocorrected != autocorrect.after { log.message( - "Autocorrecting example '\(before)' did not result in expected output. Expected '\(after)' but got '\(autocorrected)' instead.", + """ + Autocorrecting example '\(autocorrect.before)' did not result in expected output. + Expected '\(autocorrect.after)' but got '\(autocorrected)'. + """, level: .error ) log.exit(status: .failure) @@ -154,12 +169,26 @@ public enum Lint { } } - /// Numerizes references to named capture groups to work around missing named capture group replacement in `NSRegularExpression` APIs. - static func numerizedNamedCaptureRefs(in replacementString: String, relatedRegex: Regex) -> String { - let captureGroupNameRegex = Regex(#"\(\?\<([a-zA-Z0-9_-]+)\>[^\)]+\)"#) - let captureGroupNames: [String] = captureGroupNameRegex.matches(in: relatedRegex.pattern).map { $0.captures[0]! } - return captureGroupNames.enumerated().reduce(replacementString) { result, enumeratedGroupName in - result.replacingOccurrences(of: "$\(enumeratedGroupName.element)", with: "$\(enumeratedGroupName.offset + 1)") + 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/Statistics.swift b/Sources/AnyLint/Statistics.swift index 5a8621e..3546659 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -33,15 +33,26 @@ final class Statistics { } else if violationsBySeverity.values.contains(where: { $0.isFilled }) { for check in executedChecks { if let checkViolations = violationsPerCheck[check], checkViolations.isFilled { - let violationLocationMessages = checkViolations.compactMap { $0.locationMessage() } + let violationsWithLocationMessage = checkViolations.filter { $0.locationMessage() != nil } - if violationLocationMessages.isFilled { - log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s) at:", level: check.severity.logLevel) - let numerationDigits = String(violationLocationMessages.count).count + 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, locationMessage) in violationLocationMessages.enumerated() { + for (index, violation) in violationsWithLocationMessage.enumerated() { let violationNumString = String(format: "%0\(numerationDigits)d", index + 1) - log.message("> \(violationNumString). " + locationMessage, level: check.severity.logLevel) + let prefix = "> \(violationNumString). " + log.message(prefix + violation.locationMessage()!, level: check.severity.logLevel) + + if let appliedAutoCorrection = violation.appliedAutoCorrection { + let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined() + for messageLine in appliedAutoCorrection.appliedMessageLines { + log.message(prefixLengthWhitespaces + messageLine, level: .info) + } + } } } else { log.message("\("[\(check.id)]".bold) Found \(checkViolations.count) violation(s).", level: check.severity.logLevel) diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index a3c95e0..d203699 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -13,10 +13,14 @@ public struct Violation { /// The info about the exact location of the violation within the file. Will be ignored if no `filePath` specified. public let locationInfo: String.LocationInfo? - init(checkInfo: CheckInfo, filePath: String? = nil, locationInfo: String.LocationInfo? = nil) { + /// The autocorrection applied to fix this violation. + public let appliedAutoCorrection: AutoCorrection? + + init(checkInfo: CheckInfo, filePath: String? = nil, locationInfo: String.LocationInfo? = nil, appliedAutoCorrection: AutoCorrection? = nil) { self.checkInfo = checkInfo self.filePath = filePath self.locationInfo = locationInfo + self.appliedAutoCorrection = appliedAutoCorrection } func locationMessage() -> String? { diff --git a/Sources/Utility/Extensions/FileManagerExt.swift b/Sources/Utility/Extensions/FileManagerExt.swift index 2d014a4..188d608 100644 --- a/Sources/Utility/Extensions/FileManagerExt.swift +++ b/Sources/Utility/Extensions/FileManagerExt.swift @@ -5,4 +5,44 @@ extension FileManager { public var currentDirectoryUrl: URL { URL(string: currentDirectoryPath)! } + + /// 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 + } + + let targetParentDirectoryPath = targetPath.parentDirectoryPath + if !fileExists(atPath: targetParentDirectoryPath) { + try createDirectory(atPath: targetParentDirectoryPath, withIntermediateDirectories: true, attributes: nil) + } + + guard fileExistsAndIsDirectory(atPath: targetParentDirectoryPath) else { + log.message("Expected \(targetParentDirectoryPath) to be a directory.", level: .error) + log.exit(status: .failure) + return // only reachable in unit tests + } + + 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) + } + } + + /// 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 f0d6640..987b017 100644 --- a/Sources/Utility/Extensions/RegexExt.swift +++ b/Sources/Utility/Extensions/RegexExt.swift @@ -23,3 +23,19 @@ extension Regex: ExpressibleByDictionaryLiteral { } } } + +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)) + } + + /// 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/AnyLintCLI/Globals/Extensions/StringExt.swift b/Sources/Utility/Extensions/StringExt.swift similarity index 74% rename from Sources/AnyLintCLI/Globals/Extensions/StringExt.swift rename to Sources/Utility/Extensions/StringExt.swift index 3e609b6..8be934a 100644 --- a/Sources/AnyLintCLI/Globals/Extensions/StringExt.swift +++ b/Sources/Utility/Extensions/StringExt.swift @@ -1,8 +1,8 @@ import Foundation -import Utility extension String { - var absolutePath: String { + /// Returns the absolute path for a path given relative to the current directory. + public var absolutePath: String { guard let url = URL(string: self, relativeTo: fileManager.currentDirectoryUrl) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) log.exit(status: .failure) @@ -12,7 +12,8 @@ extension String { return url.absoluteString } - var parentDirectoryPath: String { + /// Returns the parent directory path. + public var parentDirectoryPath: String { guard let url = URL(string: self) else { log.message("Could not convert path '\(self)' to type URL.", level: .error) log.exit(status: .failure) @@ -22,7 +23,8 @@ extension String { return url.deletingLastPathComponent().absoluteString } - func appendingPathComponent(_ pathComponent: String) -> String { + /// 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) diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index b3d2ac3..27662d6 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -106,7 +106,7 @@ public final class Logger { print(formattedCurrentTime(), "⚠️ ", message.yellow) case .error: - print(formattedCurrentTime(), "❌ ", message.lightRed) + print(formattedCurrentTime(), "❌", message.lightRed) } } diff --git a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift index 7302a7b..5b33c5c 100644 --- a/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FileContentsCheckerTests.swift @@ -16,10 +16,11 @@ final class FileContentsCheckerTests: XCTestCase { withTemporaryFiles(temporaryFiles) { filePathsToCheck in let checkInfo = CheckInfo(id: "whitespacing", hint: "Always add a single whitespace around '='.", severity: .warning) - let violations = FileContentsChecker( + let violations = try FileContentsChecker( checkInfo: checkInfo, regex: #"(let|var) \w+=\w+"#, - filePathsToCheck: filePathsToCheck + filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil ).performCheck() XCTAssertEqual(violations.count, 2) diff --git a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift index da642a7..0d59ca7 100644 --- a/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift +++ b/Tests/AnyLintTests/Checkers/FilePathsCheckerTests.swift @@ -15,12 +15,12 @@ final class FilePathsCheckerTests: XCTestCase { (subpath: "Sources/World.swift", contents: ""), ] ) { filePathsToCheck in - let violations = sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 0) } withTemporaryFiles([(subpath: "Sources/World.swift", contents: "")]) { filePathsToCheck in - let violations = sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() + let violations = try sayHelloChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 1) @@ -36,7 +36,7 @@ final class FilePathsCheckerTests: XCTestCase { (subpath: "Sources/World.swift", contents: ""), ] ) { filePathsToCheck in - let violations = noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() + let violations = try noWorldChecker(filePathsToCheck: filePathsToCheck).performCheck() XCTAssertEqual(violations.count, 1) @@ -52,6 +52,7 @@ final class FilePathsCheckerTests: XCTestCase { checkInfo: sayHelloCheck(), regex: #".*Hello\.swift"#, filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, violateIfNoMatchesFound: true ) } @@ -65,6 +66,7 @@ final class FilePathsCheckerTests: XCTestCase { checkInfo: noWorldCheck(), regex: #".*World\.swift"#, filePathsToCheck: filePathsToCheck, + autoCorrectReplacement: nil, violateIfNoMatchesFound: false ) } diff --git a/Tests/AnyLintTests/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index 704baa8..fa9db23 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -57,8 +57,8 @@ final class LintTests: XCTestCase { Lint.validateAutocorrectsAll( examples: [ - (before: "prefix.content.suffix", after: "suffix.content.prefix"), - (before: "forums.swift.org", after: "org.swift.forums"), + 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" @@ -68,8 +68,8 @@ final class LintTests: XCTestCase { Lint.validateAutocorrectsAll( examples: [ - (before: "prefix.content.suffix", after: "suffix.content.prefix"), - (before: "forums.swift.org", after: "org.swift.forums"), + 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" @@ -91,8 +91,8 @@ final class LintTests: XCTestCase { Lint.validateAutocorrectsAll( examples: [ - (before: "prefix.content.suffix", after: "suffix.content.prefix"), - (before: "forums.swift.org", after: "org.swift.forums"), + 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" @@ -102,8 +102,8 @@ final class LintTests: XCTestCase { Lint.validateAutocorrectsAll( examples: [ - (before: "prefix.content.suffix", after: "suffix.content.prefix"), - (before: "forums.swift.org", after: "org.swift.forums"), + 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" diff --git a/lint.swift b/lint.swift index d09c838..99ed52b 100755 --- a/lint.swift +++ b/lint.swift @@ -6,13 +6,13 @@ let swiftSourceFiles: Regex = #"Sources/.*\.swift"# let swiftTestFiles: Regex = #"Tests/.*\.swift"# // MARK: - File Content Checks -Lint.checkFileContents( +try Lint.checkFileContents( checkInfo: CheckInfo( id: "empty_method_body", hint: "Don't use whitespace or newlines for the body of empty methods – use empty bodies like in `func doSomething() {}` instead.", severity: .error ), - regex: ["declaration": #"(init|func [^\(\s]+)\((.|\n)*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], + regex: ["declaration": #"(init|func [^\(\s]+)\([^{]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], matchingExamples: [ "init() { }", "init() {\n\n}", @@ -25,19 +25,19 @@ Lint.checkFileContents( includeFilters: [swiftSourceFiles, swiftTestFiles], autoCorrectReplacement: "$declaration {}", autoCorrectExamples: [ - (before: "init() { }", after: "init() {}"), - (before: "init(x: Int, y: Int) { }", after: "init(x: Int, y: Int) {}"), - (before: "init()\n{\n \n}", after: "init() {}"), - (before: "init(\n x: Int,\n y: Int\n) {\n \n}", after: "init(\n x: Int,\n y: Int\n) {}"), - (before: "func foo2bar() { }", after: "func foo2bar() {}"), - (before: "func foo2bar(x: Int, y: Int) { }", after: "func foo2bar(x: Int, y: Int) {}"), - (before: "func foo2bar()\n{\n \n}", after: "func foo2bar() {}"), - (before: "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", after: "func foo2bar(\n x: Int,\n y: Int\n) {}"), + AutoCorrection(before: "init() { }", after: "init() {}"), + AutoCorrection(before: "init(x: Int, y: Int) { }", after: "init(x: Int, y: Int) {}"), + AutoCorrection(before: "init()\n{\n \n}", after: "init() {}"), + AutoCorrection(before: "init(\n x: Int,\n y: Int\n) {\n \n}", after: "init(\n x: Int,\n y: Int\n) {}"), + AutoCorrection(before: "func foo2bar() { }", after: "func foo2bar() {}"), + AutoCorrection(before: "func foo2bar(x: Int, y: Int) { }", after: "func foo2bar(x: Int, y: Int) {}"), + AutoCorrection(before: "func foo2bar()\n{\n \n}", after: "func foo2bar() {}"), + AutoCorrection(before: "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", after: "func foo2bar(\n x: Int,\n y: Int\n) {}"), ] ) // MARK: - File Path Checks -Lint.checkFilePaths( +try Lint.checkFilePaths( checkInfo: CheckInfo( id: "readme", hint: "Each project should have a README.md file, explaining how to use or contribute to the project.", @@ -49,4 +49,20 @@ Lint.checkFilePaths( violateIfNoMatchesFound: true ) +try Lint.checkFilePaths( + checkInfo: CheckInfo( + id: "readme_path", + hint: "The README file should be named exactly `README.md`.", + severity: .error + ), + 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: [ + AutoCorrection(before: "api/readme.md", after: "api/README.md"), + AutoCorrection(before: "ReadMe.md", after: "README.md"), + AutoCorrection(before: "README.markdown", after: "README.md"), + ] +) Lint.logSummaryAndExit() From 95901ef115a4db03ac5c6abec2ae5cfa8d2f9ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Fri, 20 Mar 2020 08:49:10 +0100 Subject: [PATCH 23/27] Add reporting of violating match string + Fix newline issue --- Sources/AnyLint/CheckInfo.swift | 2 +- .../Checkers/FileContentsChecker.swift | 1 + Sources/AnyLint/Extensions/StringExt.swift | 6 +- Sources/AnyLint/Statistics.swift | 11 +- Sources/AnyLint/Violation.swift | 12 +- Sources/Utility/Logger.swift | 2 +- Sources/Utility/Regex.swift | 9 +- .../Extensions/RegexExtTests.swift | 2 +- lint.swift | 142 ++++++++++++++---- 9 files changed, 144 insertions(+), 43 deletions(-) diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index ed93cce..7caf54d 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -13,7 +13,7 @@ public struct CheckInfo { public let severity: Severity /// Initializes a new info object for the lint check. - public init(id: String, hint: String, severity: Severity) { + public init(id: String, hint: String, severity: Severity = .error) { self.id = id self.hint = hint self.severity = severity diff --git a/Sources/AnyLint/Checkers/FileContentsChecker.swift b/Sources/AnyLint/Checkers/FileContentsChecker.swift index cf83b39..135454c 100644 --- a/Sources/AnyLint/Checkers/FileContentsChecker.swift +++ b/Sources/AnyLint/Checkers/FileContentsChecker.swift @@ -34,6 +34,7 @@ extension FileContentsChecker: Checker { Violation( checkInfo: checkInfo, filePath: filePath, + matchedString: match.string, locationInfo: locationInfo, appliedAutoCorrection: appliedAutoCorrection ) diff --git a/Sources/AnyLint/Extensions/StringExt.swift b/Sources/AnyLint/Extensions/StringExt.swift index c73455f..33eccae 100644 --- a/Sources/AnyLint/Extensions/StringExt.swift +++ b/Sources/AnyLint/Extensions/StringExt.swift @@ -10,14 +10,14 @@ extension String { func locationInfo(of index: String.Index) -> LocationInfo { let prefix = self[startIndex ..< index] - let prefixLines = prefix.split(separator: "\n") + let prefixLines = prefix.components(separatedBy: .newlines) guard let lastPrefixLine = prefixLines.last else { return (line: 1, charInLine: 1) } let charInLine = prefix.last == "\n" ? 1 : lastPrefixLine.count + 1 - return (line: prefixLines.count + 1, charInLine: charInLine) + return (line: prefixLines.count, charInLine: charInLine) } func showNewlines() -> String { - replacingOccurrences(of: "\n", with: #"\n"#).replacingOccurrences(of: "\t", with: #"\t"#) + components(separatedBy: .newlines).joined(separator: #"\n"#) } } diff --git a/Sources/AnyLint/Statistics.swift b/Sources/AnyLint/Statistics.swift index 3546659..3ef9dfa 100644 --- a/Sources/AnyLint/Statistics.swift +++ b/Sources/AnyLint/Statistics.swift @@ -47,11 +47,20 @@ final class Statistics { let prefix = "> \(violationNumString). " log.message(prefix + violation.locationMessage()!, level: check.severity.logLevel) + let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined() if let appliedAutoCorrection = violation.appliedAutoCorrection { - let prefixLengthWhitespaces = (0 ..< prefix.count).map { _ in " " }.joined() 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 { diff --git a/Sources/AnyLint/Violation.swift b/Sources/AnyLint/Violation.swift index d203699..b1f9c64 100644 --- a/Sources/AnyLint/Violation.swift +++ b/Sources/AnyLint/Violation.swift @@ -10,15 +10,25 @@ public struct Violation { /// 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 locationInfo: String.LocationInfo? /// The autocorrection applied to fix this violation. public let appliedAutoCorrection: AutoCorrection? - init(checkInfo: CheckInfo, filePath: String? = nil, locationInfo: String.LocationInfo? = nil, appliedAutoCorrection: AutoCorrection? = nil) { + 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 } diff --git a/Sources/Utility/Logger.swift b/Sources/Utility/Logger.swift index 27662d6..87cb3fc 100644 --- a/Sources/Utility/Logger.swift +++ b/Sources/Utility/Logger.swift @@ -106,7 +106,7 @@ public final class Logger { print(formattedCurrentTime(), "⚠️ ", message.yellow) case .error: - print(formattedCurrentTime(), "❌", message.lightRed) + print(formattedCurrentTime(), "❌", message.red) } } diff --git a/Sources/Utility/Regex.swift b/Sources/Utility/Regex.swift index 9eeea31..b6cb6f2 100644 --- a/Sources/Utility/Regex.swift +++ b/Sources/Utility/Regex.swift @@ -102,7 +102,7 @@ public struct Regex { extension Regex: CustomStringConvertible { /// Returns a string describing the regex using its pattern string. public var description: String { - "Regex<\"\(regularExpression.pattern)\">" + "/\(regularExpression.pattern)/" } } @@ -209,11 +209,8 @@ extension Regex { } return captureRanges.map { [unowned self] captureRange in - if let captureRange = captureRange { - return String(describing: self.baseString[captureRange]) - } - - return nil + guard let captureRange = captureRange else { return nil } + return String(describing: self.baseString[captureRange]) } }() diff --git a/Tests/UtilityTests/Extensions/RegexExtTests.swift b/Tests/UtilityTests/Extensions/RegexExtTests.swift index 9c90a31..1345e5d 100644 --- a/Tests/UtilityTests/Extensions/RegexExtTests.swift +++ b/Tests/UtilityTests/Extensions/RegexExtTests.swift @@ -4,6 +4,6 @@ import XCTest final class RegexExtTests: XCTestCase { func testStringLiteralInit() { let regex: Regex = #".*"# - XCTAssertEqual(regex.description, #"Regex<".*">"#) + XCTAssertEqual(regex.description, #"/.*/"#) } } diff --git a/lint.swift b/lint.swift index 99ed52b..10f08a1 100755 --- a/lint.swift +++ b/lint.swift @@ -5,13 +5,31 @@ import AnyLint // . let swiftSourceFiles: Regex = #"Sources/.*\.swift"# let swiftTestFiles: Regex = #"Tests/.*\.swift"# +// MARK: - File Path Checks +try Lint.checkFilePaths( + checkInfo: CheckInfo(id: "readme", hint: "Each project should have a README.md file, explaining how to use or contribute to the project."), + regex: #"^README\.md$"#, + matchingExamples: ["README.md"], + nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], + violateIfNoMatchesFound: true +) + +try Lint.checkFilePaths( + checkInfo: CheckInfo(id: "readme_path", 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: [ + AutoCorrection(before: "api/readme.md", after: "api/README.md"), + AutoCorrection(before: "ReadMe.md", after: "README.md"), + AutoCorrection(before: "README.markdown", after: "README.md"), + ] +) + // MARK: - File Content Checks try Lint.checkFileContents( - checkInfo: CheckInfo( - id: "empty_method_body", - hint: "Don't use whitespace or newlines for the body of empty methods – use empty bodies like in `func doSomething() {}` instead.", - severity: .error - ), + checkInfo: CheckInfo(id: "empty_method_body", hint: "Don't use whitespace or newlines for the body of empty methods."), regex: ["declaration": #"(init|func [^\(\s]+)\([^{]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], matchingExamples: [ "init() { }", @@ -36,33 +54,99 @@ try Lint.checkFileContents( ] ) -// MARK: - File Path Checks -try Lint.checkFilePaths( - checkInfo: CheckInfo( - id: "readme", - hint: "Each project should have a README.md file, explaining how to use or contribute to the project.", - severity: .error - ), - regex: #"^README\.md$"#, - matchingExamples: ["README.md"], - nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], - violateIfNoMatchesFound: true +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "empty_todo", hint: "`// TODO:` comments should not be empty."), + regex: #"// TODO: ?(\[[\d\-_a-z]+\])? *\n"#, + matchingExamples: ["// TODO:\n", "// TODO: [2020-03-19]\n", "// TODO: [cg_2020-03-19] \n"], + nonMatchingExamples: ["// TODO: refactor", "// TODO: not yet implemented", "// TODO: [cg_2020-03-19] not yet implemented"], + includeFilters: [swiftSourceFiles, swiftTestFiles] ) -try Lint.checkFilePaths( - checkInfo: CheckInfo( - id: "readme_path", - hint: "The README file should be named exactly `README.md`.", - severity: .error - ), - 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", +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "empty_type", hint: "Don't keep empty types in code without commenting inside why they are needed."), + regex: #"(class|protocol|struct|enum) [^\{]+\{\s*\}"#, + matchingExamples: ["class Foo {}", "enum Constants {\n \n}", "struct MyViewModel(x: Int, y: Int, closure: () -> Void) {}"], + nonMatchingExamples: ["class Foo { /* TODO: not yet implemented */ }", "func foo() {}", "init() {}", "enum Bar { case x, y }"], + includeFilters: [swiftSourceFiles, swiftTestFiles] +) + +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "if_as_guard", hint: "Don't use an if statement to just return – use guard for such cases instead."), + regex: #" +if [^\{]+\{\s*return\s*[^\}]*\}(?! *else)"#, + matchingExamples: [" if x == 5 { return }", " if x == 5 {\n return nil\n}", " if x == 5 { return 500 }", " if x == 5 { return do(x: 500, y: 200) }"], + nonMatchingExamples: [" if x == 5 {\n let y = 200\n return y\n}", " if x == 5 { someMethod(x: 500, y: 200) }", " if x == 500 { return } else {"], + includeFilters: [swiftSourceFiles, swiftTestFiles] +) + + +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "late_force_unwrapping_3", hint: "Don't use ? first to force unwrap later – directly unwrap within the parantheses."), + regex: [ + "openingBrace": #"\("#, + "callPart1": #"[^\s\?\.]+"#, + "separator1": #"\?\."#, + "callPart2": #"[^\s\?\.]+"#, + "separator2": #"\?\."#, + "callPart3": #"[^\s\?\.]+"#, + "separator3": #"\?\."#, + "callPart4": #"[^\s\?\.]+"#, + "closingBraceUnwrap": #"\)!"#, + ], + matchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n"], + nonMatchingExamples: ["call(x: (viewModel?.username)!)", "let x = viewModel!.user!.profile!.imagePath\n"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3!.$callPart4", autoCorrectExamples: [ - AutoCorrection(before: "api/readme.md", after: "api/README.md"), - AutoCorrection(before: "ReadMe.md", after: "README.md"), - AutoCorrection(before: "README.markdown", after: "README.md"), + AutoCorrection(before: "let x = (viewModel?.user?.profile?.imagePath)!\n", after: "let x = viewModel!.user!.profile!.imagePath\n"), ] ) + +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "late_force_unwrapping_2", hint: "Don't use ? first to force unwrap later – directly unwrap within the parantheses."), + regex: [ + "openingBrace": #"\("#, + "callPart1": #"[^\s\?\.]+"#, + "separator1": #"\?\."#, + "callPart2": #"[^\s\?\.]+"#, + "separator2": #"\?\."#, + "callPart3": #"[^\s\?\.]+"#, + "closingBraceUnwrap": #"\)!"#, + ], + matchingExamples: ["call(x: (viewModel?.profile?.username)!)"], + nonMatchingExamples: ["let x = (viewModel?.user?.profile?.imagePath)!\n", "let x = viewModel!.profile!.imagePath\n"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$callPart1!.$callPart2!.$callPart3", + autoCorrectExamples: [ + AutoCorrection(before: "let x = (viewModel?.profile?.imagePath)!\n", after: "let x = viewModel!.profile!.imagePath\n"), + ] +) + +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "late_force_unwrapping_1", hint: "Don't use ? first to force unwrap later – directly unwrap within the parantheses."), + regex: [ + "openingBrace": #"\("#, + "callPart1": #"[^\s\?\.]+"#, + "separator1": #"\?\."#, + "callPart2": #"[^\s\?\.]+"#, + "closingBraceUnwrap": #"\)!"#, + ], + matchingExamples: ["call(x: (viewModel?.username)!)"], + nonMatchingExamples: ["call(x: (viewModel?.profile?.username)!)", "call(x: viewModel!.username)"], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: "$callPart1!.$callPart2", + autoCorrectExamples: [ + AutoCorrection(before: "call(x: (viewModel?.username)!)", after: "call(x: viewModel!.username)"), + ] +) + +try Lint.checkFileContents( + checkInfo: CheckInfo(id: "logger", hint: "Don't use `print` – use `log.message` instead."), + regex: #"print\([^\n]+\)"#, + matchingExamples: [#"print("Hellow World!")"#, #"print(5)"#, #"print(\n "hi"\n)"#], + nonMatchingExamples: [#"log.message("Hello world!")"#], + includeFilters: [swiftSourceFiles, swiftTestFiles], + excludeFilters: [#"Sources/.*/Logger\.swift"#] +) + +// MARK: - Log Summary Lint.logSummaryAndExit() From e072d94cb32a408cbb5c069c0fd838fc2bb42e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 22 Mar 2020 16:08:37 +0100 Subject: [PATCH 24/27] Add more rules in project + Improve lint blank template --- Sources/AnyLint/CheckInfo.swift | 46 +++ Sources/AnyLint/Lint.swift | 26 +- Sources/AnyLint/Severity.swift | 16 + .../AndroidTemplate.swift | 18 - .../BlankTemplate.swift | 76 +++- .../ConfigurationTemplate.swift | 11 + .../ConfigurationTemplates/IOSTemplate.swift | 16 - Sources/AnyLintCLI/Tasks/EditTask.swift | 17 - Sources/AnyLintCLI/Tasks/InitTask.swift | 8 - Sources/Utility/Regex.swift | 4 +- Tests/AnyLintTests/CheckInfoTests.swift | 34 ++ Tests/AnyLintTests/LintTests.swift | 4 + Tests/AnyLintTests/StatisticsTests.swift | 39 +- lint.swift | 387 ++++++++++++++++-- 14 files changed, 574 insertions(+), 128 deletions(-) delete mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift delete mode 100644 Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift delete mode 100644 Sources/AnyLintCLI/Tasks/EditTask.swift create mode 100644 Tests/AnyLintTests/CheckInfoTests.swift diff --git a/Sources/AnyLint/CheckInfo.swift b/Sources/AnyLint/CheckInfo.swift index 7caf54d..70e91f9 100644 --- a/Sources/AnyLint/CheckInfo.swift +++ b/Sources/AnyLint/CheckInfo.swift @@ -25,3 +25,49 @@ extension CheckInfo: Hashable { hasher.combine(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/Lint.swift b/Sources/AnyLint/Lint.swift index eef8d61..c6f2d27 100644 --- a/Sources/AnyLint/Lint.swift +++ b/Sources/AnyLint/Lint.swift @@ -34,7 +34,12 @@ public enum Lint { ) if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll(examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement) + validateAutocorrectsAll( + checkInfo: checkInfo, + examples: autoCorrectExamples, + regex: regex, + autocorrectReplacement: autoCorrectReplacement + ) } let filePathsToCheck: [String] = FilesSearch.allFiles( @@ -85,7 +90,12 @@ public enum Lint { ) if let autoCorrectReplacement = autoCorrectReplacement { - validateAutocorrectsAll(examples: autoCorrectExamples, regex: regex, autocorrectReplacement: autoCorrectReplacement) + validateAutocorrectsAll( + checkInfo: checkInfo, + examples: autoCorrectExamples, + regex: regex, + autocorrectReplacement: autoCorrectReplacement + ) } let filePathsToCheck: [String] = FilesSearch.allFiles( @@ -132,7 +142,7 @@ public enum Lint { if !regex.matches(example) { // TODO: [cg_2020-03-14] check position of ↘ is the matching line and char. 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 '\(checkInfo.id)' within matching example:\n\(example)", level: .error ) log.exit(status: .failure) @@ -145,7 +155,7 @@ public enum Lint { if regex.matches(example) { // TODO: [cg_2020-03-14] check position of ↘ is the matching line and char. 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 '\(checkInfo.id)' within non-matching example:\n\(example)", level: .error ) log.exit(status: .failure) @@ -153,14 +163,16 @@ public enum Lint { } } - static func validateAutocorrectsAll(examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { + static func validateAutocorrectsAll(checkInfo: CheckInfo, examples: [AutoCorrection], regex: Regex, autocorrectReplacement: String) { for autocorrect in examples { let autocorrected = regex.replaceAllCaptures(in: autocorrect.before, with: autocorrectReplacement) if autocorrected != autocorrect.after { log.message( """ - Autocorrecting example '\(autocorrect.before)' did not result in expected output. - Expected '\(autocorrect.after)' but got '\(autocorrected)'. + Autocorrecting example for \(checkInfo.id) did not result in expected output. + Before: '\(autocorrect.before.showNewlines())' + After: '\(autocorrected.showNewlines())' + Expected: '\(autocorrect.after.showNewlines())' """, level: .error ) diff --git a/Sources/AnyLint/Severity.swift b/Sources/AnyLint/Severity.swift index c3dfffb..2ece7ea 100644 --- a/Sources/AnyLint/Severity.swift +++ b/Sources/AnyLint/Severity.swift @@ -24,4 +24,20 @@ public enum Severity: Int, CaseIterable { 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 + } + } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift deleted file mode 100644 index 85717b3..0000000 --- a/Sources/AnyLintCLI/ConfigurationTemplates/AndroidTemplate.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import Utility - -// swiftlint:disable trailing_whitespace - -enum AndroidTemplate: ConfigurationTemplate { - static func fileContents() -> String { - """ - #!\(CLIConstants.swiftShPath) - import AnyLint // @Flinesoft ~> \(Constants.currentVersion) - - // TODO: [cg_2020-03-11] not yet implemented - - Lint.logSummaryAndExit() - - """ - } -} diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift index 421694f..fa429da 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/BlankTemplate.swift @@ -1,18 +1,78 @@ import Foundation import Utility -// swiftlint:disable trailing_whitespace +// swiftlint:disable function_body_length enum BlankTemplate: ConfigurationTemplate { static func fileContents() -> String { - """ - #!\(CLIConstants.swiftShPath) - import AnyLint // @Flinesoft ~> \(Constants.currentVersion) + commonPrefix + #""" + // MARK: - Variables + let readmeFile: Regex = #"README\.md"# - // TODO: [cg_2020-03-11] not yet implemented + // 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: #"^README\.md$"#, + matchingExamples: ["README.md"], + nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], + violateIfNoMatchesFound: true + ) - Lint.logSummaryAndExit() - - """ + // MARK: readme_path + try Lint.checkFilePaths( + checkInfo: "readme_path: 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: [ + AutoCorrection(before: "api/readme.md", after: "api/README.md"), + AutoCorrection(before: "ReadMe.md", after: "README.md"), + AutoCorrection(before: "README.markdown", after: "README.md"), + ] + ) + + // MARK: readme_top_level_title + try Lint.checkFileContents( + checkInfo: "readme_top_level_title: 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: readme_typo_license + try Lint.checkFileContents( + checkInfo: "readme_typo_license: Misspelled word 'license'.", + regex: #"([\s#]L|l)isence([\s\.,:;])"#, + matchingExamples: [" lisence:", "## Lisence\n"], + nonMatchingExamples: [" license:", "## License\n"], + includeFilters: [readmeFile], + autoCorrectReplacement: "$1icense$2", + autoCorrectExamples: [ + AutoCorrection(before: " lisence:", after: " license:"), + AutoCorrection(before: "## Lisence\n", after: "## License\n"), + ] + ) + """# + commonSuffix } } diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift index 294600d..9860e90 100644 --- a/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift +++ b/Sources/AnyLintCLI/ConfigurationTemplates/ConfigurationTemplate.swift @@ -1,5 +1,16 @@ import Foundation +import Utility protocol ConfigurationTemplate { static func fileContents() -> String } + +extension ConfigurationTemplate { + static var commonPrefix: String { + "#!\(CLIConstants.swiftShPath)\nimport AnyLint // @Flinesoft ~> \(Constants.currentVersion)\n\n" + } + + static var commonSuffix: String { + "\n\n// MARK: - Log Summary & Exit\nLint.logSummaryAndExit()\n" + } +} diff --git a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift b/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift deleted file mode 100644 index 2fe8501..0000000 --- a/Sources/AnyLintCLI/ConfigurationTemplates/IOSTemplate.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import Utility - -enum IOSTemplate: ConfigurationTemplate { - static func fileContents() -> String { - """ - #!\(CLIConstants.swiftShPath) - import AnyLint // @Flinesoft ~> \(Constants.currentVersion) - - // TODO: [cg_2020-03-11] not yet implemented - - Lint.logSummaryAndExit() - - """ - } -} diff --git a/Sources/AnyLintCLI/Tasks/EditTask.swift b/Sources/AnyLintCLI/Tasks/EditTask.swift deleted file mode 100644 index f1f3017..0000000 --- a/Sources/AnyLintCLI/Tasks/EditTask.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import SwiftCLI -import Utility - -struct EditTask { - let configFilePath: String -} - -extension EditTask: TaskHandler { - func perform() throws { - try ValidateOrFail.configFileExists(at: configFilePath) - ValidateOrFail.swiftShInstalled() - - log.message("Opening config file at \(configFilePath) in Xcode to edit ...", level: .info) - try Task.run(bash: "\(CLIConstants.swiftShPath) edit '\(configFilePath)'") - } -} diff --git a/Sources/AnyLintCLI/Tasks/InitTask.swift b/Sources/AnyLintCLI/Tasks/InitTask.swift index 385bb87..dbcf06f 100644 --- a/Sources/AnyLintCLI/Tasks/InitTask.swift +++ b/Sources/AnyLintCLI/Tasks/InitTask.swift @@ -5,19 +5,11 @@ import Utility struct InitTask { enum Template: String, CaseIterable { case blank - case ios - case android var configFileContents: String { switch self { case .blank: return BlankTemplate.fileContents() - - case .android: - return AndroidTemplate.fileContents() - - case .ios: - return IOSTemplate.fileContents() } } } diff --git a/Sources/Utility/Regex.swift b/Sources/Utility/Regex.swift index b6cb6f2..6a55bc2 100644 --- a/Sources/Utility/Regex.swift +++ b/Sources/Utility/Regex.swift @@ -214,9 +214,9 @@ extension Regex { } }() - private let result: NSTextCheckingResult + let result: NSTextCheckingResult - private let baseString: String + let baseString: String // MARK: - Initializers internal init(result: NSTextCheckingResult, in string: String) { diff --git a/Tests/AnyLintTests/CheckInfoTests.swift b/Tests/AnyLintTests/CheckInfoTests.swift new file mode 100644 index 0000000..ad69b2e --- /dev/null +++ b/Tests/AnyLintTests/CheckInfoTests.swift @@ -0,0 +1,34 @@ +@testable import AnyLint +@testable import Utility +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/LintTests.swift b/Tests/AnyLintTests/LintTests.swift index fa9db23..99402c6 100644 --- a/Tests/AnyLintTests/LintTests.swift +++ b/Tests/AnyLintTests/LintTests.swift @@ -56,6 +56,7 @@ final class LintTests: XCTestCase { 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"), @@ -67,6 +68,7 @@ final class LintTests: XCTestCase { 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"), @@ -90,6 +92,7 @@ final class LintTests: XCTestCase { ] 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"), @@ -101,6 +104,7 @@ final class LintTests: XCTestCase { 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"), diff --git a/Tests/AnyLintTests/StatisticsTests.swift b/Tests/AnyLintTests/StatisticsTests.swift index 6504c1d..b85df9d 100644 --- a/Tests/AnyLintTests/StatisticsTests.swift +++ b/Tests/AnyLintTests/StatisticsTests.swift @@ -54,7 +54,7 @@ final class StatisticsTests: XCTestCase { XCTAssertEqual(Statistics.shared.violationsPerCheck.keys.count, 3) } - func testLogSummary() { + func testLogSummary() { // swiftlint:disable:this function_body_length Statistics.shared.logSummary() XCTAssertEqual(TestHelper.shared.consoleOutputs.count, 1) XCTAssertEqual(TestHelper.shared.consoleOutputs[0].level, .warning) @@ -94,22 +94,25 @@ final class StatisticsTests: XCTestCase { [.info, .info, .warning, .warning, .warning, .warning, .error, .error, .error, .error, .error, .error] ) - XCTAssertEqual( - TestHelper.shared.consoleOutputs.map { $0.message }, - [ - "[id1] Found 1 violation(s).", - ">> Hint: hint1", - "[id2] Found 2 violation(s) at:", - "> 1. Hogwarts/Harry.swift", - "> 2. Hogwarts/Albus.swift", - ">> Hint: hint2", - "[id3] 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", - "Performed 3 check(s) and found 3 error(s) & 2 warning(s).", - ] - ) + 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) 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/lint.swift b/lint.swift index 10f08a1..027d911 100755 --- a/lint.swift +++ b/lint.swift @@ -1,35 +1,15 @@ #!/usr/local/bin/swift-sh import AnyLint // . -// MARK: - Reusables +// MARK: - Variables let swiftSourceFiles: Regex = #"Sources/.*\.swift"# let swiftTestFiles: Regex = #"Tests/.*\.swift"# +let readmeFile: Regex = #"README\.md"# -// MARK: - File Path Checks -try Lint.checkFilePaths( - checkInfo: CheckInfo(id: "readme", hint: "Each project should have a README.md file, explaining how to use or contribute to the project."), - regex: #"^README\.md$"#, - matchingExamples: ["README.md"], - nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], - violateIfNoMatchesFound: true -) - -try Lint.checkFilePaths( - checkInfo: CheckInfo(id: "readme_path", 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: [ - AutoCorrection(before: "api/readme.md", after: "api/README.md"), - AutoCorrection(before: "ReadMe.md", after: "README.md"), - AutoCorrection(before: "README.markdown", after: "README.md"), - ] -) - -// MARK: - File Content Checks +// MARK: - +// MARK: empty_method_body try Lint.checkFileContents( - checkInfo: CheckInfo(id: "empty_method_body", hint: "Don't use whitespace or newlines for the body of empty methods."), + checkInfo: "empty_method_body: Don't use whitespace or newlines for the body of empty methods.", regex: ["declaration": #"(init|func [^\(\s]+)\([^{]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], matchingExamples: [ "init() { }", @@ -54,33 +34,305 @@ try Lint.checkFileContents( ] ) +// MARK: empty_todo try Lint.checkFileContents( - checkInfo: CheckInfo(id: "empty_todo", hint: "`// TODO:` comments should not be empty."), + checkInfo: "empty_todo: `// TODO:` comments should not be empty.", regex: #"// TODO: ?(\[[\d\-_a-z]+\])? *\n"#, matchingExamples: ["// TODO:\n", "// TODO: [2020-03-19]\n", "// TODO: [cg_2020-03-19] \n"], nonMatchingExamples: ["// TODO: refactor", "// TODO: not yet implemented", "// TODO: [cg_2020-03-19] not yet implemented"], includeFilters: [swiftSourceFiles, swiftTestFiles] ) +// MARK: empty_type try Lint.checkFileContents( - checkInfo: CheckInfo(id: "empty_type", hint: "Don't keep empty types in code without commenting inside why they are needed."), + checkInfo: "empty_type: Don't keep empty types in code without commenting inside why they are needed.", regex: #"(class|protocol|struct|enum) [^\{]+\{\s*\}"#, matchingExamples: ["class Foo {}", "enum Constants {\n \n}", "struct MyViewModel(x: Int, y: Int, closure: () -> Void) {}"], nonMatchingExamples: ["class Foo { /* TODO: not yet implemented */ }", "func foo() {}", "init() {}", "enum Bar { case x, y }"], includeFilters: [swiftSourceFiles, swiftTestFiles] ) +// MARK: guard_multiline_2 +try Lint.checkFileContents( + checkInfo: "guard_multiline_2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: [ + "newline": #"\n"#, + "guardIndent": #" *"#, + "guard": #"guard *"#, + "line1": #"[^\n]+,"#, + "line1Indent": #"\n *"#, + "line2": #"[^\n]*\S"#, + "else": #"\s*else\s*\{\s*"# + ], + matchingExamples: [ + """ + + guard let x1 = y1?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """ + ], + nonMatchingExamples: [ + """ + + guard + let x1 = y1?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """, + """ + + guard let url = URL(string: self, relativeTo: fileManager.currentDirectoryUrl) else { + return 2 + } + """, + ], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: """ + + $guardIndentguard + $guardIndent $line1 + $guardIndent $line2 + $guardIndentelse { + $guardIndent\u{0020}\u{0020}\u{0020}\u{0020} + """, + autoCorrectExamples: [ + AutoCorrection( + before: """ + let x = 15 + guard let x1 = y1?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """, + after: """ + let x = 15 + guard + let x1 = y1?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """ + ), + ] +) + +// MARK: guard_multiline_3 try Lint.checkFileContents( - checkInfo: CheckInfo(id: "if_as_guard", hint: "Don't use an if statement to just return – use guard for such cases instead."), + checkInfo: "guard_multiline_3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: [ + "newline": #"\n"#, + "guardIndent": #" *"#, + "guard": #"guard *"#, + "line1": #"[^\n]+,"#, + "line1Indent": #"\n *"#, + "line2": #"[^\n]+,"#, + "line2Indent": #"\n *"#, + "line3": #"[^\n]*\S"#, + "else": #"\s*else\s*\{\s*"# + ], + matchingExamples: [ + """ + + guard let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """ + ], + nonMatchingExamples: [ + """ + + guard + let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """, + """ + + guard let url = URL(x: 1, y: 2, relativeTo: fileManager.currentDirectoryUrl) else { + return 2 + } + """, + ], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: """ + + $guardIndentguard + $guardIndent $line1 + $guardIndent $line2 + $guardIndent $line3 + $guardIndentelse { + $guardIndent\u{0020}\u{0020}\u{0020}\u{0020} + """, + autoCorrectExamples: [ + AutoCorrection( + before: """ + let x = 15 + guard let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """, + after: """ + let x = 15 + guard + let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """ + ), + ] +) + +// MARK: guard_multiline_4 +try Lint.checkFileContents( + checkInfo: "guard_multiline_4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: [ + "newline": #"\n"#, + "guardIndent": #" *"#, + "guard": #"guard *"#, + "line1": #"[^\n]+,"#, + "line1Indent": #"\n *"#, + "line2": #"[^\n]+,"#, + "line2Indent": #"\n *"#, + "line3": #"[^\n]+,"#, + "line3Indent": #"\n *"#, + "line4": #"[^\n]*\S"#, + "else": #"\s*else\s*\{\s*"# + ], + matchingExamples: [ + """ + + guard let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let x3 = y3?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """ + ], + nonMatchingExamples: [ + """ + + guard + let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let x3 = y3?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """, + """ + + guard let url = URL(x: 1, y: 2, z: 3, relativeTo: fileManager.currentDirectoryUrl) else { + return 2 + } + """, + ], + includeFilters: [swiftSourceFiles, swiftTestFiles], + autoCorrectReplacement: """ + + $guardIndentguard + $guardIndent $line1 + $guardIndent $line2 + $guardIndent $line3 + $guardIndent $line4 + $guardIndentelse { + $guardIndent\u{0020}\u{0020}\u{0020}\u{0020} + """, + autoCorrectExamples: [ + AutoCorrection( + before: """ + let x = 15 + guard let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let x3 = y3?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """, + after: """ + let x = 15 + guard + let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let x3 = y3?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """ + ), + ] +) + +// MARK: guard_multiline_n +try Lint.checkFileContents( + checkInfo: "guard_multiline_n: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + regex: #"\n *guard *([^\n]+,\n){4,}[^\n]*\S\s*else\s*\{\s*"#, + matchingExamples: [ + """ + + guard let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let x3 = y3?.imagePath, + let x4 = y4?.imagePath, + let x5 = y5?.imagePath, + let z = EnumType(rawValue: 15) else { + return 2 + } + """ + ], + nonMatchingExamples: [ + """ + + guard + let x1 = y1?.imagePath, + let x2 = y2?.imagePath, + let x3 = y3?.imagePath, + let x4 = y4?.imagePath, + let x5 = y5?.imagePath, + let z = EnumType(rawValue: 15) + else { + return 2 + } + """, + """ + + guard let url = URL(x1: 1, x2: 2, x3: 3, x4: 4, x5: 5, relativeTo: fileManager.currentDirectoryUrl) else { + return 2 + } + """, + ], + includeFilters: [swiftSourceFiles, swiftTestFiles] +) + +// MARK: if_as_guard +try Lint.checkFileContents( + checkInfo: "if_as_guard: Don't use an if statement to just return – use guard for such cases instead.", regex: #" +if [^\{]+\{\s*return\s*[^\}]*\}(?! *else)"#, matchingExamples: [" if x == 5 { return }", " if x == 5 {\n return nil\n}", " if x == 5 { return 500 }", " if x == 5 { return do(x: 500, y: 200) }"], nonMatchingExamples: [" if x == 5 {\n let y = 200\n return y\n}", " if x == 5 { someMethod(x: 500, y: 200) }", " if x == 500 { return } else {"], includeFilters: [swiftSourceFiles, swiftTestFiles] ) - +// MARK: late_force_unwrapping_3 try Lint.checkFileContents( - checkInfo: CheckInfo(id: "late_force_unwrapping_3", hint: "Don't use ? first to force unwrap later – directly unwrap within the parantheses."), + checkInfo: "late_force_unwrapping_3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -101,8 +353,9 @@ try Lint.checkFileContents( ] ) +// MARK: late_force_unwrapping_2 try Lint.checkFileContents( - checkInfo: CheckInfo(id: "late_force_unwrapping_2", hint: "Don't use ? first to force unwrap later – directly unwrap within the parantheses."), + checkInfo: "late_force_unwrapping_2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -121,8 +374,9 @@ try Lint.checkFileContents( ] ) +// MARK: late_force_unwrapping_1 try Lint.checkFileContents( - checkInfo: CheckInfo(id: "late_force_unwrapping_1", hint: "Don't use ? first to force unwrap later – directly unwrap within the parantheses."), + checkInfo: "late_force_unwrapping_1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -139,8 +393,9 @@ try Lint.checkFileContents( ] ) +// MARK: logger try Lint.checkFileContents( - checkInfo: CheckInfo(id: "logger", hint: "Don't use `print` – use `log.message` instead."), + checkInfo: "logger: Don't use `print` – use `log.message` instead.", regex: #"print\([^\n]+\)"#, matchingExamples: [#"print("Hellow World!")"#, #"print(5)"#, #"print(\n "hi"\n)"#], nonMatchingExamples: [#"log.message("Hello world!")"#], @@ -148,5 +403,69 @@ try Lint.checkFileContents( excludeFilters: [#"Sources/.*/Logger\.swift"#] ) -// MARK: - Log Summary +// MARK: readme +try Lint.checkFilePaths( + checkInfo: "readme: Each project should have a README.md file, explaining how to use or contribute to the project.", + regex: #"^README\.md$"#, + matchingExamples: ["README.md"], + nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], + violateIfNoMatchesFound: true +) + +// MARK: readme_path +try Lint.checkFilePaths( + checkInfo: "readme_path: 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: [ + AutoCorrection(before: "api/readme.md", after: "api/README.md"), + AutoCorrection(before: "ReadMe.md", after: "README.md"), + AutoCorrection(before: "README.markdown", after: "README.md"), + ] +) + +// MARK: readme_top_level_title +try Lint.checkFileContents( + checkInfo: "readme_top_level_title: 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: readme_typo_license +try Lint.checkFileContents( + checkInfo: "readme_typo_license: Misspelled word 'license'.", + regex: #"([\s#]L|l)isence([\s\.,:;])"#, + matchingExamples: [" lisence:", "## Lisence\n"], + nonMatchingExamples: [" license:", "## License\n"], + includeFilters: [readmeFile], + autoCorrectReplacement: "$1icense$2", + autoCorrectExamples: [ + AutoCorrection(before: " lisence:", after: " license:"), + AutoCorrection(before: "## Lisence\n", after: "## License\n"), + ] +) + +// MARK: - Log Summary & Exit Lint.logSummaryAndExit() From a79676f3486bd53ceb047f07235f27d6fa172704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 22 Mar 2020 22:10:59 +0100 Subject: [PATCH 25/27] [README.md] Initial Getting Started & Configuration sections --- README.md | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++++- lint.swift | 66 +++++++-------- 2 files changed, 262 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index f51f899..0c800e8 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@

Installation + • Getting StartedConfiguration - • UsageDonationIssuesContributing @@ -52,13 +52,238 @@ Lint anything by combining the power of Swift & regular expressions. TODO +## Getting Started + +To initialize AnyLint in a project, run: + +```bash +anylint --init blank +``` + +This will create the Swift script file `lint.swift` with the following contents: + +```swift +#!/usr/local/bin/swift-sh +import AnyLint // @Flinesoft ~> 0.1.0 + +// MARK: - Variables +// some example variables + +// MARK: - Checks +// some example lint checks + +// MARK: - Log Summary & Exit +Lint.logSummaryAndExit() +``` + +The most important thing to note is that the **first two lines and the last line are required** for AnyLint to work properly. + +Other than that, all the other code in between 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 last line makes sure that all violations found in the process of running the previous code are reported properly and exits the script with the proper exit code. + +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 +``` + +Runs the lint checks for both configuration files: +```bash +anylint --path Sources/lint.swift --path Tests/lint.swift +``` + ## Configuration -TODO +AnyLint provides three different kinds of lint checks: -## Usage +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. -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). + +### Basic Types + +Independent from the method used, there are a few types specified in the AnyLint package you should know of. + +#### Regex + +Many parameters in the above mentioned lint check methods are of `Regex` type. A `Regex` can be initialized in several ways: + +1. Using a **String**: + ```swift + let regex = Regex(#"(foo|bar)[0-9]+"#) // => /(foo|bar)[0-9]+/` + ``` +2. Using a **String Literal**: + ```swift + let regex: Regex = #"(foo|bar)[0-9]+"# // => /(foo|bar)[0-9]+/ + ``` +3. Using a **Dictionary Literal**: (use for [named capture groups](https://www.regular-expressions.info/named.html)) + ```swift + let regex: Regex = ["key": #"foo|bar"#, "num": "[0-9]+"] + // => /(?foo|bar)(?[0-9]+)/ + ``` + +Note that we recommend using [raw strings](https://www.hackingwithswift.com/articles/162/how-to-use-raw-strings-in-swift) (`#"foo"#` instead of `"foo"`) for all regexes to get rid of double escaping backslashes (e.g. `\\s` becomes `\s`). This also allows for testing regexes in online regex editors like [rubular](https://rubular.com/) first and then copy & pasting from them without any additional escaping. + +In general, initializing a `Regex` object is enough to use AnyLint – the matching part will be handled automatically. In case you want to use the `customCheck` method and need regexes there, you can learn more about how you can match strings with a `Regex` object on [the HandySwift docs](https://github.com/Flinesoft/AnyLint/blob/main/Sources/Utility/Regex.swift) (the project, the class was taken from) or read the [code documentation comments](https://github.com/Flinesoft/AnyLint/blob/main/Sources/Utility/Regex.swift). + +#### CheckInfo + +A `CheckInfo` 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. +3. `severity`: The severity of violations. One of `error`, `warning`, `info`. Default: `error` + +While there is an initializer available, we recommend using a String Literal instead like so: + +```swift +// accepted structure: (@): +let checkInfo: CheckInfo = "readme_path: The README file should be named exactly `README.md`." +``` + +### Check File Contents + +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`: + +```swift +// MARK: empty_todo +try Lint.checkFileContents( + checkInfo: "EmptyTodo: TODO comments should not be empty.", + regex: #"// TODO: *\n"# +) +``` + +But we *strongly recommend* to always provide also: + +1. `matchingExamples`: Array of strings expected to match the given string for `regex` validation. +2. `nonMatchingExamples`: Array of strings not matching the given string for `regex` validation. +3. `includeFilters`: Array of `Regex` objects to include to the file paths to check. + +The first two will be used on each run of AnyLint to check if the provided `regex` actually works as expected. If any of the `matchingExamples` doesn't match or if any of the `nonMatchingExamples` _does_ match, the entire AnyLint command will fail early. This a built-in validation step to help preventing a lot of issues and increasing your confidence on the lint checks. + +The third one is recommended because it increases the performance of the linter. Only files at paths matching at least one of the provided regexes will be checked. If not provided, all files within the current directory will be read recursively for each check, which is inefficient. + +Here's the *recommended minimum example*: + +```swift +// MARK: - Variables +let swiftSourceFiles: Regex = #"Sources/.*\.swift"# +let swiftTestFiles: Regex = #"Tests/.*\.swift"# + +// MARK: - Checks +// MARK: empty_todo +try Lint.checkFileContents( + checkInfo: "EmptyTodo: TODO comments should not be empty.", + regex: #"// TODO: *\n"#, + matchingExamples: ["// TODO:\n"], + nonMatchingExamples: ["// TODO: not yet implemented\n"], + includeFilters: [swiftSourceFiles, swiftTestFiles] +) +``` + +There's 3 more parameters you can optionally set if needed: + +1. `excludeFilters`: Array of `Regex` objects to exclude from the file paths to check. +2. `autoCorrectReplacement`: Replacement string which can reference any capture groups in the `regex`. +3. `autoCorrectExamples`: Example structs with `before` and `after` for autocorrection validation. + +The `excludeFilters` can be used alternatively to the `includeFilters` or alongside them. If used alongside, exclusion will take precedence over inclusion. + +If `autoCorrectReplacement` is provided, AnyLint will automatically replace matches of `regex` with the given replacement string. Capture groups are supported, both in numbered style (`([a-z]+)(\d+)` => `$1$2`) and named group style (`(?[a-z])(?\d+)` => `$alpha$num`). When provided, we strongly recommend to also provide `autoCorrectExamples` for validation. Like for `matchingExamples` / `nonMatchingExamples` the entire command will fail early if one of the examples doesn't correct from the `before` string to the expected `after` string. + +> *Caution:* When using the `autoCorrectReplacement` parameter, be sure to double-check that your regex doesn't match too much content. Additionally, we strongly recommend to commit your changes regularly to have some backup. + +Here's a *full example using all parameters* at once: + +```swift +// MARK: - Variables +let swiftSourceFiles: Regex = #"Sources/.*\.swift"# +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.", + regex: [ + "declaration": #"func [^\(\s]+\([^{]*\)"#, + "spacing": #"\s*"#, + "body": #"\{\s+\}"# + ], + matchingExamples: [ + "func foo2bar() { }", + "func foo2bar(x: Int, y: Int) { }", + "func foo2bar(\n x: Int,\n y: Int\n) {\n \n}", + ], + nonMatchingExamples: [ + "func foo2bar() {}", + "func foo2bar(x: Int, y: Int) {}" + ], + includeFilters: [swiftSourceFiles], + excludeFilters: [swiftTestFiles], + autoCorrectReplacement: "$declaration {}", + autoCorrectExamples: [ + AutoCorrection(before: "func foo2bar() { }", after: "func foo2bar() {}"), + AutoCorrection(before: "func foo2bar(x: Int, y: Int) { }", after: "func foo2bar(x: Int, y: Int) {}"), + AutoCorrection(before: "func foo2bar()\n{\n \n}", after: "func foo2bar() {}"), + ] +) +``` + +### Check File Paths + +The `checkFilePaths` method has all the same parameters like the `checkFileContents` method, so please read the above section to learn more about them. There's only one difference and one additional parameter: + +1. `autoCorrectReplacement`: Here, this will safely move the file using the path replacement. +2. `violateIfNoMatchesFound`: Will report a violation if _no_ matches are found if `true`. Default: `false` + +As this method is about file paths and not file contents, the `autoCorrectReplacement` actually also fixes the paths, which corresponds to moving files from the `before` state to the `after` state. Note that moving/renaming files here is done safely, which means that if a file already exists at the resulting path, the command will fail. + +By default, `checkFilePaths` will fail if the given `regex` matches a file. If you want to check for the _existence_ of a file though, you can set `violateIfNoMatchesFound` to `true` instead, then the method will fail if it does _not_ matchn any file. + +### Custom Checks + +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. 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. +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. + +When using the `customCheck`, you might want to also include some Swift packages for [easier file handling](https://github.com/JohnSundell/Files) or [running shell commands](https://github.com/JohnSundell/ShellOut). You can do so by adding them at the top of the file like so: + +> TODO: Improve the below code example with something more useful & realistic. + +```swift +#!/usr/local/bin/swift-sh +import AnyLint // @Flinesoft ~> 0.1.0 +import Files // @JohnSundell ~> 4.1.1 +import ShellOut // @JohnSundell ~> 2.3.0 + +// MARK: echo +try Lint.customCheck(checkInfo: "Echo: Always say hello to the world.") { + var violations: [Violation] = [] + + // use ShellOut package + let output = try shellOut(to: "echo", arguments: ["Hello world"]) + // ... + + // use Files package + try Folder(path: "MyFolder").files.forEach { file in + // ... + } + + return violations +} + +// MARK: - Log Summary & Exit +Lint.logSummaryAndExit() +``` ## Donation diff --git a/lint.swift b/lint.swift index 027d911..000c1ad 100755 --- a/lint.swift +++ b/lint.swift @@ -6,10 +6,10 @@ let swiftSourceFiles: Regex = #"Sources/.*\.swift"# let swiftTestFiles: Regex = #"Tests/.*\.swift"# let readmeFile: Regex = #"README\.md"# -// MARK: - -// MARK: empty_method_body +// MARK: - Checks +// MARK: EmptyMethodBody try Lint.checkFileContents( - checkInfo: "empty_method_body: Don't use whitespace or newlines for the body of empty methods.", + checkInfo: "EmptyMethodBody: Don't use whitespace or newlines for the body of empty methods.", regex: ["declaration": #"(init|func [^\(\s]+)\([^{]*\)"#, "spacing": #"\s*"#, "body": #"\{\s+\}"#], matchingExamples: [ "init() { }", @@ -34,27 +34,27 @@ try Lint.checkFileContents( ] ) -// MARK: empty_todo +// MARK: EmptyTodo try Lint.checkFileContents( - checkInfo: "empty_todo: `// TODO:` comments should not be empty.", + checkInfo: "EmptyTodo: `// TODO:` comments should not be empty.", regex: #"// TODO: ?(\[[\d\-_a-z]+\])? *\n"#, matchingExamples: ["// TODO:\n", "// TODO: [2020-03-19]\n", "// TODO: [cg_2020-03-19] \n"], nonMatchingExamples: ["// TODO: refactor", "// TODO: not yet implemented", "// TODO: [cg_2020-03-19] not yet implemented"], includeFilters: [swiftSourceFiles, swiftTestFiles] ) -// MARK: empty_type +// MARK: EmptyType try Lint.checkFileContents( - checkInfo: "empty_type: Don't keep empty types in code without commenting inside why they are needed.", + checkInfo: "EmptyType: Don't keep empty types in code without commenting inside why they are needed.", regex: #"(class|protocol|struct|enum) [^\{]+\{\s*\}"#, matchingExamples: ["class Foo {}", "enum Constants {\n \n}", "struct MyViewModel(x: Int, y: Int, closure: () -> Void) {}"], nonMatchingExamples: ["class Foo { /* TODO: not yet implemented */ }", "func foo() {}", "init() {}", "enum Bar { case x, y }"], includeFilters: [swiftSourceFiles, swiftTestFiles] ) -// MARK: guard_multiline_2 +// MARK: GuardMultiline2 try Lint.checkFileContents( - checkInfo: "guard_multiline_2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + checkInfo: "GuardMultiline2: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: [ "newline": #"\n"#, "guardIndent": #" *"#, @@ -121,9 +121,9 @@ try Lint.checkFileContents( ] ) -// MARK: guard_multiline_3 +// MARK: GuardMultiline3 try Lint.checkFileContents( - checkInfo: "guard_multiline_3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + checkInfo: "GuardMultiline3: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: [ "newline": #"\n"#, "guardIndent": #" *"#, @@ -197,9 +197,9 @@ try Lint.checkFileContents( ] ) -// MARK: guard_multiline_4 +// MARK: GuardMultiline4 try Lint.checkFileContents( - checkInfo: "guard_multiline_4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + checkInfo: "GuardMultiline4: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: [ "newline": #"\n"#, "guardIndent": #" *"#, @@ -280,9 +280,9 @@ try Lint.checkFileContents( ] ) -// MARK: guard_multiline_n +// MARK: GuardMultilineN try Lint.checkFileContents( - checkInfo: "guard_multiline_n: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", + checkInfo: "GuardMultilineN: Close a multiline guard via `else {` on a new line indented like the opening `guard`.", regex: #"\n *guard *([^\n]+,\n){4,}[^\n]*\S\s*else\s*\{\s*"#, matchingExamples: [ """ @@ -321,18 +321,18 @@ try Lint.checkFileContents( includeFilters: [swiftSourceFiles, swiftTestFiles] ) -// MARK: if_as_guard +// MARK: IfAsGuard try Lint.checkFileContents( - checkInfo: "if_as_guard: Don't use an if statement to just return – use guard for such cases instead.", + checkInfo: "IfAsGuard: Don't use an if statement to just return – use guard for such cases instead.", regex: #" +if [^\{]+\{\s*return\s*[^\}]*\}(?! *else)"#, matchingExamples: [" if x == 5 { return }", " if x == 5 {\n return nil\n}", " if x == 5 { return 500 }", " if x == 5 { return do(x: 500, y: 200) }"], nonMatchingExamples: [" if x == 5 {\n let y = 200\n return y\n}", " if x == 5 { someMethod(x: 500, y: 200) }", " if x == 500 { return } else {"], includeFilters: [swiftSourceFiles, swiftTestFiles] ) -// MARK: late_force_unwrapping_3 +// MARK: LateForceUnwrapping3 try Lint.checkFileContents( - checkInfo: "late_force_unwrapping_3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + checkInfo: "LateForceUnwrapping3: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -353,9 +353,9 @@ try Lint.checkFileContents( ] ) -// MARK: late_force_unwrapping_2 +// MARK: LateForceUnwrapping2 try Lint.checkFileContents( - checkInfo: "late_force_unwrapping_2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + checkInfo: "LateForceUnwrapping2: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -374,9 +374,9 @@ try Lint.checkFileContents( ] ) -// MARK: late_force_unwrapping_1 +// MARK: LateForceUnwrapping1 try Lint.checkFileContents( - checkInfo: "late_force_unwrapping_1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", + checkInfo: "LateForceUnwrapping1: Don't use ? first to force unwrap later – directly unwrap within the parantheses.", regex: [ "openingBrace": #"\("#, "callPart1": #"[^\s\?\.]+"#, @@ -393,9 +393,9 @@ try Lint.checkFileContents( ] ) -// MARK: logger +// MARK: Logger try Lint.checkFileContents( - checkInfo: "logger: Don't use `print` – use `log.message` instead.", + checkInfo: "Logger: Don't use `print` – use `log.message` instead.", regex: #"print\([^\n]+\)"#, matchingExamples: [#"print("Hellow World!")"#, #"print(5)"#, #"print(\n "hi"\n)"#], nonMatchingExamples: [#"log.message("Hello world!")"#], @@ -403,18 +403,18 @@ try Lint.checkFileContents( excludeFilters: [#"Sources/.*/Logger\.swift"#] ) -// MARK: readme +// MARK: Readme try Lint.checkFilePaths( - checkInfo: "readme: Each project should have a README.md file, explaining how to use or contribute to the project.", + checkInfo: "Readme: Each project should have a README.md file, explaining how to use or contribute to the project.", regex: #"^README\.md$"#, matchingExamples: ["README.md"], nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"], violateIfNoMatchesFound: true ) -// MARK: readme_path +// MARK: ReadmePath try Lint.checkFilePaths( - checkInfo: "readme_path: The README file should be named exactly `README.md`.", + 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"], @@ -426,9 +426,9 @@ try Lint.checkFilePaths( ] ) -// MARK: readme_top_level_title +// MARK: ReadmeTopLevelTitle try Lint.checkFileContents( - checkInfo: "readme_top_level_title: The README.md file should only contain a single top level title.", + checkInfo: "ReadmeTopLevelTitle: The README.md file should only contain a single top level title.", regex: #"(^|\n)#[^#](.*\n)*\n#[^#]"#, matchingExamples: [ """ @@ -453,9 +453,9 @@ try Lint.checkFileContents( includeFilters: [readmeFile] ) -// MARK: readme_typo_license +// MARK: ReadmeTypoLicense try Lint.checkFileContents( - checkInfo: "readme_typo_license: Misspelled word 'license'.", + checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.", regex: #"([\s#]L|l)isence([\s\.,:;])"#, matchingExamples: [" lisence:", "## Lisence\n"], nonMatchingExamples: [" license:", "## License\n"], From 84177fbccb5f24be88e4116d657499ed6f93eba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 22 Mar 2020 22:41:19 +0100 Subject: [PATCH 26/27] Prepare for first public release --- Formula/anylint.rb | 16 ++++++++++++++++ Makefile | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 33 +++++++++++++++++++++++++++------ 3 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 Formula/anylint.rb create mode 100644 Makefile diff --git a/Formula/anylint.rb b/Formula/anylint.rb new file mode 100644 index 0000000..d6f8682 --- /dev/null +++ b/Formula/anylint.rb @@ -0,0 +1,16 @@ +class Anylint < Formula + desc "Lint anything by combining the power of Swift & regular expressions" + homepage "https://github.com/Flinesoft/AnyLint" + url "https://github.com/Flinesoft/AnyLint.git", :tag => "0.1.0", :revision => "?" + head "https://github.com/Flinesoft/AnyLint.git" + + depends_on :xcode => ["11.3", :build] + + def install + system "make", "install", "prefix=#{prefix}" + end + + test do + system bin/"anylint", "-v" + end +end diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4c3b254 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +SHELL = /bin/bash + +prefix ?= /usr/local +bindir ?= $(prefix)/bin +libdir ?= $(prefix)/lib +srcdir = Sources + +REPODIR = $(shell pwd) +BUILDDIR = $(REPODIR)/.build +SOURCES = $(wildcard $(srcdir)/**/*.swift) + +.DEFAULT_GOAL = all + +.PHONY: all +all: anylint + +anylint: $(SOURCES) + @swift build \ + -c release \ + --disable-sandbox \ + --build-path "$(BUILDDIR)" + +.PHONY: install +install: anylint + @install -d "$(bindir)" "$(libdir)" + @install "$(BUILDDIR)/release/anylint" "$(bindir)" + +.PHONY: uninstall +uninstall: + @rm -rf "$(bindir)/anylint" + +.PHONY: clean +distclean: + @rm -f $(BUILDDIR)/release + +.PHONY: clean +clean: distclean + @rm -rf $(BUILDDIR) diff --git a/README.md b/README.md index 0c800e8..2242a9f 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ alt="Coverage"/> - Version: 0.0.0 + Version: 0.1.0 (@): -let checkInfo: CheckInfo = "readme_path: The README file should be named exactly `README.md`." +let checkInfo: CheckInfo = "ReadmePath: The README file should be named exactly `README.md`." ``` ### Check File Contents @@ -255,6 +274,8 @@ AnyLint allows you to do any kind of lint checks (thus its name) as it gives you 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. +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/AnyLint/blob/main/Sources/Utility/Regex.swift) (the project, the class was taken from) or read the [code documentation comments](https://github.com/Flinesoft/AnyLint/blob/main/Sources/Utility/Regex.swift). + When using the `customCheck`, you might want to also include some Swift packages for [easier file handling](https://github.com/JohnSundell/Files) or [running shell commands](https://github.com/JohnSundell/ShellOut). You can do so by adding them at the top of the file like so: > TODO: Improve the below code example with something more useful & realistic. From 8b8a9d3cd577a583662274dfb8c6ffd45bb851b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20Gu=CC=88ndu=CC=88z?= Date: Sun, 22 Mar 2020 22:45:41 +0100 Subject: [PATCH 27/27] Document initial release --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab46810..82d0bfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,3 +30,6 @@ If needed, pluralize to `Tasks`, `PRs` or `Authors` and list multiple entries se - None. ### Security - None. + +## [0.1.0] - 2020-03-22 +Initial public release.