diff --git a/.bazelrc b/.bazelrc index aef68378fb..12ae6e95c6 100644 --- a/.bazelrc +++ b/.bazelrc @@ -13,7 +13,6 @@ build --disk_cache=~/.bazel_cache build --experimental_remote_cache_compression build --remote_build_event_upload=minimal build --nolegacy_important_outputs -build --swiftcopt=-warnings-as-errors build:release \ --compilation_mode=opt \ diff --git a/.bcr/config.yml b/.bcr/config.yml index 89045ecc3b..c35c48cf09 100644 --- a/.bcr/config.yml +++ b/.bcr/config.yml @@ -1,3 +1,3 @@ fixedReleaser: - login: jpsim - email: jp@jpsim.com + login: SimplyDanny + email: danny.moesch@icloud.com diff --git a/.bcr/metadata.template.json b/.bcr/metadata.template.json index 907734edc3..84b6013c36 100644 --- a/.bcr/metadata.template.json +++ b/.bcr/metadata.template.json @@ -5,6 +5,11 @@ "email": "jp@jpsim.com", "github": "jpsim", "name": "JP Simard" + }, + { + "email": "danny.moesch@icloud.com", + "github": "SimplyDanny", + "name": "Danny Mösch" } ], "repository": [ diff --git a/.bcr/presubmit.yml b/.bcr/presubmit.yml index e1febf595f..0ca2eb510b 100644 --- a/.bcr/presubmit.yml +++ b/.bcr/presubmit.yml @@ -1,25 +1,26 @@ -shell_commands: &shell_commands -- "echo --- Downloading and extracting Swift $SWIFT_VERSION to $SWIFT_HOME" -- "mkdir $SWIFT_HOME" -- "curl https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2004/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu20.04.tar.gz | tar xvz --strip-components=1 -C $SWIFT_HOME" - tasks: verify_targets_linux: - name: Verify targets (Linux) + name: Verify Targets (Linux) platform: ubuntu2004 + bazel: 7.x environment: CC: "clang" - SWIFT_VERSION: "5.8.1" + SWIFT_VERSION: "5.10" SWIFT_HOME: "$HOME/swift-$SWIFT_VERSION" PATH: "$PATH:$SWIFT_HOME/usr/bin" - shell_commands: *shell_commands + shell_commands: + - "echo --- Downloading and extracting Swift $SWIFT_VERSION to $SWIFT_HOME" + - "mkdir $SWIFT_HOME" + - "curl https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2004/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu20.04.tar.gz | tar xvz --strip-components=1 -C $SWIFT_HOME" build_flags: - "--action_env=PATH" build_targets: - # TODO: Build `:swiftlint` target when the Swift compiler crash is fixed - - '@swiftlint//:SwiftLintFramework' + - '@swiftlint//:swiftlint' verify_targets_macos: - name: Verify targets (macOS) + name: Verify Targets (macOS) platform: macos + bazel: 7.x build_targets: - '@swiftlint//:swiftlint' + build_flags: + - "--repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1" diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 4fcb804d9d..22fb23aafe 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -7,8 +7,6 @@ steps: - bazel test --test_output=errors //Tests/... - label: "Build With Strict Concurrency" commands: - - echo "+++ Add @preconcurrency imports" - - ./tools/add-preconcurrency-imports.sh - echo "+++ Build" - bazel build --define strict_concurrency_builtin_rules=true :swiftlint - echo "--- Clean up" diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..7a704c91b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,40 @@ +--- +name: Bug Report +about: Create a report to help us improve. + +--- + +### New Issue Checklist + +- [ ] I've Updated SwiftLint to the latest version. +- [ ] I've searched for [existing GitHub issues](https://github.com/realm/SwiftLint/issues). + +### Bug Description + +A clear and concise description of what the bug is. Ideally, provide a small (but compilable) example code snippet that +can be used to reproduce the issue. + +```swift +// This triggers a violation: +let foo = try! bar() +``` + +Mention the command or other SwiftLint integration method that caused the issue. Include stack traces or command output. + +```bash +$ swiftlint lint [--no-cache] [--fix] +``` + +### Environment + +* SwiftLint version (run `swiftlint version` to be sure) +* Xcode version (run `xcodebuild -version` to be sure) +* Installation method used (Homebrew, CocoaPods, building from source, etc) +* Configuration file: + +```yml +# insert yaml contents here +``` + +Are you using [nested configurations](https://github.com/realm/SwiftLint#nested-configurations)? If so, paste their +relative paths and respective contents. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 000b5bdea1..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - -### New Issue Checklist - -- [ ] Updated SwiftLint to the latest version -- [ ] I searched for [existing GitHub issues](https://github.com/realm/SwiftLint/issues) - -### Describe the bug - -A clear and concise description of what the bug is. - -##### Complete output when running SwiftLint, including the stack trace and command used - -```bash -$ swiftlint lint -``` - -### Environment - -* SwiftLint version (run `swiftlint version` to be sure)? -* Installation method used (Homebrew, CocoaPods, building from source, etc)? -* Paste your configuration file: - -```yml -# insert yaml contents here -``` - -* Are you using [nested configurations](https://github.com/realm/SwiftLint#nested-configurations)? - If so, paste their relative paths and respective contents. -* Which Xcode version are you using (check `xcodebuild -version`)? -* Do you have a sample that shows the issue? Run `echo "[string here]" | swiftlint lint --no-cache --use-stdin --enable-all-rules` - to quickly test if your example is really demonstrating the issue. If your example is more - complex, you can use `swiftlint lint --path [file here] --no-cache --enable-all-rules`. - -```swift -// This triggers a violation: -let foo = try! bar() -``` diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000000..92b30675fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,15 @@ +--- +name: Feature or Enhancement Proposal +about: Let us know about a feature idea or propose an improvement. + +--- + +### New Issue Checklist + +- [ ] I've Updated SwiftLint to the latest version. +- [ ] I've searched for [existing GitHub issues](https://github.com/realm/SwiftLint/issues). + +### Feature or Enhancement Proposal + +Describe you idea or proposal here. This can be a new feature, an enhancement to an existing feature, or a change to the +project's behavior. Be sure to include the rationale behind the proposal and any relevant context or examples. diff --git a/.github/ISSUE_TEMPLATE/rule-request.md b/.github/ISSUE_TEMPLATE/rule-request.md index fedc4e92fd..1736669bae 100644 --- a/.github/ISSUE_TEMPLATE/rule-request.md +++ b/.github/ISSUE_TEMPLATE/rule-request.md @@ -1,6 +1,6 @@ --- -name: Rule request -about: Share your idea for a new rule +name: Rule Request +about: Share your idea for a new rule. --- @@ -9,10 +9,9 @@ about: Share your idea for a new rule - [ ] Updated SwiftLint to the latest version - [ ] I searched for [existing GitHub issues](https://github.com/realm/SwiftLint/issues) -### New rule request +### New Rule Request -Please describe the rule idea, format -this issue's title as `Rule Request: [Rule Name]` and describe: +Please describe the rule idea, format this issue's title as `Rule Request: [Rule Name]` and describe: 1. Why should this rule be added? Share links to existing discussion about what the community thinks about this. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..be006de9a1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly diff --git a/.github/plugins-sync.yml b/.github/plugins-sync.yml new file mode 100644 index 0000000000..87c26302d6 --- /dev/null +++ b/.github/plugins-sync.yml @@ -0,0 +1,2 @@ +SimplyDanny/SwiftLintPlugins: + - Plugins/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4c8cd4cadf..7736846b9e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,46 +1,48 @@ -name: docker +name: Docker Build on: push: branches: - main - tags: - - '*' + workflow_call: + inputs: + tag: + description: 'Docker tag' + required: true + type: string + default: 'latest' + workflow_dispatch: + inputs: + tag: + description: 'Docker tag' + required: true + type: string + default: 'latest' jobs: build: - runs-on: ubuntu-20.04 - + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 - - - name: Extract DOCKER_TAG using tag name - if: startsWith(github.ref, 'refs/tags/') - run: | - echo "DOCKER_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - - - name: Use default DOCKER_TAG - if: startsWith(github.ref, 'refs/tags/') != true - run: | - echo "DOCKER_TAG=latest" >> $GITHUB_ENV - + - uses: actions/checkout@v4 + - name: Set Docker tag + if: github.event_name != 'push' && ${{ inputs.tag }} + run: echo "DOCKER_TAG=${{ inputs.tag }}" >> $GITHUB_ENV + - name: Use default Docker tag + if: github.event_name == 'push' + run: echo "DOCKER_TAG=latest" >> $GITHUB_ENV - name: Set lowercase repository name - run: | - echo "REPOSITORY_LC=${REPOSITORY,,}" >>${GITHUB_ENV} + run: echo "REPOSITORY_LC=${REPOSITORY,,}" >> $GITHUB_ENV env: REPOSITORY: '${{ github.repository }}' - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - + uses: docker/setup-buildx-action@v3 - name: Login to GitHub registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + password: ${{ secrets.GITHUB_TOKEN }} registry: ghcr.io - - - uses: docker/build-push-action@v2 + - uses: docker/build-push-action@v6 with: push: true tags: ghcr.io/${{ env.REPOSITORY_LC }}:${{ env.DOCKER_TAG }} diff --git a/.github/workflows/plugins-sync.yml b/.github/workflows/plugins-sync.yml new file mode 100644 index 0000000000..e517bd3158 --- /dev/null +++ b/.github/workflows/plugins-sync.yml @@ -0,0 +1,24 @@ +name: Plugins Sync + +on: + push: + branches: + - main + paths: + - 'Plugins/**' + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Run file sync + uses: BetaHuhn/repo-file-sync-action@v1 + with: + GH_PAT: ${{ secrets.SIMPLYDANNY_PLUGINS_SYNC }} + IS_FINE_GRAINED: true + CONFIG_PATH: .github/plugins-sync.yml + SKIP_PR: true + COMMIT_PREFIX: 🔄 Workflow in 'realm/SwiftLint' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..9f6acc511a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +name: Release + +on: + release: + types: [released] + +jobs: + dispatch-plugins: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Parse checksum + id: parse_checksum + run: echo "checksum=$(grep -o '[a-fA-F0-9]\{64\}' Package.swift)" >> $GITHUB_OUTPUT + - name: Dispatch release of plugins package + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.SIMPLYDANNY_PLUGINS_SYNC }} + repository: SimplyDanny/SwiftLintPlugins + event-type: swiftlint-release + client-payload: |- + { + "title": "${{ github.event.release.name }}", + "tag": "${{ github.event.release.tag_name }}", + "checksum": "${{ steps.parse_checksum.outputs.checksum }}" + } + trigger-docker: + uses: ./.github/workflows/docker.yml + secrets: inherit + with: + tag: ${{ github.event.release.tag_name }} + upload-docker: + needs: trigger-docker + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Upload binary to existing release + run: make zip_linux_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + publish-pod: + runs-on: macOS-latest + steps: + - uses: actions/checkout@v4 + - name: Retrieve author in uppercase + id: retrieve_author + run: | + author=${{ github.event.release.author.login }} + AUTHOR=$(echo $author | tr '[:lower:]' '[:upper:]') + echo "name=${AUTHOR}" >> $GITHUB_OUTPUT + - name: Deploy to CocoaPods + run: make pod_publish + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets[format('COCOAPODS_TRUNK_TOKEN_{0}', steps.retrieve_author.outputs.name)] }} diff --git a/.jazzy.yaml b/.jazzy.yaml index 6ea87fd649..d25736b793 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -19,14 +19,3 @@ custom_categories: - name: Rules children: - Rule Directory - - name: Reporters - children: - - CSVReporter - - CheckstyleReporter - - CodeClimateReporter - - EmojiReporter - - GitHubActionsLoggingReporter - - HTMLReporter - - JSONReporter - - JUnitReporter - - MarkdownReporter diff --git a/.sourcery/BuiltInRules.stencil b/.sourcery/BuiltInRules.stencil index 45ef080360..79ffcc8a7b 100644 --- a/.sourcery/BuiltInRules.stencil +++ b/.sourcery/BuiltInRules.stencil @@ -1,5 +1,5 @@ /// The rule list containing all available rules built into SwiftLint. public let builtInRules: [any Rule.Type] = [ -{% for rule in types.structs where rule.name|hasSuffix:"Rule" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %} +{% for rule in types.structs where rule.name|hasSuffix:"Rule" %} {{ rule.name }}.self, {% endfor %}] diff --git a/.sourcery/GeneratedTests.stencil b/.sourcery/GeneratedTests.stencil index 6b9dc8a71e..cd6f14c0fc 100644 --- a/.sourcery/GeneratedTests.stencil +++ b/.sourcery/GeneratedTests.stencil @@ -7,7 +7,7 @@ import SwiftLintTestHelpers {% for rule in types.structs %} {% if rule.name|hasSuffix:"Rule" %} -class {{ rule.name }}GeneratedTests: SwiftLintTestCase { +final class {{ rule.name }}GeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule({{ rule.name }}.description) } diff --git a/.sourcery/ReportersList.stencil b/.sourcery/ReportersList.stencil index 404eda2942..6e35c46e6f 100644 --- a/.sourcery/ReportersList.stencil +++ b/.sourcery/ReportersList.stencil @@ -2,6 +2,6 @@ /// The reporters list containing all the reporters built into SwiftLint. public let reportersList: [any Reporter.Type] = [ {% for reporter in types.structs where reporter.name|hasSuffix:"Reporter" %} - {{ reporter.name }}.self{% if not forloop.last %},{% endif %} + {{ reporter.name }}.self, {% endfor %} ] diff --git a/.swiftlint.yml b/.swiftlint.yml index 5ed708e2b5..975ca897d8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,9 +1,13 @@ +# Directory and file filters included: - Plugins - Source - Tests + - Package.swift excluded: - Tests/SwiftLintFrameworkTests/Resources + +# Enabled/disabled rules analyzer_rules: - unused_declaration - unused_import @@ -11,9 +15,9 @@ opt_in_rules: - all disabled_rules: - anonymous_argument_in_multiline_closure - - anyobject_protocol - - closure_body_length + - async_without_await - conditional_returns_on_newline + - contrasted_opening_brace - convenience_type - discouraged_optional_collection - explicit_acl @@ -21,32 +25,23 @@ disabled_rules: - explicit_top_level_acl - explicit_type_interface - file_types_order - - final_test_case - force_unwrapping - function_default_parameter_at_end - - implicit_return - - implicitly_unwrapped_optional - indentation_width - inert_defer - missing_docs - multiline_arguments - multiline_arguments_brackets - multiline_function_chains - - multiline_literal_brackets - - multiline_parameters - multiline_parameters_brackets - no_extension_access_modifier - - no_fallthrough_only - no_grouping_extension - no_magic_numbers - one_declaration_per_file + - prefer_key_path # Re-enable once we are on Swift 6. - prefer_nimble - - prefer_self_in_static_references - prefixed_toplevel_constant - - redundant_self_in_closure - required_deinit - - self_binding - - shorthand_argument - sorted_enum_cases - strict_fileprivate - switch_case_on_newline @@ -57,17 +52,20 @@ disabled_rules: - vertical_whitespace_between_cases - json_decoding +# Configurations attributes: always_on_line_above: - "@ConfigurationElement" - "@OptionGroup" - "@RuleConfigurationDescriptionBuilder" -identifier_name: - excluded: - - id -large_tuple: 3 -number_separator: - minimum_length: 5 +balanced_xctest_lifecycle: &unit_test_configuration + test_parent_classes: + - SwiftLintTestCase + - XCTestCase +closure_body_length: + warning: 50 + error: 100 +empty_xctest_method: *unit_test_configuration file_name: excluded: - Exports.swift @@ -75,19 +73,28 @@ file_name: - RuleConfigurationMacros.swift - SwiftSyntax+SwiftLint.swift - TestHelpers.swift -unneeded_override: - affect_initializers: true - -balanced_xctest_lifecycle: &unit_test_configuration - test_parent_classes: - - SwiftLintTestCase - - XCTestCase -empty_xctest_method: *unit_test_configuration -single_test_class: *unit_test_configuration - +final_test_case: *unit_test_configuration function_body_length: 60 +identifier_name: + excluded: + - id +large_tuple: 3 +number_separator: + minimum_length: 5 +redundant_type_annotation: + consider_default_literal_types_redundant: true +single_test_class: *unit_test_configuration +trailing_comma: + mandatory_comma: true type_body_length: 400 +unneeded_override: + affect_initializers: true +unused_import: + always_keep_imports: + - SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes + - SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused +# Custom rules custom_rules: rule_id: included: Source/SwiftLintBuiltInRules/Rules/.+/\w+\.swift @@ -109,8 +116,3 @@ custom_rules: message: Rule Test Function mustn't end with `rule` regex: func\s*test\w+(r|R)ule\(\) severity: error - -unused_import: - always_keep_imports: - - SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes - - SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused diff --git a/BUILD b/BUILD index 7475808562..f6e1b93ccf 100644 --- a/BUILD +++ b/BUILD @@ -26,8 +26,17 @@ config_setting( ) copts = [ + "-warnings-as-errors", "-enable-upcoming-feature", "ExistentialAny", + "-enable-upcoming-feature", + "ConciseMagicFile", + "-enable-upcoming-feature", + "ImportObjcForwardDeclarations", + "-enable-upcoming-feature", + "ForwardTrailingClosures", + "-enable-upcoming-feature", + "ImplicitOpenExistentials", ] strict_concurrency_copts = [ @@ -82,7 +91,7 @@ swift_library( "@SwiftSyntax//:SwiftParserDiagnostics_opt", "@SwiftSyntax//:SwiftSyntaxBuilder_opt", "@SwiftSyntax//:SwiftSyntax_opt", - "@com_github_jpsim_sourcekitten//:SourceKittenFramework", + ":SourceKittenFramework.wrapper", "@sourcekitten_com_github_jpsim_yams//:Yams", "@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable", ] + select({ @@ -91,14 +100,21 @@ swift_library( }), ) +swift_library( + name = "SourceKittenFramework.wrapper", + srcs = ["Source/SourceKittenFrameworkWrapper/Empty.swift"], + module_name = "SourceKittenFrameworkWrapper", + visibility = ["//visibility:public"], + deps = [ + "@com_github_jpsim_sourcekitten//:SourceKittenFramework", + ], +) + swift_library( name = "SwiftLintBuiltInRules", package_name = "SwiftLint", srcs = glob(["Source/SwiftLintBuiltInRules/**/*.swift"]), - copts = copts + select({ - ":strict_concurrency_builtin_rules": strict_concurrency_copts, - "//conditions:default": [], - }), + copts = copts + strict_concurrency_copts, module_name = "SwiftLintBuiltInRules", visibility = ["//visibility:public"], deps = [ @@ -134,33 +150,23 @@ swift_library( ":SwiftLintBuiltInRules", ":SwiftLintCore", ":SwiftLintExtraRules", + "@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit", ], ) -swift_library( - name = "swiftlint.library", +swift_binary( + name = "swiftlint", package_name = "SwiftLint", srcs = glob(["Source/swiftlint/**/*.swift"]), - copts = copts, # TODO: strict_concurrency_copts - module_name = "swiftlint", + copts = copts + strict_concurrency_copts, visibility = ["//visibility:public"], deps = [ ":SwiftLintFramework", - "@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit", "@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser", "@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable", ], ) -swift_binary( - name = "swiftlint", - copts = copts + strict_concurrency_copts, - visibility = ["//visibility:public"], - deps = [ - ":swiftlint.library", - ], -) - apple_universal_binary( name = "universal_swiftlint", binary = ":swiftlint", diff --git a/CHANGELOG.md b/CHANGELOG.md index d67fd76ba7..5b716a67cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,259 @@ #### Breaking +* The command plugin now requires write permissions so that it works with the `--fix` option without an error. + [SimplyDanny](https://github.com/SimplyDanny) + +* The artifact bundle name has changed. `SwiftLintBinary-macos.artifactbundle.zip` is now called + `SwiftLintBinary.artifactbundle.zip`. It now includes an AMD64 Linux binary. + [Bradley Mackey](https://github.com/bradleymackey) + [#5514](https://github.com/realm/SwiftLint/issues/5514) + +#### Experimental + +* None. + +#### Enhancements + +* Add Xcode command plugin allowing to run SwiftLint from within Xcode. + [SimplyDanny](https://github.com/SimplyDanny) + +* Add new `async_without_await` opt-in rule that checks if an `async` declaration contains at least one `await`. + [Jan Kolarik](https://github.com/jkolarik-paylocity) + [#5082](https://github.com/realm/SwiftLint/issues/5082) + +* Support replacing identity expressions with `\.self` in `prefer_key_path` rule from Swift 6 on. + [SimplyDanny](https://github.com/SimplyDanny) + +#### Bug Fixes + +* None. + +## 0.57.1: Squeaky Clean Cycle + +#### Breaking + +* None. + +#### Experimental + +* None. + +#### Enhancements + +* Suggest failable `String(bytes:encoding:)` initializer in + `optional_data_string_conversion` rule as it accepts all `Sequence` + types. + [Jordan Rose](https://github.com/jrose-signal) + [SimplyDanny](https://github.com/SimplyDanny) + +* Support reading files to lint from Input File Lists provided + by Run Script Build Phases in Xcode using the command-line + argument `--use-script-input-file-lists`. + [BlueVirusX](https://github.com/BlueVirusX) + +* Adds a `lenient` configuration file setting, equivalent to the `--lenient` + command line option. + [Martin Redington](https://github.com/mildm8nnered) + [#5801](https://github.com/realm/SwiftLint/issues/5801) + +* Support type casting on configuration option values defined by environment variables. + Without a cast, these values would always be treated as strings leading to a potentially + invalid configuration. + [SimplyDanny](https://github.com/SimplyDanny) + [#5774](https://github.com/realm/SwiftLint/issues/5774) + +* Add new option `max_number_of_single_line_parameters` that allows only the specified maximum + number of parameters to be on one line when `allows_single_line = true`. If the limit is + exceeded, the rule will still trigger. Confusing option combinations like `allows_single_line = false` + together with `max_number_of_single_line_parameters > 1` will be reported. + [kimdv](https://github.com/kimdv) + [SimplyDanny](https://github.com/SimplyDanny) + [#5781](https://github.com/realm/SwiftLint/issues/5781) + +* The `redundant_type_annotation` rule gains a new option, + `ignore_properties`, that skips enforcement on members in a + type declaration (like a `struct`). This helps the rule coexist with + the `explicit_type_interface` rule that requires such redundancy. + [jaredgrubb](https://github.com/jaredgrubb) + [#3750](https://github.com/realm/SwiftLint/issues/3750) + +* Allow inherited isolation parameter to be first in function signatures + depending on the new option `ignore_first_isolation_inheritance_parameter` + which is `true` by default. + [SimplyDanny](https://github.com/SimplyDanny) + [#5793](https://github.com/realm/SwiftLint/issues/5793) + +#### Bug Fixes + +* Run command plugin in whole package if no targets are defined in the + package manifest. + [SimplyDanny](https://github.com/SimplyDanny) + [#5787](https://github.com/realm/SwiftLint/issues/5787) + +* Silence `superfluous_else` rule on `if` expressions with only a single + availability condition. + [SimplyDanny](https://github.com/SimplyDanny) + [#5833](https://github.com/realm/SwiftLint/issues/5833) + +* Stop triggering the `control_statement` rule on closures being directly + called as conditions. + [SimplyDanny](https://github.com/SimplyDanny) + [#5846](https://github.com/realm/SwiftLint/issues/5846) + +* Do not trigger `self_in_property_initialization` rule on `self` in + key paths expressions. + [SimplyDanny](https://github.com/SimplyDanny) + [#5835](https://github.com/realm/SwiftLint/issues/5835) + +* Allow to specify transitive modules to be taken into account by + `unused_import` rule. This avoids that required imports are removed. + [Paul Taykalo](https://github.com/PaulTaykalo) + [SimplyDanny](https://github.com/SimplyDanny) + [#5167](https://github.com/realm/SwiftLint/issues/5167) + +* Only pass cache path and directory paths to commands that accept these arguments + in the command plugin. + [SimplyDanny](https://github.com/SimplyDanny) + [#5848](https://github.com/realm/SwiftLint/issues/5848) + +* Do not throw deprecation warning if deprecated property is not + presented in configuration. + [chipp](https://github.com/chipp) + [#5791](https://github.com/realm/SwiftLint/issues/5791) + +* The `prefer_type_checking` rule will no longer trigger for non-optional + type casting (`as`), or for comparisons to optional types. + [Martin Redington](https://github.com/mildm8nnered) + [#5802](https://github.com/realm/SwiftLint/issues/5802) + +* Fixes an issue where the `superfluous_disable_command` rule could generate + false positives for nested disable commands for custom rules. + [Martin Redington](https://github.com/mildm8nnered) + [#5788](https://github.com/realm/SwiftLint/issues/5788) + +* Fixes the `--only-rule` command line option, when a default `.swiftlint.yml` + is absent. Additionally rules specified with `--only-rule` on the command + line can now be disabled in a child configuration, to allow specific + directories to be excluded from the rule (or from being auto-corrected by + the rule), and `--only-rule` can now be specified multiple times + to run multiple rules. + [Martin Redington](https://github.com/mildm8nnered) + [#5711](https://github.com/realm/SwiftLint/issues/5711) + +* Fixes `file_name` rule to match fully-qualified names of nested types. + Additionally adds a `require_fully_qualified_names` boolean option to enforce + that file names match nested types only using their fully-qualified name. + [fraioli](https://github.com/fraioli) + [#5840](https://github.com/realm/SwiftLint/issues/5840) + +* Fixes an issue where the `vertical_whitespace_between_cases` rule does not + recognize `@unknown default`. + [Jared Grubb](https://github.com/jaredtrubb) + [#5788](https://github.com/realm/SwiftLint/issues/3511) + +## 0.57.0: Squeaky Clean Cycle + +#### Breaking + +* The deprecated `anyobject_protocol` rule has now been removed. + [Martin Redington](https://github.com/mildm8nnered) + [#5769](https://github.com/realm/SwiftLint/issues/5769) + +* Revert the part of the `non_optional_string_data_conversion` + rule that enforces non-failable conversions of `Data` to UTF-8 + `String`. This is due to the fact that the data to be converted + can be arbitrary and especially doesn't need to represent a valid + UTF-8-encoded string. + [Sam Rayner](https://github.com/samrayner) + [#5263](https://github.com/realm/SwiftLint/issues/5263) + +#### Experimental + +* None. + +#### Enhancements + +* Add `ignore_multiline_type_headers` and `ignore_multiline_statement_conditions` + options to `opening_brace` rule to allow opening braces to be on a new line after + multiline type headers or statement conditions. Rename `allow_multiline_func` to + `ignore_multiline_function_signatures`. + [leonardosrodrigues0](https://github.com/leonardosrodrigues0) + [#3720](https://github.com/realm/SwiftLint/issues/3720) + +* Add new `optional_data_string_conversion` rule to enforce + failable conversions of `Data` to UTF-8 `String`. + [Sam Rayner](https://github.com/samrayner) + [#5263](https://github.com/realm/SwiftLint/issues/5263) + +* The `no_magic_numbers` rule will now ignore violations in + SwiftUI's `Preview` macro. + [Martin Redington](https://github.com/mildm8nnered) + [#5778](https://github.com/realm/SwiftLint/issues/5778) + +#### Bug Fixes + +* `superfluous_disable_command` violations are now triggered for + custom rules. + [Marcelo Fabri](https://github.com/marcelofabri) + [Martin Redington](https://github.com/mildm8nnered) + [SimplyDanny](https://github.com/SimplyDanny) + [#4754](https://github.com/realm/SwiftLint/issues/4754) + +* Trailing comments are now preserved by the `opening_brace` rule when + rewriting. + [Martin Redington](https://github.com/mildm8nnered) + [#5751](https://github.com/realm/SwiftLint/issues/5751) + +## 0.56.2: Heat Pump Dryer + +#### Breaking + +* None. + +#### Experimental + +* None. + +#### Enhancements + +* None. + +#### Bug Fixes + +* Ignore initializers with attributes in `unneeded_synthesized_initializer` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5153](https://github.com/realm/SwiftLint/issues/5153) + +* Silence `prefer_key_path` rule on macro expansion expressions. + [SimplyDanny](https://github.com/SimplyDanny) + [#5744](https://github.com/realm/SwiftLint/issues/5744) + +* Check `if` expressions nested arbitrarily deep in `contrasted_opening_brace` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5752](https://github.com/realm/SwiftLint/issues/5752) + +* Align left closure brace with associated parent function call in `contrasted_opening_brace` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5752](https://github.com/realm/SwiftLint/issues/5752) + +* Align left brace of additional trailing closures with right brace of previous trailing closure + in `contrasted_opening_brace` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5752](https://github.com/realm/SwiftLint/issues/5752) + +* Trigger on empty closure blocks in `no_empty_block` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5762](https://github.com/realm/SwiftLint/issues/5762) + +* Silence `unneeded_override` rule on methods and initializers with attributes. + [SimplyDanny](https://github.com/SimplyDanny) + [#5753](https://github.com/realm/SwiftLint/issues/5753) + +## 0.56.1: Heat Pump Dryer + +#### Breaking + * None. #### Experimental @@ -10,6 +263,259 @@ #### Enhancements +* None. + +#### Bug Fixes + +* Let `contrasted_opening_brace` be an opt-in rule. + [SimplyDanny](https://github.com/SimplyDanny) + +## 0.56.0: Heat Pump Dryer + +#### Breaking + +* The deprecated `--path` and `--in-process-sourcekit` arguments have now been + removed completely. + [Martin Redington](https://github.com/mildm8nnered) + [SimplyDanny](https://github.com/SimplyDanny) + [#5614](https://github.com/realm/SwiftLint/issues/5614) + +* When SwiftLint corrects violations automatically (`swiftlint lint --fix`) + it doesn't report the exact location of the fix any longer. The new format + is `: Correcting ` without line and column numbers. + Reason: Correction positions are likely just incorrect, especially when + multiple rules apply their rewrites. Fixing that is not trivial and likely + not worth the effort also considering that there haven't been any bug + reports about wrong correction positions so far. + [SimplyDanny](https://github.com/SimplyDanny) + +#### Experimental + +* None. + +#### Enhancements + +* Add new `attribute_name_spacing` rule to enforce no trailing whitespace between + attribute names and parentheses, ensuring compatibility with Swift 6, where this spacing + causes compilation errors. + [aryamansharda](https://github.com/aryamansharda) + [#5667](https://github.com/realm/SwiftLint/issues/5667) + +* Linting got up to 30% faster due to the praisworthy performance + improvements done in the [SwiftSyntax](https://github.com/swiftlang/swift-syntax) + library. + +* Rewrite the following rules with SwiftSyntax: + * `missing_docs` + + [woxtu](https://github.com/woxtu) + [SimplyDanny](https://github.com/SimplyDanny) + +* Add new `prefer_key_path` rule that triggers when a trailing closure on a standard + function call is only hosting a (chained) member access expression since the closure + can be replaced with a key path argument. Likewise, it triggers on closure arguments. + [SimplyDanny](https://github.com/SimplyDanny) + +* Adds `baseline` and `write_baseline` configuration file settings, equivalent + to the `--baseline` and `--write-baseline` command line options. + [Martin Redington](https://github.com/mildm8nnered) + [#5552](https://github.com/realm/SwiftLint/issues/5552) + +* Add `no_empty_block` opt-in rule to validate that code blocks are not empty. + They should at least contain a comment. + [Ueeek](https://github.com/Ueeek) + [#5615](https://github.com/realm/SwiftLint/issues/5615) + +* Add new `contrasted_opening_brace` rule that enforces opening + braces to be on a separate line after the preceding declaration. + [SimplyDanny](https://github.com/SimplyDanny) + +* Add new `unused_parameter` rule that triggers on function/initializer/subscript + parameters that are not used inside of the function/initializer/subscript. + [SimplyDanny](https://github.com/SimplyDanny) + [#2120](https://github.com/realm/SwiftLint/issues/2120) + +* Support `--target` paths being passed to command plugin by Xcode. + [SimplyDanny](https://github.com/SimplyDanny) + [#5603](https://github.com/realm/SwiftLint/issues/5603) + +* Add modified configurations to examples in rule documentation. + [SimplyDanny](https://github.com/SimplyDanny) + +* Add new option `evaluate_effective_access_control_level` to `missing_docs` + rule. Setting it to `true` stops the rule from triggering on declarations + inside of types with lower visibility. These declarations effectively + have at most the same access level. + [SimplyDanny](https://github.com/SimplyDanny) + +* Add new `--check-for-updates` command line option for the `lint`, `analyze`, + and `version` subcommands to check for new versions of SwiftLint, and an + equivalent `check_for_updates` configuration file setting. + [Martin Redington](https://github.com/mildm8nnered) + [SimplyDanny](https://github.com/SimplyDanny) + [Ian Leitch](https://github.com/ileitch) + [#5613](https://github.com/realm/SwiftLint/issues/5613) + +* Add new `--only-rule` command line option for the `lint` and `analyze`, + subcommands that overrides configuration file rule enablement and + disablement, in particular to facilitate running `--fix` for single rules + without having to temporarily edit the configuration file. + [Martin Redington](https://github.com/mildm8nnered) + [#5666](https://github.com/realm/SwiftLint/issues/5666) + +#### Bug Fixes + +* Fix a few false positives and negatives by updating the parser to support + Swift 6 with all its new language constructs. + [SimplyDanny](https://github.com/SimplyDanny) + +* Stop triggering `mark` rule on "mark" comments in the middle of another + comment. + [SimplyDanny](https://github.com/SimplyDanny) + [#5592](https://github.com/realm/SwiftLint/issues/5592) + +* Don't consider specialized imports with attributes as duplicates in + `duplicate_imports` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5716](https://github.com/realm/SwiftLint/issues/5716) + +* Use correct types and relative paths in SARIF reporter output. Generally + avoid escaping slashes in JSON output as well. + [SimplyDanny](https://github.com/SimplyDanny) + [#5598](https://github.com/realm/SwiftLint/issues/5598) + [#5599](https://github.com/realm/SwiftLint/issues/5599) + +* Keep initializers with attributed parameters in + `unneeded_synthesized_initializer` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5153](https://github.com/realm/SwiftLint/issues/5153) + +* Make `vertical_whitespace_between_cases` rule work for + cases ending with a string literal. + [ilendemli](https://github.com/ilendemli) + [#5612](https://github.com/realm/SwiftLint/issues/5612) + +* Ignore access level modifiers restricted to value setting in + `extension_access_modifier` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5623](https://github.com/realm/SwiftLint/issues/5623) + +* Fix `baseline compare` incorrectly reporting some violations + as new, and also now sorts the violations from `baseline compare` + deterministically. + [Martin Redington](https://github.com/mildm8nnered) + [#5606](https://github.com/realm/SwiftLint/issues/5606) + +* Fix rewriting for `implicit_return` rule when violations are + nested within each other. + [Martin Redington](https://github.com/mildm8nnered) + [#5660](https://github.com/realm/SwiftLint/issues/5660) + +* Fix `opening_brace` correction and make sure that disable commands + are taken into account before applying a fix. + [swiftty](https://github.com/swiftty) + [SimplyDanny](https://github.com/SimplyDanny) + [#5598](https://github.com/realm/SwiftLint/issues/5598) + +* Violations of the `typesafe_array_init` rule will now be correctly + reported as such, instead of as violations of the `array_init` + rule. + [Martin Redington](https://github.com/mildm8nnered) + [#5709](https://github.com/realm/SwiftLint/issues/5709) + +## 0.55.1: Universal Washing Powder + +#### Breaking + +* None. + +#### Experimental + +* None. + +#### Enhancements + +* Clarify wording of `static_over_final_class` rule's violation message. + [SimplyDanny](https://github.com/SimplyDanny) + [#5570](https://github.com/realm/SwiftLint/issues/5570) + +#### Bug Fixes + +* Fix Bazel build when `bzlmod` is not in use by adding transitive dependencies + explicitly. + [SimplyDanny](https://github.com/SimplyDanny) + [#5568](https://github.com/realm/SwiftLint/issues/5568) + +* Treat condionally activatable variable declarations and initializer as if + they were always active in `unneeded_synthesized_initializer` rule to avoid + compilation issues when unexpected items are there after all. + [SimplyDanny](https://github.com/SimplyDanny) + [#5574](https://github.com/realm/SwiftLint/issues/5574) + +* Silence `unused_enumerated` rule when `$0` in a closure is explicitly unpacked. + [SimplyDanny](https://github.com/SimplyDanny) + [#5573](https://github.com/realm/SwiftLint/issues/5573) + +* Remove redundant initializers in `unneeded_override` rule only when checking + initializers is actually enabled in the configuration. + [SimplyDanny](https://github.com/SimplyDanny) + [#5571](https://github.com/realm/SwiftLint/issues/5571) + +* Respect comments before opening brace in `opening_brace` rule when there is + one space before the brace after the comment. Everything else is still a + violation, yet the rewriter will not remove the comment anymore. + [SimplyDanny](https://github.com/SimplyDanny) + [#5578](https://github.com/realm/SwiftLint/issues/5578) + +## 0.55.0: Universal Washing Powder + +#### Breaking + +* Rewrite `SwiftLintBuildToolPlugin` using `BUILD_WORKSPACE_DIRECTORY` without relying + on the `--config` option. + [Garric Nahapetian](https://github.com/garricn) + +* Introduce SwiftLintCommandPlugin. + Rename SwiftLintBuildToolPlugin. + Add Swift Package Manager installation instructions. + [garricn](https://github.com/garricn) + +* Fix Code Climate reporter output by having lower case severity + values to comply with the Code Climate specification. + [waitButY](https://github.com/waitbutY) + +* The `superfluous_disable_command` rule will now be enabled for the `analyze` + command, unless it has been disabled, and will warn about superfluous + disablement of analyzer rules. + [Martin Redington](https://github.com/mildm8nnered) + [#4792](https://github.com/realm/SwiftLint/issues/4792) + +* With the introduction of the `consider_default_literal_types_redundant` + option to the `redundant_type_annotation` rule, `Bool` literals will no + longer be considered redundant by default. Set this option to true to + preserve the previous behavior. + [Garric Nahapetian](https://github.com/garricn) + +#### Experimental + +* Add two new options to the `lint` and `analyze` commands: `--write-baseline` + to save a baseline to disk, and `--baseline` to read a saved baseline and + use it to filter out detected pre-existing violations. A new `baseline` + command uses the reporters to print the violations in a baseline. + [Martin Redington](https://github.com/mildm8nnered) + [#5475](https://github.com/realm/SwiftLint/pull/5475) + [#3421](https://github.com/realm/SwiftLint/pull/3421) + +#### Enhancements + +* Add a reporter that outputs violations in the Static + Analysis Results Interchange Format (SARIF). + [waitButY](https://github.com/waitbutY) + +* Ignore absence of a non-initial local config instead of + falling back to default. + [kohtenko](https://github.com/kohtenko) + * Add new option `ignore_typealiases_and_associatedtypes` to `nesting` rule. It excludes `typealias` and `associatedtype` declarations from the analysis. @@ -20,6 +526,11 @@ [Julien Baillon](https://github.com/julien-baillon) [#5372](https://github.com/realm/SwiftLint/issues/5372) +* Allow to set the severity of rules (if they have one) in the short form + `rule_name: warning|error` provided that no other attributes need to be + configured. + [SimplyDanny](https://github.com/SimplyDanny) + * Add new `ignore_one_liners` option to `switch_case_alignment` rule to ignore switch statements written in a single line. [tonell-m](https://github.com/tonell-m) @@ -31,12 +542,23 @@ [SimplyDanny](https://github.com/SimplyDanny) [#70](https://github.com/realm/SwiftLint/issues/70) +* Warn when `--fix` comes together with `--strict` or `--lenient` as only `--fix` + takes effect then. + [SimplyDanny](https://github.com/SimplyDanny) + [#5387](https://github.com/realm/SwiftLint/pull/5387) + * Add new `one_declaration_per_file` rule that allows only a single class/struct/enum/protocol declaration per file. Extensions are an exception; more than one is allowed. [Muhammad Zeeshan](https://github.com/mzeeshanid) [#2802](https://github.com/realm/SwiftLint/issues/2802) +* Add new `ignore_attributes` option to `redundant_type_annotation` rule + that allows disabling the rule for properties that are marked with at least + one of the configured attributes. + [tonell-m](https://github.com/tonell-m) + [#5366](https://github.com/realm/SwiftLint/issues/5366) + * Rewrite the following rules with SwiftSyntax: * `explicit_acl` * `extension_access_modifier` @@ -48,6 +570,7 @@ * `nimble_operator` * `opening_brace` * `orphaned_doc_comment` + * `redundant_type_annotation` * `trailing_closure` * `void_return` @@ -56,6 +579,7 @@ [Marcelo Fabri](https://github.com/marcelofabri) [swiftty](https://github.com/swiftty) [KS1019](https://github.com/KS1019) + [tonell-m](https://github.com/tonell-m) * Print invalid keys when configuration parsing fails. [SimplyDanny](https://github.com/SimplyDanny) @@ -103,8 +627,8 @@ [SimplyDanny](https://github.com/SimplyDanny) [#5418](https://github.com/realm/SwiftLint/pull/5418) -* Add new `non_optional_string_data_conversion` rule to enforce - non-failable conversions of UTF-8 `String` <-> `Data`. +* Add new `non_optional_string_data_conversion` rule to enforce + non-failable conversions of UTF-8 `String` <-> `Data`. [Ben P](https://github.com/ben-p-commits) [#5263](https://github.com/realm/SwiftLint/issues/5263) @@ -121,12 +645,49 @@ * Make `empty_count` auto-correctable. [KS1019](https://github.com/KS1019/) + +* Make `private_swiftui_state` auto-correctable. + [mt00chikin](https://github.com/mt00chikin) * Make `trailing_closure` correctable. [KS1019](https://github.com/KS1019/) +* Add new `static_over_final_class` rule to prefer `static` over + `final class` declaration. + [phlippieb](https://github.com/phlippieb) + [#5471](https://github.com/realm/SwiftLint/issues/5471) + +* Extends `unused_enumerated` rule to cover closure parameters, to + detect cases like `list.enumerated().map { idx, _ in idx }` and + `list.enumerated().map { $1 }`. + [Martin Redington](https://github.com/mildm8nnered) + [#5470](https://github.com/realm/SwiftLint/issues/5470) + +* Include `Double`, `Int` and `String` to the exiting redundant type validation + check of `Bool` in the `redundant_type_annotation` rule. Add + `consider_default_literal_types_redundant` option supporting `Bool`, + `Double`, `Int` and `String`. Setting this option to `true` lets the rule + consider said types in declarations like `let i: Int = 1` or + `let s: String = ""` as redundant. + [Garric Nahapetian](https://github.com/garricn) + +* Add new `prefer_type_checking` rule to prefer `a is X` over `a as? X != nil`. + [ikelax](https://github.com/ikelax) + [mildm8nnered](https://github.com/mildm8nnered) + [#5295](https://github.com/realm/SwiftLint/issues/5295) + #### Bug Fixes +* Invalid keys in a configuration don't lead to the default configuration being + used anymore. The invalid key will just be reported but otherwise ignored. + [SimplyDanny](https://github.com/SimplyDanny) + [#5565](https://github.com/realm/SwiftLint/issues/5565) + +* Fix version comparison algorithm which caused some version-dependent rules to + misbehave with Swift 5.10. + [chandlerwall](https://github.com/chandlerwall) + [#5517](https://github.com/realm/SwiftLint/issues/5517) + * Silence `discarded_notification_center_observer` rule in closures. Furthermore, handle `get` and `set` accessors correctly and consider implicit returns. [SimplyDanny](https://github.com/SimplyDanny) @@ -137,6 +698,15 @@ [SimplyDanny](https://github.com/SimplyDanny) [#4801](https://github.com/realm/SwiftLint/pull/4801) +* Support `private_over_fileprivate` rule for actors. + [SimplyDanny](https://github.com/SimplyDanny) + [#5489](https://github.com/realm/SwiftLint/pull/5489) + +* Ensure that declarations referenced only as extended types do not count as + used by means of the `unused_declaration` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5550](https://github.com/realm/SwiftLint/issues/5550) + * Fix some false positives in `multiline_literal_brackets` rule that would happen when comments are present. [Marcelo Fabri](https://github.com/marcelofabri) @@ -166,7 +736,12 @@ are defined in a tuple like `let (a, b) = (5, 10)` or `let a = (2, 3)`. [Martin Redington](https://github.com/mildm8nnered) [#5305](https://github.com/realm/SwiftLint/pull/5305) - + +* Take array and nested types into account in `redundant_type_annotation` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#3141](https://github.com/realm/SwiftLint/pull/3141) + [#3146](https://github.com/realm/SwiftLint/pull/3146) + * Silence `pattern_matching_keywords` rule when an identifier is referenced in the argument list of a matching enum case. [SimplyDanny](https://github.com/SimplyDanny) @@ -175,6 +750,28 @@ * Don't trigger the `return_value_from_void_function` warning from initializers. [mrbkap](https://github.com/mrbkap) +* Fixes superfluous warnings about configurations for rules that were not + enabled, when the rules were enabled in a parent configuration. + [Martin Redington](https://github.com/mildm8nnered) + [#4858](https://github.com/realm/SwiftLint/issues/4858) + +* Add `all` pseudo-rule for `analyzer_rules` - enables all analyzer rules + that are not listed in `disabled_rules`. + [woxtu](https://github.com/woxtu) + [Martin Redington](https://github.com/mildm8nnered) + [#4999](https://github.com/realm/SwiftLint/issues/4999) + +* Updates the reasons provided by violations of the `blanket_disable_command` + to omit language about the end of the file, and to direct users to + re-enable the rule as soon as possible. + [Martin Redington](https://github.com/mildm8nnered) + [#5450](https://github.com/realm/SwiftLint/issues/5450) + +* Add a `--working-directory` command line option, for users who cannot + otherwise control which directory SwiftLint is run from. + [Martin Redington](https://github.com/mildm8nnered) + [#5424](https://github.com/realm/SwiftLint/issues/5424) + ## 0.54.0: Macro-Economic Forces #### Breaking @@ -841,10 +1438,6 @@ * Catch more valid `no_magic_numbers` violations. [JP Simard](https://github.com/jpsim) -* Rewrite `SwiftLintPlugin` using `BUILD_WORKSPACE_DIRECTORY` without relying - on the `--config` option. - [Garric Nahapetian](https://github.com/garricn) - * Add `blanket_disable_command` rule that checks whether rules are re-enabled after being disabled. [Martin Redington](https://github.com/mildm8nnered) @@ -1454,7 +2047,7 @@ accordingly._ progress bar instead of each file being processed. [JP Simard](https://github.com/jpsim) -* `--fix` now works with `--use-stdin`, printing the output to to STDOUT instead +* `--fix` now works with `--use-stdin`, printing the output to STDOUT instead of crashing. [SimplyDanny](https://github.com/SimplyDanny) [#4127](https://github.com/realm/SwiftLint/issues/4127) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a9f305a99..51d771aaa8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,6 +158,21 @@ We follow the same syntax as CocoaPods' CHANGELOG.md: you may instead link to the change's pull request. 1. All CHANGELOG.md content is hard-wrapped at 80 characters. +## Cutting a Release + +SwiftLint maintainers follow these steps to cut a release: + +1. Come up with a witty washer- or dryer-themed release name. Past names include: + * Tumble Dry + * FabricSoftenerRule + * Top Loading + * Fresh Out Of The Dryer +1. Make sure you have the latest stable Xcode version installed and `xcode-select`ed. +1. Make sure that the selected Xcode has the latest SDKs of all supported platforms installed. This is required to + build the CocoaPods release. +1. Release a new version by running `make release "0.2.0: Tumble Dry"`. +1. Celebrate. :tada: + ## CI SwiftLint uses Azure Pipelines for most of its CI jobs, primarily because diff --git a/Dockerfile b/Dockerfile index cef955886e..d4b0c2f92e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -# Explicitly specify `jammy` to keep the Swift & Ubuntu images in sync. -ARG BUILDER_IMAGE=swift:5.9-jammy -ARG RUNTIME_IMAGE=ubuntu:jammy +# Explicitly specify `noble` to keep the Swift & Ubuntu images in sync. +ARG BUILDER_IMAGE=swift:6.0-noble +ARG RUNTIME_IMAGE=ubuntu:noble # Builder image FROM ${BUILDER_IMAGE} AS builder @@ -15,13 +15,14 @@ COPY Tests Tests/ COPY Package.* ./ RUN swift package update -ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -lCFURLSessionInterface -Xlinker -lCFXMLInterface -Xlinker -lcurl -Xlinker -lxml2 -Xswiftc -I. -Xlinker -fuse-ld=lld -Xlinker -L/usr/lib/swift/linux" +ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -l_CFURLSessionInterface -Xlinker -l_CFXMLInterface -Xlinker -lcurl -Xlinker -lxml2 -Xswiftc -I. -Xlinker -fuse-ld=lld -Xlinker -L/usr/lib/swift/linux" RUN swift build $SWIFT_FLAGS --product swiftlint RUN mv `swift build $SWIFT_FLAGS --show-bin-path`/swiftlint /usr/bin +RUN strip /usr/bin/swiftlint # Runtime image FROM ${RUNTIME_IMAGE} -LABEL org.opencontainers.image.source https://github.com/realm/SwiftLint +LABEL org.opencontainers.image.source=https://github.com/realm/SwiftLint RUN apt-get update && apt-get install -y \ libcurl4 \ libxml2 \ @@ -30,20 +31,31 @@ COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftBasicFormat.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftCompilerPluginMessageHandling.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftDiagnostics.so /usr/lib +COPY --from=builder /usr/lib/swift/host/libSwiftIDEUtils.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftOperators.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftParser.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftParserDiagnostics.so /usr/lib +COPY --from=builder /usr/lib/swift/host/libSwiftRefactor.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftSyntax.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftSyntaxBuilder.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftSyntaxMacroExpansion.so /usr/lib COPY --from=builder /usr/lib/swift/host/libSwiftSyntaxMacros.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/lib_FoundationICU.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libFoundation.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libFoundationInternationalization.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libFoundationEssentials.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libFoundationNetworking.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libFoundationXML.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libswift_Concurrency.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libswift_RegexParser.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libswift_StringProcessing.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libswiftCore.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libswiftDispatch.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libswiftGlibc.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libswiftSynchronization.so /usr/lib +COPY --from=builder /usr/lib/swift/linux/libswiftSwiftOnoneSupport.so /usr/lib COPY --from=builder /usr/bin/swiftlint /usr/bin RUN swiftlint version diff --git a/Gemfile b/Gemfile index d613241b20..e19d589da9 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source 'https://rubygems.org' gem 'cocoapods' gem 'danger' -gem 'jazzy', '~> 0.14.4' +gem 'jazzy', '~> 0.15.1' diff --git a/Gemfile.lock b/Gemfile.lock index 51e0719663..c67e893bb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,12 +92,12 @@ GEM httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) - jazzy (0.14.4) + jazzy (0.15.1) cocoapods (~> 1.5) mustache (~> 1.1) open4 (~> 1.3) redcarpet (~> 3.4) - rexml (~> 3.2) + rexml (>= 3.2.7, < 4.0) rouge (>= 2.0.6, < 5.0) sassc (~> 2.1) sqlite3 (~> 1.3) @@ -122,8 +122,8 @@ GEM public_suffix (4.0.7) rchardet (1.8.0) redcarpet (3.6.0) - rexml (3.2.5) - rouge (4.2.0) + rexml (3.3.9) + rouge (4.3.0) ruby-macho (2.5.1) ruby2_keywords (0.0.5) sassc (2.4.0) @@ -131,8 +131,8 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - sqlite3 (1.7.2-arm64-darwin) - sqlite3 (1.7.2-x86_64-linux) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86_64-linux) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) typhoeus (1.4.0) @@ -142,24 +142,25 @@ GEM unicode-display_width (2.4.2) xcinvoke (0.3.0) liferaft (~> 0.0.6) - xcodeproj (1.22.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) PLATFORMS arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES cocoapods danger - jazzy (~> 0.14.4) + jazzy (~> 0.15.1) BUNDLED WITH 2.4.12 diff --git a/MODULE.bazel b/MODULE.bazel index d38d2cd08e..ff17e604d9 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,19 +1,19 @@ module( name = "swiftlint", - version = "0.54.0", + version = "0.57.1", compatibility_level = 1, repo_name = "SwiftLint", ) -bazel_dep(name = "apple_support", version = "1.11.1", repo_name = "build_bazel_apple_support") -bazel_dep(name = "bazel_skylib", version = "1.5.0") -bazel_dep(name = "platforms", version = "0.0.8") -bazel_dep(name = "rules_apple", version = "3.1.1", repo_name = "build_bazel_rules_apple") -bazel_dep(name = "rules_swift", version = "1.16.0", repo_name = "build_bazel_rules_swift") -bazel_dep(name = "sourcekitten", version = "0.34.1", repo_name = "com_github_jpsim_sourcekitten") -bazel_dep(name = "swift-syntax", version = "509.1.1", repo_name = "SwiftSyntax") -bazel_dep(name = "swift_argument_parser", version = "1.2.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") -bazel_dep(name = "yams", version = "5.0.6", repo_name = "sourcekitten_com_github_jpsim_yams") +bazel_dep(name = "apple_support", version = "1.16.0", repo_name = "build_bazel_apple_support") +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "platforms", version = "0.0.10") +bazel_dep(name = "rules_apple", version = "3.8.0", repo_name = "build_bazel_rules_apple") +bazel_dep(name = "rules_swift", version = "2.1.1", repo_name = "build_bazel_rules_swift") +bazel_dep(name = "sourcekitten", version = "0.36.0", repo_name = "com_github_jpsim_sourcekitten") +bazel_dep(name = "swift-syntax", version = "600.0.0", repo_name = "SwiftSyntax") +bazel_dep(name = "swift_argument_parser", version = "1.3.1.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") +bazel_dep(name = "yams", version = "5.1.3", repo_name = "sourcekitten_com_github_jpsim_yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") use_repo( @@ -31,4 +31,4 @@ use_repo(apple_cc_configure, "local_config_apple_cc") # Dev Dependencies -bazel_dep(name = "rules_xcodeproj", version = "1.13.0", dev_dependency = True) +bazel_dep(name = "rules_xcodeproj", version = "2.6.1", dev_dependency = True) diff --git a/Makefile b/Makefile index 822b53df1a..b624d78cac 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ SWIFT_BUILD_FLAGS=--configuration release -Xlinker -dead_strip SWIFTLINT_EXECUTABLE_PARENT=.build/universal SWIFTLINT_EXECUTABLE=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint +SWIFTLINT_EXECUTABLE_LINUX_PARENT=.build/linux +SWIFTLINT_EXECUTABLE_LINUX_AMD64=$(SWIFTLINT_EXECUTABLE_LINUX_PARENT)/swiftlint_linux_amd64 ARTIFACT_BUNDLE_PATH=$(TEMPORARY_FOLDER)/SwiftLintBinary.artifactbundle @@ -26,7 +28,7 @@ OUTPUT_PACKAGE=SwiftLint.pkg VERSION_STRING=$(shell ./tools/get-version) -.PHONY: all clean build install package test uninstall docs +.PHONY: all clean build build_linux install package test uninstall docs all: build @@ -63,7 +65,8 @@ analyze_autocorrect: write_xcodebuild_log clean: rm -f "$(OUTPUT_PACKAGE)" rm -rf "$(TEMPORARY_FOLDER)" - rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256" + rm -rf rule_docs/ docs/ + rm -f ./*.{zip,pkg} bazel.tar.gz bazel.tar.gz.sha256 swift package clean clean_xcode: @@ -77,6 +80,11 @@ build: chmod +w "$(SWIFTLINT_EXECUTABLE)" strip -rSTX "$(SWIFTLINT_EXECUTABLE)" +build_linux: + mkdir -p "$(SWIFTLINT_EXECUTABLE_LINUX_PARENT)" + docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(SWIFTLINT_EXECUTABLE_LINUX_AMD64)" + chmod +x "$(SWIFTLINT_EXECUTABLE_LINUX_AMD64)" + build_with_disable_sandbox: swift build --disable-sandbox $(SWIFT_BUILD_FLAGS) @@ -92,6 +100,10 @@ installables: build install -d "$(TEMPORARY_FOLDER)$(BINARIES_FOLDER)" install "$(SWIFTLINT_EXECUTABLE)" "$(TEMPORARY_FOLDER)$(BINARIES_FOLDER)" +installables_linux: build_linux + install -d "$(TEMPORARY_FOLDER)$(BINARIES_FOLDER)" + install "$(SWIFTLINT_EXECUTABLE_LINUX_AMD64)" "$(TEMPORARY_FOLDER)$(BINARIES_FOLDER)" + prefix_install: build_with_disable_sandbox install -d "$(PREFIX)/bin/" install "$(SWIFTLINT_EXECUTABLE)" "$(PREFIX)/bin/" @@ -101,24 +113,24 @@ portable_zip: installables cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)" (cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip" -spm_artifactbundle_macos: installables +spm_artifactbundle: installables installables_linux mkdir -p "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin" - sed 's/__VERSION__/$(VERSION_STRING)/g' tools/info-macos.json.template > "$(ARTIFACT_BUNDLE_PATH)/info.json" - cp -f "$(SWIFTLINT_EXECUTABLE)" "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin" + mkdir -p "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-linux-gnu/bin" + sed 's/__VERSION__/$(VERSION_STRING)/g' tools/info.json.template > "$(ARTIFACT_BUNDLE_PATH)/info.json" + cp -f "$(SWIFTLINT_EXECUTABLE)" "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin/swiftlint" + cp -f "$(SWIFTLINT_EXECUTABLE_LINUX_AMD64)" "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-linux-gnu/bin/swiftlint" cp -f "$(LICENSE_PATH)" "$(ARTIFACT_BUNDLE_PATH)" - (cd "$(TEMPORARY_FOLDER)"; zip -yr - "SwiftLintBinary.artifactbundle") > "./SwiftLintBinary-macos.artifactbundle.zip" + (cd "$(TEMPORARY_FOLDER)"; zip -yr - "SwiftLintBinary.artifactbundle") > "./SwiftLintBinary.artifactbundle.zip" -zip_linux: docker_image +zip_linux: docker_image build_linux $(eval TMP_FOLDER := $(shell mktemp -d)) - docker run swiftlint cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint" - chmod +x "$(TMP_FOLDER)/swiftlint" + cp -f $(SWIFTLINT_EXECUTABLE_LINUX_AMD64) "$(TMP_FOLDER)/swiftlint" cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)" (cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip" -zip_linux_release: +zip_linux_release: build_linux $(eval TMP_FOLDER := $(shell mktemp -d)) - docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint" - chmod +x "$(TMP_FOLDER)/swiftlint" + cp -f "$(SWIFTLINT_EXECUTABLE_LINUX_AMD64)" "$(TMP_FOLDER)/swiftlint" cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)" (cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip" gh release upload "$(VERSION_STRING)" swiftlint_linux.zip @@ -153,8 +165,10 @@ docker_htop: display_compilation_time: $(BUILD_TOOL) $(XCODEFLAGS) OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" clean build-for-testing | grep -E ^[1-9]{1}[0-9]*.[0-9]+ms | sort -n -publish: +formula_bump: brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint + +pod_publish: bundle install bundle exec pod trunk push SwiftLint.podspec @@ -179,14 +193,14 @@ endif make package make bazel_release make portable_zip - make spm_artifactbundle_macos + make spm_artifactbundle ./tools/update-artifact-bundle.sh "$(NEW_VERSION)" - git commit -a -m "release $(NEW_VERSION)" + git commit -a -m "Release $(NEW_VERSION)" git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)" git push origin HEAD git push origin $(NEW_VERSION) ./tools/create-github-release.sh "$(NEW_VERSION)" - make publish + make formula_bump ./tools/add-new-changelog-section.sh git commit -a -m "Add new changelog section" git push origin HEAD diff --git a/Package.resolved b/Package.resolved index 8c2cf35951..ee63d9748b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "db51c407d3be4a051484a141bf0bff36c43d3b1e", - "version" : "1.8.0" + "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version" : "1.8.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" + "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", + "version" : "0.35.0" } }, { @@ -32,17 +32,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { - "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", - "version" : "5.0.6" + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" } } ], diff --git a/Package.swift b/Package.swift index 9f18539861..85cdce9d31 100644 --- a/Package.swift +++ b/Package.swift @@ -3,10 +3,16 @@ import CompilerPluginSupport import PackageDescription let swiftFeatures: [SwiftSetting] = [ - .enableUpcomingFeature("ExistentialAny") + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ImportObjcForwardDeclarations"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("ImplicitOpenExistentials"), ] +let strictConcurrency = [SwiftSetting.enableExperimentalFeature("StrictConcurrency")] let swiftLintPluginDependencies: [Target.Dependency] + #if os(macOS) swiftLintPluginDependencies = [.target(name: "SwiftLintBinary")] #else @@ -19,23 +25,36 @@ let package = Package( products: [ .executable(name: "swiftlint", targets: ["swiftlint"]), .library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]), - .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"]) + .plugin(name: "SwiftLintBuildToolPlugin", targets: ["SwiftLintBuildToolPlugin"]), + .plugin(name: "SwiftLintCommandPlugin", targets: ["SwiftLintCommandPlugin"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")), - .package(url: "https://github.com/apple/swift-syntax.git", exact: "509.1.1"), - .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.1"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "600.0.0"), + .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.35.0")), .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6"), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.8.0")) + .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.8.0")), ], targets: [ .plugin( - name: "SwiftLintPlugin", + name: "SwiftLintBuildToolPlugin", capability: .buildTool(), dependencies: swiftLintPluginDependencies ), + .plugin( + name: "SwiftLintCommandPlugin", + capability: .command( + intent: .custom(verb: "swiftlint", description: "SwiftLint Command Plugin"), + permissions: [ + .writeToPackageDirectory( + reason: "When this command is run with the `--fix` option it may modify source files." + ), + ] + ), + dependencies: swiftLintPluginDependencies + ), .executableTarget( name: "swiftlint", dependencies: [ @@ -44,12 +63,12 @@ let package = Package( "SwiftLintFramework", "SwiftyTextTable", ], - swiftSettings: swiftFeatures + swiftSettings: swiftFeatures + strictConcurrency ), .testTarget( name: "CLITests", dependencies: [ - "swiftlint" + "SwiftLintFramework", ], swiftSettings: swiftFeatures ), @@ -66,18 +85,19 @@ let package = Package( .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "SwiftyTextTable", package: "SwiftyTextTable"), .product(name: "Yams", package: "Yams"), - "SwiftLintCoreMacros" + "SwiftLintCoreMacros", ], swiftSettings: swiftFeatures ), .target( name: "SwiftLintBuiltInRules", dependencies: ["SwiftLintCore"], - swiftSettings: swiftFeatures + swiftSettings: swiftFeatures + strictConcurrency ), .target( name: "SwiftLintExtraRules", - dependencies: ["SwiftLintCore"] + dependencies: ["SwiftLintCore"], + swiftSettings: swiftFeatures + strictConcurrency ), .target( name: "SwiftLintFramework", @@ -85,11 +105,9 @@ let package = Package( "SwiftLintBuiltInRules", "SwiftLintCore", "SwiftLintExtraRules", - // Workaround for https://github.com/apple/swift-package-manager/issues/6940: - .product(name: "ArgumentParser", package: "swift-argument-parser"), - "CollectionConcurrencyKit" + "CollectionConcurrencyKit", ], - swiftSettings: swiftFeatures + swiftSettings: swiftFeatures + strictConcurrency ), .target(name: "DyldWarningWorkaround"), .target( @@ -97,14 +115,15 @@ let package = Package( dependencies: [ "SwiftLintFramework" ], - path: "Tests/SwiftLintTestHelpers" + path: "Tests/SwiftLintTestHelpers", + swiftSettings: swiftFeatures ), .testTarget( name: "SwiftLintFrameworkTests", dependencies: [ "SwiftLintFramework", "SwiftLintTestHelpers", - "SwiftLintCoreMacros" + "SwiftLintCoreMacros", ], exclude: [ "Resources", @@ -115,7 +134,7 @@ let package = Package( name: "GeneratedTests", dependencies: [ "SwiftLintFramework", - "SwiftLintTestHelpers" + "SwiftLintTestHelpers", ], swiftSettings: swiftFeatures ), @@ -123,7 +142,10 @@ let package = Package( name: "IntegrationTests", dependencies: [ "SwiftLintFramework", - "SwiftLintTestHelpers" + "SwiftLintTestHelpers", + ], + exclude: [ + "default_rule_configurations.yml" ], swiftSettings: swiftFeatures ), @@ -131,22 +153,18 @@ let package = Package( name: "ExtraRulesTests", dependencies: [ "SwiftLintFramework", - "SwiftLintTestHelpers" - ] - ), - .binaryTarget( - name: "SwiftLintBinary", - url: "https://github.com/realm/SwiftLint/releases/download/0.54.0/SwiftLintBinary-macos.artifactbundle.zip", - checksum: "963121d6babf2bf5fd66a21ac9297e86d855cbc9d28322790646b88dceca00f1" + "SwiftLintTestHelpers", + ], + swiftSettings: swiftFeatures ), .macro( name: "SwiftLintCoreMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), ], path: "Source/SwiftLintCoreMacros", - swiftSettings: swiftFeatures + swiftSettings: swiftFeatures + strictConcurrency ), .testTarget( name: "MacroTests", @@ -158,3 +176,14 @@ let package = Package( ), ] ) + +#if os(macOS) +// TODO: in the next release the artifactbundle is not suffixed with "-macos" +package.targets.append( + .binaryTarget( + name: "SwiftLintBinary", + url: "https://github.com/realm/SwiftLint/releases/download/0.57.1/SwiftLintBinary-macos.artifactbundle.zip", + checksum: "c88bf3e5bc1326d8ca66bc3f9eae786f2094c5172cd70b26b5f07686bb883899" + ) +) +#endif diff --git a/Plugins/SwiftLintPlugin/Path+Helpers.swift b/Plugins/SwiftLintBuildToolPlugin/Path+Helpers.swift similarity index 89% rename from Plugins/SwiftLintPlugin/Path+Helpers.swift rename to Plugins/SwiftLintBuildToolPlugin/Path+Helpers.swift index 239b190592..8de05a75fd 100644 --- a/Plugins/SwiftLintPlugin/Path+Helpers.swift +++ b/Plugins/SwiftLintBuildToolPlugin/Path+Helpers.swift @@ -16,7 +16,7 @@ extension Path { func resolveWorkingDirectory(in directory: Path) throws -> Path { guard "\(self)".hasPrefix("\(directory)") else { - throw SwiftLintPluginError.pathNotInDirectory(path: self, directory: directory) + throw SwiftLintBuildToolPluginError.pathNotInDirectory(path: self, directory: directory) } let path: Path? = sequence(first: self) { path in diff --git a/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift similarity index 93% rename from Plugins/SwiftLintPlugin/SwiftLintPlugin.swift rename to Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift index 0969731cd9..2005c4dfda 100644 --- a/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift +++ b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift @@ -2,7 +2,7 @@ import Foundation import PackagePlugin @main -struct SwiftLintPlugin: BuildToolPlugin { +struct SwiftLintBuildToolPlugin: BuildToolPlugin { func createBuildCommands( context: PluginContext, target: Target @@ -57,7 +57,7 @@ struct SwiftLintPlugin: BuildToolPlugin { // We always pass all of the Swift source files in the target to the tool, // so we need to ensure that any exclusion rules in the configuration are // respected. - "--force-exclude" + "--force-exclude", ] // Determine whether we need to enable cache or not (for Xcode Cloud we don't) let cacheArguments: [String] @@ -76,7 +76,7 @@ struct SwiftLintPlugin: BuildToolPlugin { executable: executable.path, arguments: arguments + cacheArguments + swiftFiles.map(\.string), environment: environment, - outputFilesDirectory: outputPath) + outputFilesDirectory: outputPath), ] } } @@ -86,7 +86,7 @@ struct SwiftLintPlugin: BuildToolPlugin { import XcodeProjectPlugin // swiftlint:disable:next no_grouping_extension -extension SwiftLintPlugin: XcodeBuildToolPlugin { +extension SwiftLintBuildToolPlugin: XcodeBuildToolPlugin { func createBuildCommands( context: XcodePluginContext, target: XcodeTarget @@ -125,7 +125,7 @@ extension SwiftLintPlugin: XcodeBuildToolPlugin { let swiftFilesNotInProjectDirectory: [Path] = swiftFiles.filter { !$0.isDescendant(of: projectDirectory) } guard swiftFilesNotInProjectDirectory.isEmpty else { - throw SwiftLintPluginError.swiftFilesNotInProjectDirectory(projectDirectory) + throw SwiftLintBuildToolPluginError.swiftFilesNotInProjectDirectory(projectDirectory) } let directories: [Path] = try swiftFiles.map { try $0.resolveWorkingDirectory(in: projectDirectory) } @@ -133,7 +133,7 @@ extension SwiftLintPlugin: XcodeBuildToolPlugin { let swiftFilesNotInWorkingDirectory: [Path] = swiftFiles.filter { !$0.isDescendant(of: workingDirectory) } guard swiftFilesNotInWorkingDirectory.isEmpty else { - throw SwiftLintPluginError.swiftFilesNotInWorkingDirectory(workingDirectory) + throw SwiftLintBuildToolPluginError.swiftFilesNotInWorkingDirectory(workingDirectory) } return ["BUILD_WORKSPACE_DIRECTORY": "\(workingDirectory)"] diff --git a/Plugins/SwiftLintPlugin/SwiftLintPluginError.swift b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPluginError.swift similarity index 90% rename from Plugins/SwiftLintPlugin/SwiftLintPluginError.swift rename to Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPluginError.swift index 3136f368a7..16f839502f 100644 --- a/Plugins/SwiftLintPlugin/SwiftLintPluginError.swift +++ b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPluginError.swift @@ -1,6 +1,6 @@ import PackagePlugin -enum SwiftLintPluginError: Error, CustomStringConvertible { +enum SwiftLintBuildToolPluginError: Error, CustomStringConvertible { case pathNotInDirectory(path: Path, directory: Path) case swiftFilesNotInProjectDirectory(Path) case swiftFilesNotInWorkingDirectory(Path) diff --git a/Plugins/SwiftLintCommandPlugin/CommandContext.swift b/Plugins/SwiftLintCommandPlugin/CommandContext.swift new file mode 100644 index 0000000000..bbe39f4640 --- /dev/null +++ b/Plugins/SwiftLintCommandPlugin/CommandContext.swift @@ -0,0 +1,92 @@ +import PackagePlugin + +protocol CommandContext { + var tool: String { get throws } + + var cacheDirectory: String { get } + + var workingDirectory: String { get } + + var unitName: String { get } + + var subUnitName: String { get } + + func targets(named names: [String]) throws -> [(paths: [String], name: String)] +} + +extension PluginContext: CommandContext { + var tool: String { + get throws { + try tool(named: "swiftlint").path.string + } + } + + var cacheDirectory: String { + pluginWorkDirectory.string + } + + var workingDirectory: String { + package.directory.string + } + + var unitName: String { + "package" + } + + var subUnitName: String { + "module" + } + + func targets(named names: [String]) throws -> [(paths: [String], name: String)] { + let targets = names.isEmpty + ? package.targets + : try package.targets(named: names) + return targets.compactMap { target in + guard let target = target.sourceModule else { + Diagnostics.warning("Target '\(target.name)' is not a source module; skipping it") + return nil + } + // Packages have a 1-to-1 mapping between targets and directories. + return (paths: [target.directory.string], name: target.name) + } + } +} + +#if canImport(XcodeProjectPlugin) + +import XcodeProjectPlugin + +extension XcodePluginContext: CommandContext { + var tool: String { + get throws { + try tool(named: "swiftlint").path.string + } + } + + var cacheDirectory: String { + pluginWorkDirectory.string + } + + var workingDirectory: String { + xcodeProject.directory.string + } + + var unitName: String { + "project" + } + + var subUnitName: String { + "target" + } + + func targets(named names: [String]) throws -> [(paths: [String], name: String)] { + if names.isEmpty { + return [(paths: [xcodeProject.directory.string], name: xcodeProject.displayName)] + } + return xcodeProject.targets + .filter { names.contains($0.displayName) } + .map { (paths: $0.inputFiles.map(\.path.string), name: $0.displayName) } + } +} + +#endif diff --git a/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift b/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift new file mode 100644 index 0000000000..21754b569d --- /dev/null +++ b/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift @@ -0,0 +1,90 @@ +import Foundation +import PackagePlugin + +private let commandsNotExpectingPaths: Set = [ + "docs", + "generate-docs", + "baseline", + "reporters", + "rules", + "version", +] + +private let commandsWithoutCachPathOption: Set = commandsNotExpectingPaths.union([ + "analyze", +]) + +@main +struct SwiftLintCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + try lintFiles(context: context, arguments: arguments) + } +} + +#if canImport(XcodeProjectPlugin) + +import XcodeProjectPlugin + +extension SwiftLintCommandPlugin: XcodeCommandPlugin { + func performCommand(context: XcodePluginContext, arguments: [String]) throws { + try lintFiles(context: context, arguments: arguments) + } +} + +#endif + +extension SwiftLintCommandPlugin { + private func lintFiles(context: some CommandContext, arguments: [String]) throws { + guard !arguments.contains("--cache-path") else { + Diagnostics.error("Caching is managed by the plugin and so setting `--cache-path` is not allowed") + return + } + var argExtractor = ArgumentExtractor(arguments) + let targetNames = argExtractor.extractOption(named: "target") + let remainingArguments = argExtractor.remainingArguments + guard !targetNames.isEmpty, commandsNotExpectingPaths.isDisjoint(with: remainingArguments) else { + try lintFiles(with: context, arguments: remainingArguments) + return + } + for target in try context.targets(named: targetNames) { + try lintFiles(in: target.paths, for: target.name, with: context, arguments: remainingArguments) + } + } + + private func lintFiles(in paths: [String] = ["."], + for targetName: String? = nil, + with context: some CommandContext, + arguments: [String]) throws { + let process = Process() + process.currentDirectoryURL = URL(fileURLWithPath: context.workingDirectory) + process.executableURL = URL(fileURLWithPath: try context.tool) + process.arguments = arguments + if commandsWithoutCachPathOption.isDisjoint(with: arguments) { + process.arguments! += ["--cache-path", context.cacheDirectory] + } + if commandsNotExpectingPaths.isDisjoint(with: arguments) { + process.arguments! += paths + } + + try process.run() + process.waitUntilExit() + + let module = targetName.map { "\(context.subUnitName) '\($0)'" } ?? context.unitName + switch process.terminationReason { + case .exit: + Diagnostics.remark("Finished running in \(module)") + case .uncaughtSignal: + Diagnostics.error("Got uncaught signal while running in \(module)") + @unknown default: + Diagnostics.error("Stopped running in \(module) due to unexpected termination reason") + } + + if process.terminationStatus != EXIT_SUCCESS { + Diagnostics.error(""" + Command found error violations or unsuccessfully stopped running with \ + exit code \(process.terminationStatus) in \(module) + """ + ) + } + } +} diff --git a/README.md b/README.md index b011d96131..39b7294f7e 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,119 @@ # SwiftLint -A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide). +A tool to enforce Swift style and conventions, loosely based on the now +archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). +SwiftLint enforces the style guide rules that are generally accepted by the +Swift community. These rules are well described in popular style guides like +[Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide). SwiftLint hooks into [Clang](http://clang.llvm.org) and [SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation of your source files for more accurate results. -[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=main)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main) -[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=main)](https://codecov.io/github/realm/SwiftLint?branch=main) +[![Azure Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=main)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main) +[![Buildkite Build Status](https://badge.buildkite.com/e2a5bc32c347e76e2793e4c5764a5f42bcd42bbe32f79c3a53.svg?branch=main)](https://buildkite.com/swiftlint/swiftlint) ![](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/screenshot.png) -This project adheres to the [Contributor Covenant Code of Conduct](https://realm.io/conduct). +This project adheres to the +[Contributor Covenant Code of Conduct](https://realm.io/conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to [info@realm.io](mailto:info@realm.io). -> Language Switch: [中文](https://github.com/realm/SwiftLint/blob/main/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/main/README_KR.md). +> Switch Language: +> [中文](https://github.com/realm/SwiftLint/blob/main/README_CN.md) +> [한국어](https://github.com/realm/SwiftLint/blob/main/README_KR.md) + +## Video Introduction + +To get a high-level overview of SwiftLint, we encourage you to watch this +presentation recorded January 9th, 2017 by JP Simard (transcript provided): + +[![Presentation](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/presentation.svg)](https://youtu.be/9Z1nTMTejqU) ## Installation -### Using [Homebrew](http://brew.sh/): +### [Swift Package Manager](https://github.com/apple/swift-package-manager) -``` -brew install swiftlint +SwiftLint can be used as a [command plugin](#swift-package-command-plugin) +or a [build tool plugin](#build-tool-plugins). + +Add + +```swift +.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "") ``` -### Using [CocoaPods](https://cocoapods.org): +to your `Package.swift` file to consume the latest release of SwiftLint +automatically or pin the dependency to a specific version: -Simply add the following line to your Podfile: +```swift +.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "") +``` -```ruby -pod 'SwiftLint' +Therein, replace `` with the desired minimum or exact version. + +> [!NOTE] +> Consuming the plugins directly from the SwiftLint repository comes +> with several drawbacks. To avoid them and reduce the overhead imposed, it's +> highly recommended to consume the plugins from the dedicated +> [SwiftLintPlugins repository](https://github.com/SimplyDanny/SwiftLintPlugins), +> even though plugins from the SwiftLint repository are also absolutely +> functional. If the plugins from SwiftLint are preferred, just use the URL +> `https://github.com/realm/SwiftLint` in the package declarations above. +> +> However, [SwiftLintPlugins](https://github.com/SimplyDanny/SwiftLintPlugins) +> facilitates plugin adoption massively. It lists some of the reasons that +> drive the plugins as provided by SwiftLint itself very troublesome. Since +> the plugin code and the releases are kept in sync, there is no difference +> in functionality between the two, but you spare yourself a lot of time and +> trouble using the dedicated plugins repository. +> +> This document assumes you're relying on SwiftLintPlugins. + +### [Xcode Package Dependency](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) + +Use the following link to add SwiftLint as a Package Dependency to an Xcode +project: + +```bash +https://github.com/SimplyDanny/SwiftLintPlugins ``` -This will download the SwiftLint binaries and dependencies in `Pods/` during your next -`pod install` execution and will allow you to invoke it via `${PODS_ROOT}/SwiftLint/swiftlint` -in your Script Build Phases. +### [Homebrew](http://brew.sh) -This is the recommended way to install a specific version of SwiftLint since it supports -installing a pinned version rather than simply the latest (which is the case with Homebrew). +```bash +brew install swiftlint +``` -Note that this will add the SwiftLint binaries, its dependencies' binaries, and the Swift binary -library distribution to the `Pods/` directory, so checking in this directory to SCM such as -git is discouraged. +### [CocoaPods](https://cocoapods.org) -### Using [Mint](https://github.com/yonaskolb/mint): +Add the following to your `Podfile`: -``` -$ mint install realm/SwiftLint +```ruby +pod 'SwiftLint' ``` -### Using a pre-built package: +This will download the SwiftLint binaries and dependencies in `Pods/` during +your next `pod install` execution and will allow you to invoke it via +`${PODS_ROOT}/SwiftLint/swiftlint` in your Script Build Phases. -You can also install SwiftLint by downloading `SwiftLint.pkg` from the -[latest GitHub release](https://github.com/realm/SwiftLint/releases/latest) and -running it. +Installing via Cocoapods also enables pinning to a specific version of +SwiftLint rather than simply the latest (which is the case with +[Homebrew](#homebrew)). + +Note that this will add the SwiftLint binaries, its dependencies' binaries, and +the Swift binary library distribution to the `Pods/` directory, so checking in +this directory to SCM such as Git is discouraged. -### Installing from source: +### [Mint](https://github.com/yonaskolb/mint) -You can also build and install from source by cloning this project and running -`make install` (Xcode 15.0 or later). +```bash +mint install realm/SwiftLint +``` -### Using Bazel +### [Bazel](https://bazel.build) Put this in your `MODULE.bazel`: @@ -129,154 +180,254 @@ Then you can run SwiftLint in the current directory with this command: bazel run -c opt @SwiftLint//:swiftlint ``` -## Usage +### Pre-Built Package -### Presentation +Download `SwiftLint.pkg` from the +[latest GitHub release](https://github.com/realm/SwiftLint/releases/latest) and +run it. -To get a high-level overview of recommended ways to integrate SwiftLint into your project, -we encourage you to watch this presentation or read the transcript: +### From Source -[![Presentation](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/presentation.svg)](https://youtu.be/9Z1nTMTejqU) +Make sure the build tool [Bazel](https://bazel.build) and a +recent [Swift toolchain](https://www.swift.org/download/) are +installed and all tools are discoverable in your `PATH`. -### Xcode +To build SwiftLint, clone this repository and run `make install`. -Integrate SwiftLint into your Xcode project to get warnings and errors displayed -in the issue navigator. +## Setup -To do this select the project in the file navigator, then select the primary app -target, and go to Build Phases. Click the + and select "New Run Script Phase". -Insert the following as the script: +> [!IMPORTANT] +> While it may seem intuitive to run SwiftLint before compiling Swift source +> files to exit a build early when there are lint violations, it is important +> to understand that SwiftLint is designed to analyze valid source code that +> is compilable. Non-compiling code can very easily lead to unexpected and +> confusing results, especially when executing with `--fix`/`--autocorrect` +> command line arguments. -![](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/runscript.png) +### Build Tool Plugins -Xcode 15 made a significant change by setting the default value of the `ENABLE_USER_SCRIPT_SANDBOXING` Build Setting from `NO` to `YES`. -As a result, SwiftLint encounters an error related to missing file permissions, -which typically manifests as follows: `error: Sandbox: swiftlint(19427) deny(1) file-read-data.` +SwiftLint can be used as a build tool plugin for both +[Swift Package projects](#swift-package-projects) +and [Xcode projects](#xcode-projects). -To resolve this issue, it is necessary to manually set the `ENABLE_USER_SCRIPT_SANDBOXING` setting to `NO` for the specific target that SwiftLint is being configured for. +The build tool plugin determines the SwiftLint working directory by locating +the topmost config file within the package/project directory. If a config file +is not found therein, the package/project directory is used as the working +directory. -If you installed SwiftLint via Homebrew on Apple Silicon, you might experience this warning: +The plugin throws an error when it is unable to resolve the SwiftLint working +directory. For example, this will occur in Xcode projects where the target's +Swift files are not located within the project directory. -> warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint +To maximize compatibility with the plugin, avoid project structures that require +the use of the `--config` option. -That is because Homebrew on Apple Silicon installs the binaries into the `/opt/homebrew/bin` -folder by default. To instruct Xcode where to find SwiftLint, you can either add -`/opt/homebrew/bin` to the `PATH` environment variable in your build phase +### Swift Package Projects -```bash -if [[ "$(uname -m)" == arm64 ]]; then - export PATH="/opt/homebrew/bin:$PATH" -fi +> [!NOTE] +> Requires installing via [Swift Package Manager](#swift-package-manager). -if which swiftlint > /dev/null; then - swiftlint -else - echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" -fi -``` +Build tool plugins run when building each target. When a project has multiple +targets, the plugin must be added to the desired targets individually. -or you can create a symbolic link in `/usr/local/bin` pointing to the actual binary: +To do this, add the plugin to the target(s) to be linted as follows: -```bash -ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint +```swift +.target( + ... + plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")] +), ``` -You might want to move your SwiftLint phase directly before the 'Compile Sources' -step to detect errors quickly before compiling. However, SwiftLint is designed -to run on valid Swift code that cleanly completes the compiler's parsing stage. -So running SwiftLint before 'Compile Sources' might yield some incorrect -results. +### Swift Package Command Plugin -If you wish to fix violations as well, your script could run -`swiftlint --fix && swiftlint` instead of just `swiftlint`. This will mean -that all correctable violations are fixed while ensuring warnings show up in -your project for remaining violations. +> [!NOTE] +> Requires installing via [Swift Package Manager](#swift-package-manager). -If you've installed SwiftLint via CocoaPods the script should look like this: +The command plugin enables running SwiftLint from the command line as follows: -```bash -"${PODS_ROOT}/SwiftLint/swiftlint" +```shell +swift package plugin swiftlint ``` -### Plug-in Support +### Xcode Projects -SwiftLint can be used as a build tool plugin for both Swift Package projects -and Xcode projects. +> [!NOTE] +> Requires installing via [Xcode Package Dependency](#xcode-package-dependency). -The build tool plugin determines the SwiftLint working directory by locating -the topmost config file within the package/project directory. If a config file -is not found therein, the package/project directory is used as the working -directory. +Build tool plugins run as a build phase of each target. When a project has +multiple targets, the plugin must be added to the desired targets individually. -The plugin throws an error when it is unable to resolve the SwiftLint working -directory. For example, this will occur in Xcode projects where the target's -Swift files are not located within the project directory. +To do this, add the `SwiftLintBuildToolPlugin` to the `Run Build Tool Plug-ins` +phase of the `Build Phases` for the target(s) to be linted. -To maximize compatibility with the plugin, avoid project structures that require -the use of the `--config` option. +> [!TIP] +> When using the plugin for the first time, be sure to trust and enable +> it when prompted. If a macros build warning exists, select it to trust +> and enable the macros as well. + +For unattended use (e.g. on CI), package plugin and macro +validations can be disabled with either of the following: + +* Using `xcodebuild` options: + + ```bash + -skipPackagePluginValidation + -skipMacroValidation + ``` + +* Setting Xcode defaults: + + ```bash + defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + ``` -#### Unexpected Project Structures +> [!IMPORTANT] +> The unattended use options bypass Xcode's validation dialogs +> and implicitly trust all plugins and macros, which has security implications. -Project structures where SwiftLint's configuration file is located -outside of the package/project directory are not directly supported -by the build tool plugin. This is because it isn't possible to pass -arguments to build tool plugins (e.g., passing the config file path). +#### Unexpected Xcode Project Structures -If your project structure doesn't work directly with the build tool +Project structures where SwiftLint's configuration file is located +outside of the package/project directory are not directly supported +by the build tool plugin. This is because it isn't possible to pass +arguments to build tool plugins (e.g., passing the config file path). + +If your project structure doesn't work directly with the build tool plugin, please consider one of the following options: -- To use a config file located outside the package/project directory, a config - file may be added to that directory specifying a parent config path to the +* To use a config file located outside the package/project directory, a config + file may be added to that directory specifying a parent config path to the other config file, e.g., `parent_config: path/to/.swiftlint.yml`. -- You can also consider the use of a Run Script Build Phase in place of the - build tool plugin. +* You can also consider the use of a + [Run Script Build Phase](#xcode-run-script-build-phase) in place of the build + tool plugin. -#### Xcode +### Xcode Run Script Build Phase -You can integrate SwiftLint as an Xcode Build Tool Plug-in if you're working -with a project in Xcode. +> [!NOTE] +> Based upon the installation method used, the shell command syntax in the +> Run Script Build Phase may be different or additional configuration could +> be required. Refer to the [installation](#installation) instructions for +> more information. -Add SwiftLint as a package dependency to your project without linking any of the -products. +If the build tool plugin does not work for your project setup or when +additional custom setup is required, SwiftLint can be added as a Run Script +Build Phase. This is useful when a project setup relies on the `--config` +SwiftLint option; or to lint all targets together in a single `swiftlint` +invocation. File inclusions and exclusions can be configured in the +[`.swiftlint.yml` configuration](#configuration). -Select the target you want to add linting to and open the `Build Phases` inspector. -Open `Run Build Tool Plug-ins` and select the `+` button. -Select `SwiftLintPlugin` from the list and add it to the project. +To do this, add a custom script to a `Run Script` phase of the `Build Phases` +of the primary app target, after the `Compile Sources` phase. Use the +following script implementation: -![](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/select-swiftlint-plugin.png) +```bash +if command -v swiftlint >/dev/null 2>&1 +then + swiftlint +else + echo "warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions." +fi +``` -For unattended use (e.g. on CI), you can disable the package and macro validation dialog by +If you're using the SwiftLintPlugin in a Swift package, +you may refer to the `swiftlint` executable in the +following way: -* individually passing `-skipPackagePluginValidation` and `-skipMacroValidation` to `xcodebuild` or -* globally setting `defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES` and `defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES` - for that user. +```bash +SWIFT_PACKAGE_DIR="${BUILD_DIR%Build/*}SourcePackages/artifacts" +SWIFTLINT_CMD=$(ls "$SWIFT_PACKAGE_DIR"/swiftlintplugins/SwiftLintBinary/SwiftLintBinary.artifactbundle/swiftlint-*/bin/swiftlint | head -n 1) -_Note: This implicitly trusts all Xcode package plugins and macros in packages and bypasses Xcode's package validation - dialogs, which has security implications._ +if test -f "$SWIFTLINT_CMD" 2>&1 +then + "$SWIFTLINT_CMD" +else + echo "warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions." +fi +``` -#### Swift Package +> [!NOTE] +> The `SWIFTLINT_CMD` path uses the default Xcode configuration and has been +> tested on Xcode 15/16. In case of another configuration (e.g. a custom +> Swift package path), please adapt the values accordingly. -You can integrate SwiftLint as a Swift Package Manager Plug-in if you're working with -a Swift Package with a `Package.swift` manifest. +> [!TIP] +> Uncheck `Based on dependency analysis` to run `swiftlint` on all incremental +> builds, suppressing the unspecified outputs warning. -Add SwiftLint as a package dependency to your `Package.swift` file. -Add SwiftLint to a target using the `plugins` parameter. +#### Consideration for Xcode 15.0 -```swift -.target( - ... - plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] -), +Xcode 15 made a significant change by setting the default value of the +`ENABLE_USER_SCRIPT_SANDBOXING` build setting from `NO` to `YES`. +As a result, SwiftLint encounters an error related to missing file permissions, +which typically manifests as +`error: Sandbox: swiftlint(19427) deny(1) file-read-data.` + +To resolve this issue, it is necessary to manually set the +`ENABLE_USER_SCRIPT_SANDBOXING` setting to `NO` for the specific target that +SwiftLint is being configured for. + +#### Consideration for Apple Silicon + +If you installed SwiftLint via Homebrew on Apple Silicon, you might experience +this warning: + +```bash +warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint +``` + +That is because Homebrew on Apple Silicon installs the binaries into the +`/opt/homebrew/bin` folder by default. To instruct Xcode where to find +SwiftLint, you can either add `/opt/homebrew/bin` to the `PATH` environment +variable in your build phase: + +```bash +if [[ "$(uname -m)" == arm64 ]] +then + export PATH="/opt/homebrew/bin:$PATH" +fi + +if command -v swiftlint >/dev/null 2>&1 +then + swiftlint +else + echo "warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions." +fi +``` + +or you can create a symbolic link in `/usr/local/bin` pointing to the actual +binary: + +```bash +ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint +``` + +#### Additional Considerations + +If you wish to fix violations as well, your script could run +`swiftlint --fix && swiftlint` instead of just `swiftlint`. This will mean +that all correctable violations are fixed while ensuring warnings show up in +your project for remaining violations. + +If you've installed SwiftLint via CocoaPods the script should look like this: + +```bash +"${PODS_ROOT}/SwiftLint/swiftlint" ``` ### Visual Studio Code -To integrate SwiftLint with [vscode](https://code.visualstudio.com), install the -[`vscode-swiftlint`](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swiftlint) extension from the marketplace. +To integrate SwiftLint with [Visual Studio Code](https://code.visualstudio.com), install the +[`vscode-swiftlint`](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swiftlint) +extension from the marketplace. -### fastlane +### Fastlane -You can use the [official swiftlint fastlane action](https://docs.fastlane.tools/actions/swiftlint) to run SwiftLint as part of your fastlane process. +You can use the official +[`swiftlint` fastlane action](https://docs.fastlane.tools/actions/swiftlint) +to run SwiftLint as part of your fastlane process. ```ruby swiftlint( @@ -298,18 +449,23 @@ swiftlint( ### Docker -`swiftlint` is also available as a [Docker](https://www.docker.com/) image using `Ubuntu`. -So just the first time you need to pull the docker image using the next command: +SwiftLint is also available as a [Docker](https://www.docker.com/) image using +`Ubuntu`. So just the first time you need to pull the docker image using the +next command: + ```bash docker pull ghcr.io/realm/swiftlint:latest ``` Then following times, you just run `swiftlint` inside of the docker like: + ```bash docker run -it -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest ``` -This will execute `swiftlint` in the folder where you are right now (`pwd`), showing an output like: +This will execute `swiftlint` in the folder where you are right now (`pwd`), +showing an output like: + ```bash $ docker run -it -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest Linting Swift files in current working directory @@ -319,11 +475,12 @@ Linting 'YamlSwiftLintTests.swift' (490/490) Done linting! Found 0 violations, 0 serious in 490 files. ``` -Here you have more documentation about the usage of [Docker Images](https://docs.docker.com/). +Here you have more documentation about the usage of +[Docker Images](https://docs.docker.com/). -### Command Line +## Command Line Usage -``` +```txt $ swiftlint help OVERVIEW: A tool to enforce Swift style and conventions. @@ -336,8 +493,9 @@ OPTIONS: SUBCOMMANDS: analyze Run analysis rules docs Open SwiftLint documentation website in the default web browser - generate-docs Generates markdown documentation for all rules + generate-docs Generates markdown documentation for selected group of rules lint (default) Print lint warnings and errors + baseline Operations on existing baselines reporters Display the list of reporters and their identifiers rules Display the list of rules and their identifiers version Display the current version of SwiftLint @@ -353,13 +511,19 @@ To specify a list of files when using `lint` or `analyze` [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode plugin, or modified files in the working tree based on `git ls-files -m`), you can do so by passing the option `--use-script-input-files` and setting the -following instance variables: `SCRIPT_INPUT_FILE_COUNT` and -`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`. +following instance variables: `SCRIPT_INPUT_FILE_COUNT` +and `SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`, ..., +`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`. +Similarly, files can be read from file lists by passing +the option `--use-script-input-file-lists` and setting the +following instance variables: `SCRIPT_INPUT_FILE_LIST_COUNT` +and `SCRIPT_INPUT_FILE_LIST_0`, `SCRIPT_INPUT_FILE_LIST_1`, ..., +`SCRIPT_INPUT_FILE_LIST_{SCRIPT_INPUT_FILE_LIST_COUNT - 1}`. These are same environment variables set for input files to [custom Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/). -### Working With Multiple Swift Versions +## Working With Multiple Swift Versions SwiftLint hooks into SourceKit so it continues working even as Swift evolves! @@ -390,14 +554,14 @@ You may also set the `TOOLCHAINS` environment variable to the reverse-DNS notation that identifies a Swift toolchain version: ```shell -$ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint --fix +TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint --fix ``` On Linux, SourceKit is expected to be located in `/usr/lib/libsourcekitdInProc.so` or specified by the `LINUX_SOURCEKIT_LIB_PATH` environment variable. -### pre-commit +## Git `pre-commit` Hook SwiftLint can be run as a [pre-commit](https://pre-commit.com/) hook. Once [installed](https://pre-commit.com/#install), add this to the @@ -411,9 +575,11 @@ repos: - id: swiftlint ``` -Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` can be used to update to the current version. +Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` +can be used to update to the current version. SwiftLint can be configured using `entry` to apply fixes and fail on errors: + ```yaml - repo: https://github.com/realm/SwiftLint rev: 0.50.3 @@ -426,12 +592,14 @@ SwiftLint can be configured using `entry` to apply fixes and fail on errors: Over 200 rules are included in SwiftLint and the Swift community (that's you!) continues to contribute more over time. -[Pull requests](https://github.com/realm/SwiftLint/blob/main/CONTRIBUTING.md) are encouraged. +[Pull requests](https://github.com/realm/SwiftLint/blob/main/CONTRIBUTING.md) +are encouraged. You can find an updated list of rules and more information about them [here](https://realm.github.io/SwiftLint/rule-directory.html). -You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules) +You can also check the +[Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules) directory to see their implementation. ### Opt-In Rules @@ -467,7 +635,8 @@ let noWarning :String = "" // No warning about colons immediately after variable let hasWarning :String = "" // Warning generated about colons immediately after variable names ``` -Including the `all` keyword will disable all rules until the linter sees a matching enable comment: +Including the `all` keyword will disable all rules until the linter sees a +matching enable comment: `// swiftlint:disable all` `// swiftlint:enable all` @@ -517,7 +686,8 @@ Rule inclusion: * `analyzer_rules`: This is an entirely separate list of rules that are only run by the `analyze` command. All analyzer rules are opt-in, so this is the only configurable rule list, there are no equivalents for `disabled_rules` - `only_rules`. + and `only_rules`. The special `all` identifier can also be used here to enable + all analyzer rules, except the ones listed in `disabled_rules`. ```yaml # By default, SwiftLint uses a set of sensible default rules you can adjust: @@ -536,7 +706,9 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in analyzer_rules: # rules run by `swiftlint analyze` - explicit_self -included: # case-sensitive paths to include during linting. `--path` is ignored if present +# Case-sensitive paths to include during linting. Directory paths supplied on the +# command line will be ignored. +included: - Sources excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included` - Carthage @@ -551,6 +723,18 @@ allow_zero_lintable_files: false # If true, SwiftLint will treat all warnings as errors. strict: false +# If true, SwiftLint will treat all errors as warnings. +lenient: false + +# The path to a baseline file, which will be used to filter out detected violations. +baseline: Baseline.json + +# The path to save detected violations to as a new baseline. +write_baseline: Baseline.json + +# If true, SwiftLint will check for updates after linting or analyzing. +check_for_updates: true + # configurable rules can be customized from this configuration file # binary rules can set their severity level force_cast: warning # implicitly @@ -613,9 +797,9 @@ following syntax: ```yaml custom_rules: pirates_beat_ninjas: # rule identifier - included: + included: - ".*\\.swift" # regex that defines paths to include during linting. optional. - excluded: + excluded: - ".*Test\\.swift" # regex that defines paths to exclude during linting. optional name: "Pirates Beat Ninjas" # rule name. optional. regex: "([nN]inja)" # matching pattern @@ -634,12 +818,13 @@ This is what the output would look like: ![](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/custom-rule.png) -It is important to note that the regular expression pattern is used with the flags `s` -and `m` enabled, that is `.` +It is important to note that the regular expression pattern is used with the +flags `s` and `m` enabled, that is `.` [matches newlines](https://developer.apple.com/documentation/foundation/nsregularexpression/options/1412529-dotmatcheslineseparators) -and `^`/`$` +and `^`/`$` [match the start and end of lines](https://developer.apple.com/documentation/foundation/nsregularexpression/options/1408263-anchorsmatchlines), -respectively. If you do not want to have `.` match newlines, for example, the regex can be prepended by `(?-s)`. +respectively. If you do not want to have `.` match newlines, for example, the +regex can be prepended by `(?-s)`. You can filter the matches by providing one or more `match_kinds`, which will reject matches that include syntax kinds that are not present in this list. Here @@ -674,8 +859,18 @@ All syntax kinds used in a snippet of Swift code can be extracted asking which match to `keyword` and `identifier` in the above list. -If using custom rules in combination with `only_rules`, make sure to add -`custom_rules` as an item under `only_rules`. +If using custom rules in combination with `only_rules`, you must include the +literal string `custom_rules` in the `only_rules` list: + +```yaml +only_rules: + - custom_rules + +custom_rules: + no_hiding_in_strings: + regex: "([nN]inja)" + match_kinds: string +``` Unlike Swift custom rules, you can use official SwiftLint builds (e.g. from Homebrew) to run regex custom rules. @@ -700,7 +895,7 @@ command invocation (incremental builds will fail) must be passed to `analyze` via the `--compiler-log-path` flag. e.g. `--compiler-log-path /path/to/xcodebuild.log` -This can be obtained by +This can be obtained by 1. Cleaning DerivedData (incremental builds won't work with analyze) 2. Running `xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log` @@ -711,13 +906,14 @@ Analyzer rules tend to be considerably slower than lint rules. ## Using Multiple Configuration Files SwiftLint offers a variety of ways to include multiple configuration files. -Multiple configuration files get merged into one single configuration that is then applied -just as a single configuration file would get applied. +Multiple configuration files get merged into one single configuration that is +then applied just as a single configuration file would get applied. -There are quite a lot of use cases where using multiple configuration files could be helpful: +There are quite a lot of use cases where using multiple configuration files +could be helpful: -For instance, one could use a team-wide shared SwiftLint configuration while allowing overrides -in each project via a child configuration file. +For instance, one could use a team-wide shared SwiftLint configuration while +allowing overrides in each project via a child configuration file. Team-Wide Configuration: @@ -733,18 +929,20 @@ opt_in_rules: - force_cast ``` -### Child / Parent Configs (Locally) +### Child/Parent Configs (Locally) -You can specify a `child_config` and / or a `parent_config` reference within a configuration file. -These references should be local paths relative to the folder of the configuration file they are specified in. -This even works recursively, as long as there are no cycles and no ambiguities. +You can specify a `child_config` and/or a `parent_config` reference within a +configuration file. These references should be local paths relative to the +folder of the configuration file they are specified in. This even works +recursively, as long as there are no cycles and no ambiguities. -**A child config is treated as a refinement and therefore has a higher priority**, -while a parent config is considered a base with lower priority in case of conflicts. +**A child config is treated as a refinement and thus has a higher priority**, +while a parent config is considered a base with lower priority in case of +conflicts. Here's an example, assuming you have the following file structure: -``` +```txt ProjectRoot |_ .swiftlint.yml |_ .swiftlint_refinement.yml @@ -752,7 +950,8 @@ ProjectRoot |_ .swiftlint_base.yml ``` -To include both the refinement and the base file, your `.swiftlint.yml` should look like this: +To include both the refinement and the base file, your `.swiftlint.yml` should +look like this: ```yaml child_config: .swiftlint_refinement.yml @@ -763,11 +962,12 @@ When merging parent and child configs, `included` and `excluded` configurations are processed carefully to account for differences in the directory location of the containing configuration files. -### Child / Parent Configs (Remote) +### Child/Parent Configs (Remote) -Just as you can provide local `child_config` / `parent_config` references, instead of -referencing local paths, you can just put urls that lead to configuration files. -In order for SwiftLint to detect these remote references, they must start with `http://` or `https://`. +Just as you can provide local `child_config`/`parent_config` references, +instead of referencing local paths, you can just put urls that lead to +configuration files. In order for SwiftLint to detect these remote references, +they must start with `http://` or `https://`. The referenced remote configuration files may even recursively reference other remote configuration files, but aren't allowed to include local references. @@ -778,20 +978,23 @@ Using a remote reference, your `.swiftlint.yml` could look like this: parent_config: https://myteamserver.com/our-base-swiftlint-config.yml ``` -Every time you run SwiftLint and have an Internet connection, SwiftLint tries to get a new version of -every remote configuration that is referenced. If this request times out, a cached version is -used if available. If there is no cached version available, SwiftLint fails – but no worries, a cached version -should be there once SwiftLint has run successfully at least once. +Every time you run SwiftLint and have an Internet connection, SwiftLint tries +to get a new version of every remote configuration that is referenced. If this +request times out, a cached version is used if available. If there is no cached +version available, SwiftLint fails – but no worries, a cached version should be +there once SwiftLint has run successfully at least once. -If needed, the timeouts for the remote configuration fetching can be specified manually via the -configuration file(s) using the `remote_timeout` / `remote_timeout_if_cached` specifiers. -These values default to 2 / 1 second(s). +If needed, the timeouts for the remote configuration fetching can be specified +manually via the configuration file(s) using the +`remote_timeout`/`remote_timeout_if_cached` specifiers. These values default +to 2 seconds or 1 second, respectively. ### Command Line -Instead of just providing one configuration file when running SwiftLint via the command line, -you can also pass a hierarchy, where the first configuration is treated as a parent, -while the last one is treated as the highest-priority child. +Instead of just providing one configuration file when running SwiftLint via the +command line, you can also pass a hierarchy, where the first configuration is +treated as a parent, while the last one is treated as the highest-priority +child. A simple example including just two configuration files looks like this: @@ -799,23 +1002,26 @@ A simple example including just two configuration files looks like this: ### Nested Configurations -In addition to a main configuration (the `.swiftlint.yml` file in the root folder), -you can put other configuration files named `.swiftlint.yml` into the directory structure -that then get merged as a child config, but only with an effect for those files -that are within the same directory as the config or in a deeper directory where -there isn't another configuration file. In other words: Nested configurations don't work -recursively – there's a maximum number of one nested configuration per file -that may be applied in addition to the main configuration. - -`.swiftlint.yml` files are only considered as a nested configuration if they have not been -used to build the main configuration already (e. g. by having been referenced via something -like `child_config: Folder/.swiftlint.yml`). Also, `parent_config` / `child_config` -specifications of nested configurations are getting ignored because there's no sense to that. - -If one (or more) SwiftLint file(s) are explicitly specified via the `--config` parameter, -that configuration will be treated as an override, no matter whether there exist -other `.swiftlint.yml` files somewhere within the directory. **So if you want to use - nested configurations, you can't use the `--config` parameter.** +In addition to a main configuration (the `.swiftlint.yml` file in the root +folder), you can put other configuration files named `.swiftlint.yml` into the +directory structure that then get merged as a child config, but only with an +effect for those files that are within the same directory as the config or in a +deeper directory where there isn't another configuration file. In other words: +Nested configurations don't work recursively – there's a maximum number of one +nested configuration per file that may be applied in addition to the main +configuration. + +`.swiftlint.yml` files are only considered as a nested configuration if they +have not been used to build the main configuration already (e. g. by having +been referenced via something like `child_config: Folder/.swiftlint.yml`). +Also, `parent_config`/`child_config` specifications of nested configurations +are getting ignored because there's no sense to that. + +If one (or more) SwiftLint file(s) are explicitly specified via the `--config` +parameter, that configuration will be treated as an override, no matter whether +there exist other `.swiftlint.yml` files somewhere within the directory. +**So if you want to use nested configurations, you can't use the `--config` +parameter.** ## License diff --git a/README_CN.md b/README_CN.md index afc8083e36..a1593f6e51 100644 --- a/README_CN.md +++ b/README_CN.md @@ -13,6 +13,33 @@ SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jps 不可接受的行为报告给 [info@realm.io](mailto:info@realm.io)。 ## 安装 +### 使用[Swift Package Manager](https://github.com/apple/swift-package-manager) + +SwiftLint 可以用作[命令插件](#swift-package-command-plugin)或[构建工具插件](#build-tool-plugins) + +添加 + +```swift +.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "") +``` + +到你的 `Package.swift` 文件中,以自动获取 SwiftLint 的最新版本,或者将依赖项固定到特定版本: + +```swift +.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "") +``` + +其中,用所需的最低版本或精确版本替换 ``。 + + +### [Xcode Package Dependency](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) + +使用以下链接将 SwiftLint 作为包依赖添加到 Xcode 项目中: + +```bash +https://github.com/SimplyDanny/SwiftLintPlugins +``` + ### 使用 [Homebrew](http://brew.sh/): @@ -193,7 +220,7 @@ Xcode 构建工具插件。 选择要添加修正的目标,打开 `Build Phases` 检查器。 打开 `Run Build Tool Plug-ins` 并选择 `+` 按钮。 -从列表中选择 `SwiftLintPlugin` 并将其添加到项目中。 +从列表中选择 `SwiftLintBuildToolPlugin` 并将其添加到项目中。 ![](https://raw.githubusercontent.com/realm/SwiftLint/main/assets/select-swiftlint-plugin.png) diff --git a/Releasing.md b/Releasing.md deleted file mode 100644 index fe7520ad4b..0000000000 --- a/Releasing.md +++ /dev/null @@ -1,14 +0,0 @@ -# Releasing SwiftLint - -For SwiftLint contributors, follow these steps to cut a release: - -1. Come up with a witty washer- or dryer-themed release name. Past names include: - * Tumble Dry - * FabricSoftenerRule - * Top Loading - * Fresh Out Of The Dryer -1. Make sure you have the latest stable Xcode version installed and - `xcode-select`ed -1. Release new version: `make release "0.2.0: Tumble Dry"` -1. Wait for the Docker CI job to finish then run: `make zip_linux_release` -1. Celebrate. :tada: diff --git a/Source/SourceKittenFrameworkWrapper/Empty.swift b/Source/SourceKittenFrameworkWrapper/Empty.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift b/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift index caea996073..088917375e 100644 --- a/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift +++ b/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift @@ -135,7 +135,7 @@ extension SourceKittenDictionary { /// Whether or not the dictionary represents a SwiftUI View with an `accesibilityHidden(true)` /// or `accessibility(hidden: true)` modifier. func hasAccessibilityHiddenModifier(in file: SwiftLintFile) -> Bool { - return hasModifier( + hasModifier( anyOf: [ SwiftUIModifier( name: "accessibilityHidden", @@ -144,7 +144,7 @@ extension SourceKittenDictionary { SwiftUIModifier( name: "accessibility", arguments: [.init(name: "hidden", values: ["true"])] - ) + ), ], in: file ) @@ -153,12 +153,12 @@ extension SourceKittenDictionary { /// Whether or not the dictionary represents a SwiftUI View with an `accessibilityElement()` or /// `accessibilityElement(children: .ignore)` modifier (`.ignore` is the default parameter value). func hasAccessibilityElementChildrenIgnoreModifier(in file: SwiftLintFile) -> Bool { - return hasModifier( + hasModifier( anyOf: [ SwiftUIModifier( name: "accessibilityElement", arguments: [.init(name: "children", required: false, values: [".ignore"], matchType: .suffix)] - ) + ), ], in: file ) diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 8cea60e325..52d20f156f 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -6,8 +6,9 @@ public let builtInRules: [any Rule.Type] = [ AccessibilityLabelForImageRule.self, AccessibilityTraitForButtonRule.self, AnonymousArgumentInMultilineClosureRule.self, - AnyObjectProtocolRule.self, ArrayInitRule.self, + AsyncWithoutAwaitRule.self, + AttributeNameSpacingRule.self, AttributesRule.self, BalancedXCTestLifecycleRule.self, BlanketDisableCommandRule.self, @@ -31,6 +32,7 @@ public let builtInRules: [any Rule.Type] = [ ContainsOverFilterIsEmptyRule.self, ContainsOverFirstNotNilRule.self, ContainsOverRangeNilComparisonRule.self, + ContrastedOpeningBraceRule.self, ControlStatementRule.self, ConvenienceTypeRule.self, CyclomaticComplexityRule.self, @@ -129,6 +131,7 @@ public let builtInRules: [any Rule.Type] = [ NavigationTitleRule.self, NestingRule.self, NimbleOperatorRule.self, + NoEmptyBlockRule.self, NoExtensionAccessModifierRule.self, NoFallthroughOnlyRule.self, NoGroupingExtensionRule.self, @@ -139,19 +142,22 @@ public let builtInRules: [any Rule.Type] = [ NotificationCenterDetachmentRule.self, NumberSeparatorRule.self, ObjectLiteralRule.self, - OneDelarationPerFileRule.self, + OneDeclarationPerFileRule.self, OpeningBraceRule.self, OperatorFunctionWhitespaceRule.self, OperatorUsageWhitespaceRule.self, + OptionalDataStringConversionRule.self, OptionalEnumCaseMatchingRule.self, OrphanedDocCommentRule.self, OverriddenSuperCallRule.self, OverrideInExtensionRule.self, PatternMatchingKeywordsRule.self, PeriodSpacingRule.self, + PreferKeyPathRule.self, PreferNimbleRule.self, PreferSelfInStaticReferencesRule.self, PreferSelfTypeOverTypeOfSelfRule.self, + PreferTypeCheckingRule.self, PreferZeroOverExplicitInitRule.self, PrefixedTopLevelConstantRule.self, PrivateActionRule.self, @@ -193,6 +199,7 @@ public let builtInRules: [any Rule.Type] = [ SortedImportsRule.self, StatementPositionRule.self, StaticOperatorRule.self, + StaticOverFinalClassRule.self, StrictFilePrivateRule.self, StringLocalizationCorrectArgumentsRule.self, StrongIBOutletRule.self, @@ -230,6 +237,7 @@ public let builtInRules: [any Rule.Type] = [ UnusedEnumeratedRule.self, UnusedImportRule.self, UnusedOptionalBindingRule.self, + UnusedParameterRule.self, UnusedSetterValueRule.self, ValidIBInspectableRule.self, VerticalParameterAlignmentOnCallRule.self, @@ -243,5 +251,5 @@ public let builtInRules: [any Rule.Type] = [ WeakDelegateRule.self, XCTFailMessageRule.self, XCTSpecificMatcherRule.self, - YodaConditionRule.self + YodaConditionRule.self, ] diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/AnonymousArgumentInMultilineClosureRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/AnonymousArgumentInMultilineClosureRule.swift index ab214fade3..332668063b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/AnonymousArgumentInMultilineClosureRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/AnonymousArgumentInMultilineClosureRule.swift @@ -21,14 +21,14 @@ struct AnonymousArgumentInMultilineClosureRule: OptInRule { closure { arg in nestedClosure { $0 + arg } } - """) + """), ], triggeringExamples: [ Example(""" closure { print(↓$0) } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/BlockBasedKVORule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/BlockBasedKVORule.swift index 0554ce90a4..e0115da924 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/BlockBasedKVORule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/BlockBasedKVORule.swift @@ -14,7 +14,7 @@ struct BlockBasedKVORule: Rule { let observer = foo.observe(\.value, options: [.new]) { (foo, change) in print(change.newValue) } - """#) + """#), ], triggeringExamples: [ Example(""" @@ -30,7 +30,7 @@ struct BlockBasedKVORule: Rule { change: Dictionary?, context: UnsafeMutableRawPointer?) {} } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ConvenienceTypeRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ConvenienceTypeRule.swift index 049817115c..50c4f7002a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ConvenienceTypeRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ConvenienceTypeRule.swift @@ -73,7 +73,7 @@ struct ConvenienceTypeRule: OptInRule { @globalActor actor MyActor { static let shared = MyActor() } - """) + """), ], triggeringExamples: [ Example(""" @@ -112,7 +112,7 @@ struct ConvenienceTypeRule: OptInRule { ↓class SomeClass { static func foo() {} } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedAssertRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedAssertRule.swift index f23f7cccf6..266cbc3269 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedAssertRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedAssertRule.swift @@ -14,13 +14,13 @@ struct DiscouragedAssertRule: OptInRule { Example(#"assert(true, "foobar")"#), Example(#"assert(true, "foobar", file: "toto", line: 42)"#), Example(#"assert(false || true)"#), - Example(#"XCTAssert(false)"#) + Example(#"XCTAssert(false)"#), ], triggeringExamples: [ Example(#"↓assert(false)"#), Example(#"↓assert(false, "foobar")"#), Example(#"↓assert(false, "foobar", file: "toto", line: 42)"#), - Example(#"↓assert( false , "foobar")"#) + Example(#"↓assert( false , "foobar")"#), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedNoneNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedNoneNameRule.swift index e80b19c359..a496e6e87f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedNoneNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedNoneNameRule.swift @@ -83,7 +83,7 @@ struct DiscouragedNoneNameRule: OptInRule { class MyClass { var none = MyClass() } - """) + """), ], triggeringExamples: [ Example(""" @@ -174,7 +174,7 @@ struct DiscouragedNoneNameRule: OptInRule { struct MyStruct { ↓static var none = MyStruct(), a = MyStruct() } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift index 0e5f7c6cf5..f818d0f2dc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift @@ -15,11 +15,11 @@ struct DiscouragedObjectLiteralRule: OptInRule { Example("let color = UIColor(red: value, green: value, blue: value, alpha: 1)"), Example("let image = NSImage(named: aVariable)"), Example("let image = NSImage(named: \"interpolated \\(variable)\")"), - Example("let color = NSColor(red: value, green: value, blue: value, alpha: 1)") + Example("let color = NSColor(red: value, green: value, blue: value, alpha: 1)"), ], triggeringExamples: [ Example("let image = ↓#imageLiteral(resourceName: \"image.jpg\")"), - Example("let color = ↓#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)") + Example("let color = ↓#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalBooleanRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalBooleanRuleExamples.swift index 7a309d258d..40506631bd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalBooleanRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalBooleanRuleExamples.swift @@ -47,7 +47,7 @@ internal struct DiscouragedOptionalBooleanRuleExamples { wrapExample("enum", "func foo(input: Bool = true) {}"), wrapExample("enum", "func foo(input: [String: Bool] = [:]) {}"), - wrapExample("enum", "func foo(input: [Bool] = []) {}") + wrapExample("enum", "func foo(input: [Bool] = []) {}"), ] static let triggeringExamples = [ @@ -157,12 +157,15 @@ internal struct DiscouragedOptionalBooleanRuleExamples { wrapExample("enum", "static func foo(input: [↓Bool?]) {}"), // Optional chaining - Example("_ = ↓Bool?.values()") + Example("_ = ↓Bool?.values()"), ] } // MARK: - Private -private func wrapExample(_ type: String, _ test: String, file: StaticString = #file, line: UInt = #line) -> Example { - return Example("\(type) Foo {\n\t\(test)\n}", file: file, line: line) +private func wrapExample(_ type: String, + _ test: String, + file: StaticString = #filePath, + line: UInt = #line) -> Example { + Example("\(type) Foo {\n\t\(test)\n}", file: file, line: line) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift index 56030d9803..b289d34cb6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift @@ -51,7 +51,7 @@ internal struct DiscouragedOptionalCollectionExamples { wrapExample("enum", "func foo(input: [String] = []) {}"), wrapExample("enum", "func foo(input: [String: String] = [:]) {}"), - wrapExample("enum", "func foo(input: Set = []) {}") + wrapExample("enum", "func foo(input: Set = []) {}"), ] static let triggeringExamples = [ @@ -204,14 +204,17 @@ internal struct DiscouragedOptionalCollectionExamples { wrapExample("enum", "static func foo(input: [String: ↓[String: String]?]) {}"), wrapExample("enum", "static func foo(input: ↓[String: ↓[String: String]?]?) {}"), wrapExample("enum", "static func foo(_ dict1: [K: V], _ dict2: ↓[K: V]?) -> [K: V]"), - wrapExample("enum", "static func foo(dict1: [K: V], dict2: ↓[K: V]?) -> [K: V]") + wrapExample("enum", "static func foo(dict1: [K: V], dict2: ↓[K: V]?) -> [K: V]"), ] } // MARK: - Private -private func wrapExample(_ type: String, _ test: String, file: StaticString = #file, line: UInt = #line) -> Example { - return Example(""" +private func wrapExample(_ type: String, + _ test: String, + file: StaticString = #filePath, + line: UInt = #line) -> Example { + Example(""" \(type) Foo { \(test) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRule.swift index 30aa2332f4..69514185f5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRule.swift @@ -9,7 +9,7 @@ struct DuplicateImportsRule: SwiftSyntaxCorrectableRule { static let importKinds = [ "typealias", "struct", "class", "enum", "protocol", "let", - "var", "func" + "var", "func", ] static let description = RuleDescription( @@ -31,7 +31,7 @@ struct DuplicateImportsRule: SwiftSyntaxCorrectableRule { } } - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { + func makeVisitor(file _: SwiftLintFile) -> ViolationsSyntaxVisitor { queuedFatalError("Unreachable: `validate(file:)` will be used instead") } @@ -42,16 +42,26 @@ struct DuplicateImportsRule: SwiftSyntaxCorrectableRule { // MARK: - Private +private struct Import { + let position: AbsolutePosition + let path: [String] + let hasAttributes: Bool +} + private final class ImportPathVisitor: SyntaxVisitor { - var importPaths = [AbsolutePosition: [String]]() - var sortedImportPaths: [(position: AbsolutePosition, path: [String])] { - importPaths - .sorted { $0.key < $1.key } - .map { (position: $0, path: $1) } + var importPaths = [Import]() + var sortedImportPaths: [Import] { + importPaths.sorted { $0.position < $1.position } } override func visitPost(_ node: ImportDeclSyntax) { - importPaths[node.positionAfterSkippingLeadingTrivia] = node.path.map(\.name.text) + importPaths.append( + .init( + position: node.positionAfterSkippingLeadingTrivia, + path: node.path.map(\.name.text), + hasAttributes: node.attributes.contains { $0.is(AttributeSyntax.self) } + ) + ) } } @@ -100,7 +110,9 @@ private extension SwiftLintFile { var seen = Set() // Exact matches - for (position, path) in importPaths { + for `import` in importPaths { + let path = `import`.path + let position = `import`.position let rangesForPosition = ranges(for: position) defer { @@ -127,7 +139,9 @@ private extension SwiftLintFile { } // Partial matches - for (position, path) in importPaths { + for `import` in importPaths where !`import`.hasAttributes { + let path = `import`.path + let position = `import`.position let violation = importPaths.contains { other in let otherPath = other.path guard path.starts(with: otherPath), otherPath != path else { return false } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRuleExamples.swift index a2c29282b7..4a0613fc88 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DuplicateImportsRuleExamples.swift @@ -32,11 +32,16 @@ internal struct DuplicateImportsRuleExamples { #if TEST func test() { } - """) + """), + Example(""" + import Foo + @testable import struct Foo.Bar + """), ] static let triggeringExamples = Array(corrections.keys.sorted()) + // swiftlint:disable:next closure_body_length static let corrections: [Example: Example] = { var corrections = [ Example(""" @@ -126,6 +131,12 @@ internal struct DuplicateImportsRuleExamples { """), Example(""" + @testable import Foo + import struct Foo.Bar + """): Example(""" + @testable import Foo + """), + Example(""" ↓import A.B.C ↓import A.B import A @@ -169,7 +180,7 @@ internal struct DuplicateImportsRuleExamples { """, excludeFromDocumentation: true): Example(""" import A - """) + """), ] DuplicateImportsRule.importKinds.map { importKind in diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitACLRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitACLRule.swift index a3cdde9f18..0cf148a8fc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitACLRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitACLRule.swift @@ -76,7 +76,7 @@ struct ExplicitACLRule: OptInRule { let b = 2 } } - """) + """), ], triggeringExamples: [ Example("↓enum A {}"), @@ -101,7 +101,7 @@ struct ExplicitACLRule: OptInRule { ↓let b = 2 } } - """) + """), ] ) } @@ -121,7 +121,7 @@ private extension ExplicitACLRule { SubscriptDeclSyntax.self, VariableDeclSyntax.self, ProtocolDeclSyntax.self, - InitializerDeclSyntax.self + InitializerDeclSyntax.self, ] } @@ -131,7 +131,7 @@ private extension ExplicitACLRule { return node.modifiers.containsPrivateOrFileprivate() ? .skipChildren : .visitChildren } - override func visitPost(_ node: ActorDeclSyntax) { + override func visitPost(_: ActorDeclSyntax) { declScope.pop() } @@ -141,15 +141,15 @@ private extension ExplicitACLRule { return node.modifiers.containsPrivateOrFileprivate() ? .skipChildren : .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { + override func visitPost(_: ClassDeclSyntax) { declScope.pop() } - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } - override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind { .skipChildren } @@ -158,7 +158,7 @@ private extension ExplicitACLRule { return node.modifiers.containsPrivateOrFileprivate() ? .skipChildren : .visitChildren } - override func visitPost(_ node: ExtensionDeclSyntax) { + override func visitPost(_: ExtensionDeclSyntax) { declScope.pop() } @@ -168,7 +168,7 @@ private extension ExplicitACLRule { return node.modifiers.containsPrivateOrFileprivate() ? .skipChildren : .visitChildren } - override func visitPost(_ node: EnumDeclSyntax) { + override func visitPost(_: EnumDeclSyntax) { declScope.pop() } @@ -190,7 +190,7 @@ private extension ExplicitACLRule { return node.modifiers.containsPrivateOrFileprivate() ? .skipChildren : .visitChildren } - override func visitPost(_ node: StructDeclSyntax) { + override func visitPost(_: StructDeclSyntax) { declScope.pop() } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitEnumRawValueRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitEnumRawValueRule.swift index 2177f135ed..7191dbd589 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitEnumRawValueRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitEnumRawValueRule.swift @@ -39,7 +39,7 @@ struct ExplicitEnumRawValueRule: OptInRule { enum Numbers: Algebra { case one } - """) + """), ], triggeringExamples: [ Example(""" @@ -74,7 +74,7 @@ struct ExplicitEnumRawValueRule: OptInRule { case ↓one, ↓two } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift index c25ead7e36..3a0add4f3a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift @@ -48,7 +48,7 @@ struct ExplicitInitRule: OptInRule { obs2, resultSelector: MyType.init ).asMaybe() - """) + """), ], triggeringExamples: [ Example(""" @@ -86,7 +86,7 @@ struct ExplicitInitRule: OptInRule { .init(1.0) - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], corrections: [ Example(""" @@ -178,7 +178,7 @@ struct ExplicitInitRule: OptInRule { Example("_ = GleanMetrics.Tabs.GroupedTabExtra↓.init()"): Example("_ = GleanMetrics.Tabs.GroupedTabExtra()"), Example("_ = Set↓.init()"): - Example("_ = Set()") + Example("_ = Set()"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTopLevelACLRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTopLevelACLRule.swift index 56c30b7040..d85c33c66a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTopLevelACLRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTopLevelACLRule.swift @@ -12,20 +12,26 @@ struct ExplicitTopLevelACLRule: OptInRule { nonTriggeringExamples: [ Example("internal enum A {}"), Example("public final class B {}"), - Example("private struct C {}"), + Example(""" + private struct S1 { + struct S2 {} + } + """), Example("internal enum A { enum B {} }"), - Example("internal final class Foo {}"), - Example("internal\nclass Foo {}"), + Example("internal final actor Foo {}"), + Example("internal typealias Foo = Bar"), Example("internal func a() {}"), Example("extension A: Equatable {}"), - Example("extension A {}") + Example("extension A {}"), + Example("f { func f() {} }", excludeFromDocumentation: true), + Example("do { func f() {} }", excludeFromDocumentation: true), ], triggeringExamples: [ Example("↓enum A {}"), Example("final ↓class B {}"), - Example("↓struct C {}"), + Example("↓protocol P {}"), Example("↓func a() {}"), - Example("internal let a = 0\n↓func b() {}") + Example("internal let a = 0\n↓func b() {}"), ] ) } @@ -66,11 +72,11 @@ private extension ExplicitTopLevelACLRule { collectViolations(decl: node, token: node.bindingSpecifier) } - override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind { .skipChildren } - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTypeInterfaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTypeInterfaceRule.swift index b37a318712..ec4fc88fe6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTypeInterfaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitTypeInterfaceRule.swift @@ -34,7 +34,7 @@ struct ExplicitTypeInterfaceRule: OptInRule { func f() { if case .failure(let error) = errorCompletion {} } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example(""" @@ -66,7 +66,7 @@ struct ExplicitTypeInterfaceRule: OptInRule { class Foo { let ↓myVar = Set(0) } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExtensionAccessModifierRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExtensionAccessModifierRule.swift index 6ec81d59d3..3fdee1d48e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExtensionAccessModifierRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExtensionAccessModifierRule.swift @@ -97,7 +97,12 @@ struct ExtensionAccessModifierRule: OptInRule { set { Foo.shared.bar = newValue } } } - """) + """), + Example(""" + public extension Foo { + private(set) var value: Int { 1 } + } + """), ], triggeringExamples: [ Example(""" @@ -156,7 +161,12 @@ struct ExtensionAccessModifierRule: OptInRule { ↓private func bar() {} ↓private func baz() {} } - """) + """), + Example(""" + ↓extension Foo { + private(set) public var value: Int { 1 } + } + """), ] ) } @@ -166,7 +176,7 @@ private extension ExtensionAccessModifierRule { case implicit case explicit(TokenKind) - static func from(tokenKind: TokenKind?) -> ACL { + static func from(tokenKind: TokenKind?) -> Self { switch tokenKind { case nil: return .implicit @@ -180,7 +190,7 @@ private extension ExtensionAccessModifierRule { .explicit(.keyword(.internal)), .explicit(.keyword(.private)), .explicit(.keyword(.open)), - .implicit + .implicit, ].contains(acl) } } @@ -198,7 +208,7 @@ private extension ExtensionAccessModifierRule { for decl in node.memberBlock.expandingIfConfigs() { let modifiers = decl.asProtocol((any WithModifiersSyntax).self)?.modifiers - let aclToken = modifiers?.accessLevelModifier?.name + let aclToken = modifiers?.accessLevelModifier()?.name let acl = ACL.from(tokenKind: aclToken?.tokenKind) if areAllACLsEqual, acl != aclTokens.last?.acl, aclTokens.isNotEmpty { areAllACLsEqual = false diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FallthroughRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FallthroughRule.swift index f8671cf27e..6b07fe9310 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FallthroughRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FallthroughRule.swift @@ -15,7 +15,7 @@ struct FallthroughRule: OptInRule { case .bar, .bar2, .bar3: something() } - """) + """), ], triggeringExamples: [ Example(""" @@ -25,7 +25,7 @@ struct FallthroughRule: OptInRule { case .bar2: something() } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FatalErrorMessageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FatalErrorMessageRule.swift index b50be5e4de..4d7af1dc8b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FatalErrorMessageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FatalErrorMessageRule.swift @@ -19,7 +19,7 @@ struct FatalErrorMessageRule: OptInRule { func foo() { fatalError(x) } - """) + """), ], triggeringExamples: [ Example(""" @@ -31,7 +31,7 @@ struct FatalErrorMessageRule: OptInRule { func foo() { ↓fatalError() } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift index 24f8afae05..892ca60313 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift @@ -19,8 +19,12 @@ struct FileNameNoSpaceRule: OptInRule, SourceKitFreeRule { return [] } - return [StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: filePath, line: 1))] + return [ + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: filePath, line: 1) + ), + ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift index fc8839f43d..fa8999122d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift @@ -35,7 +35,9 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { } // Process nested type separator - let allDeclaredTypeNames = TypeNameCollectingVisitor(viewMode: .sourceAccurate) + let allDeclaredTypeNames = TypeNameCollectingVisitor( + requireFullyQualifiedNames: configuration.requireFullyQualifiedNames + ) .walk(tree: file.syntaxTree, handler: \.names) .map { $0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator) @@ -45,40 +47,107 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { return [] } - return [StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: filePath, line: 1))] + return [ + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: filePath, line: 1) + ), + ] } } private class TypeNameCollectingVisitor: SyntaxVisitor { + /// All of a visited node's ancestor type names if that node is nested, starting with the furthest + /// ancestor and ending with the direct parent + private var ancestorNames = Stack() + + /// All of the type names found in the file private(set) var names: Set = [] - override func visitPost(_ node: ClassDeclSyntax) { - names.insert(node.name.text) + /// If true, nested types are only allowed in the file name when used by their fully-qualified name + /// (e.g. `My.Nested.Type` and not just `Type`) + private let requireFullyQualifiedNames: Bool + + init(requireFullyQualifiedNames: Bool) { + self.requireFullyQualifiedNames = requireFullyQualifiedNames + super.init(viewMode: .sourceAccurate) + } + + /// Calls `visit(name:)` using the name of the provided node + private func visit(node: some NamedDeclSyntax) -> SyntaxVisitorContinueKind { + visit(name: node.name.trimmedDescription) + } + + /// Visits a node with the provided name, storing that name as an ancestor type name to prepend to + /// any children to form their fully-qualified names + private func visit(name: String) -> SyntaxVisitorContinueKind { + let fullyQualifiedName = (ancestorNames + [name]).joined(separator: ".") + names.insert(fullyQualifiedName) + + // If the options don't require only fully-qualified names, then we will allow this node's + // name to be used by itself + if !requireFullyQualifiedNames { + names.insert(name) + } + + ancestorNames.push(name) + return .visitChildren + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + visit(node: node) + } + + override func visitPost(_: ClassDeclSyntax) { + ancestorNames.pop() + } + + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + visit(node: node) + } + + override func visitPost(_: ActorDeclSyntax) { + ancestorNames.pop() + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + visit(node: node) + } + + override func visitPost(_: StructDeclSyntax) { + ancestorNames.pop() + } + + override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind { + visit(node: node) + } + + override func visitPost(_: TypeAliasDeclSyntax) { + ancestorNames.pop() } - override func visitPost(_ node: ActorDeclSyntax) { - names.insert(node.name.text) + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + visit(node: node) } - override func visitPost(_ node: StructDeclSyntax) { - names.insert(node.name.text) + override func visitPost(_: EnumDeclSyntax) { + ancestorNames.pop() } - override func visitPost(_ node: TypeAliasDeclSyntax) { - names.insert(node.name.text) + override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + visit(node: node) } - override func visitPost(_ node: EnumDeclSyntax) { - names.insert(node.name.text) + override func visitPost(_: ProtocolDeclSyntax) { + ancestorNames.pop() } - override func visitPost(_ node: ProtocolDeclSyntax) { - names.insert(node.name.text) + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + visit(name: node.extendedType.trimmedDescription) } - override func visitPost(_ node: ExtensionDeclSyntax) { - names.insert(node.extendedType.trimmedDescription) + override func visitPost(_: ExtensionDeclSyntax) { + ancestorNames.pop() } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForWhereRule.swift index e6cc7b2a51..5292e8d287 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForWhereRule.swift @@ -90,7 +90,7 @@ struct ForWhereRule: Rule { return derivedValue != 0 } } - """, configuration: ["allow_for_as_filter": true]) + """, configuration: ["allow_for_as_filter": true]), ], triggeringExamples: [ Example(""" @@ -113,7 +113,7 @@ struct ForWhereRule: Rule { subview.removeFromSuperview() } } - """, configuration: ["allow_for_as_filter": true]) + """, configuration: ["allow_for_as_filter": true]), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceTryRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceTryRule.swift index e86f4499ab..c1387dfb37 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceTryRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceTryRule.swift @@ -15,13 +15,13 @@ struct ForceTryRule: Rule { do { try a() } catch {} - """) + """), ], triggeringExamples: [ Example(""" func a() throws {} ↓try! a() - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceUnwrappingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceUnwrappingRule.swift index 0a2baaeab8..261f36df97 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceUnwrappingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceUnwrappingRule.swift @@ -29,7 +29,7 @@ struct ForceUnwrappingRule: OptInRule { Example("func foo() -> [Int]!"), Example("func foo() -> [AnyHashable: Any]!"), Example("func foo() -> [Int]! { return [] }"), - Example("return self") + Example("return self"), ], triggeringExamples: [ Example("let url = NSURL(string: query)↓!"), @@ -62,7 +62,7 @@ struct ForceUnwrappingRule: OptInRule { Example("open var computed: String { return foo.bar↓! }"), Example("return self↓!"), Example("[1, 3, 5, 6].first { $0.isMultiple(of: 2) }↓!"), - Example("map[\"a\"]↓!↓!") + Example("map[\"a\"]↓!↓!"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FunctionDefaultParameterAtEndRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FunctionDefaultParameterAtEndRule.swift index 0663543107..a949725fbf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FunctionDefaultParameterAtEndRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FunctionDefaultParameterAtEndRule.swift @@ -2,7 +2,7 @@ import SwiftSyntax @SwiftSyntaxRule struct FunctionDefaultParameterAtEndRule: OptInRule { - var configuration = SeverityConfiguration(.warning) + var configuration = FunctionDefaultParameterAtEndConfiguration() static let description = RuleDescription( identifier: "function_default_parameter_at_end", @@ -41,12 +41,20 @@ struct FunctionDefaultParameterAtEndRule: OptInRule { """), Example(""" func expect(file: String = #file, _ expression: @autoclosure () -> (() throws -> T)) -> Expectation {} - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), + Example("func foo(bar: Int, baz: Int = 0, z: () -> Void) {}"), + Example("func foo(bar: Int, baz: Int = 0, z: () -> Void, x: Int = 0) {}"), + Example("func foo(isolation: isolated (any Actor)? = #isolation, bar: String) {}"), ], triggeringExamples: [ - Example("↓func foo(bar: Int = 0, baz: String) {}"), - Example("private ↓func foo(bar: Int = 0, baz: String) {}"), - Example("public ↓init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) {}") + Example("func foo(↓bar: Int = 0, baz: String) {}"), + Example("private func foo(↓bar: Int = 0, baz: String) {}"), + Example("public init?(↓for date: Date = Date(), coordinate: CLLocationCoordinate2D) {}"), + Example("func foo(bar: Int, ↓baz: Int = 0, z: () -> Void, x: Int) {}"), + Example( + "func foo(isolation: isolated (any Actor)? = #isolation, bar: String) {}", + configuration: ["ignore_first_isolation_inheritance_parameter": false] + ), ] ) } @@ -54,76 +62,68 @@ struct FunctionDefaultParameterAtEndRule: OptInRule { private extension FunctionDefaultParameterAtEndRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: FunctionDeclSyntax) { - guard !node.modifiers.contains(keyword: .override), node.signature.containsViolation else { - return + if !node.modifiers.contains(keyword: .override) { + collectViolations(for: node.signature) } - - violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia) } override func visitPost(_ node: InitializerDeclSyntax) { - guard !node.modifiers.contains(keyword: .override), node.signature.containsViolation else { - return + if !node.modifiers.contains(keyword: .override) { + collectViolations(for: node.signature) } - - violations.append(node.initKeyword.positionAfterSkippingLeadingTrivia) - } - } -} - -private extension FunctionSignatureSyntax { - var containsViolation: Bool { - let params = parameterClause.parameters.filter { param in - !param.isClosure } - guard params.isNotEmpty else { - return false - } - - let defaultParams = params.filter { param in - param.defaultValue != nil - } - guard defaultParams.isNotEmpty else { - return false - } - - let lastParameters = params.suffix(defaultParams.count) - let lastParametersWithDefaultValue = lastParameters.filter { param in - param.defaultValue != nil + private func collectViolations(for signature: FunctionSignatureSyntax) { + let numberOfParameters = signature.parameterClause.parameters.count + if numberOfParameters < 2 { + return + } + var previousWithDefault = true + for (index, param) in signature.parameterClause.parameters.reversed().enumerated() { + if param.isClosure { + continue + } + let hasDefault = param.defaultValue != nil + if !previousWithDefault, hasDefault { + if index + 1 == numberOfParameters, + param.isInheritedIsolation, + configuration.ignoreFirstIsolationInheritanceParameter { + break // It's the last element anyway. + } + violations.append(param.positionAfterSkippingLeadingTrivia) + } + previousWithDefault = hasDefault + } } - - return lastParameters.count != lastParametersWithDefaultValue.count } } private extension FunctionParameterSyntax { var isClosure: Bool { - if isEscaping || type.is(FunctionTypeSyntax.self) { - return true - } - - if let optionalType = type.as(OptionalTypeSyntax.self), - let tuple = optionalType.wrappedType.as(TupleTypeSyntax.self) { - return tuple.elements.onlyElement?.type.as(FunctionTypeSyntax.self) != nil - } - - if let tuple = type.as(TupleTypeSyntax.self) { - return tuple.elements.onlyElement?.type.as(FunctionTypeSyntax.self) != nil - } + isEscaping || type.isFunctionType + } - if let attrType = type.as(AttributedTypeSyntax.self) { - return attrType.baseType.is(FunctionTypeSyntax.self) - } + var isEscaping: Bool { + type.as(AttributedTypeSyntax.self)?.attributes.contains(attributeNamed: "escaping") == true + } - return false + var isInheritedIsolation: Bool { + defaultValue?.value.as(MacroExpansionExprSyntax.self)?.macroName.text == "isolation" } +} - var isEscaping: Bool { - guard let attrType = type.as(AttributedTypeSyntax.self) else { - return false +private extension TypeSyntax { + var isFunctionType: Bool { + if `is`(FunctionTypeSyntax.self) { + true + } else if let optionalType = `as`(OptionalTypeSyntax.self) { + optionalType.wrappedType.isFunctionType + } else if let tupleType = `as`(TupleTypeSyntax.self) { + tupleType.elements.onlyElement?.type.isFunctionType == true + } else if let attributedType = `as`(AttributedTypeSyntax.self) { + attributedType.baseType.isFunctionType + } else { + false } - - return attrType.attributes.contains(attributeNamed: "escaping") } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift index 0864c43827..91502f3403 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift @@ -28,7 +28,7 @@ struct GenericTypeNameRule: Rule { Example("func configureWith(data: Either)"), Example("typealias StringDictionary = Dictionary"), Example("typealias BackwardTriple = (T3, T2, T1)"), - Example("typealias DictionaryOfStrings = Dictionary") + Example("typealias DictionaryOfStrings = Dictionary"), ], triggeringExamples: [ Example("func foo<↓T_Foo>() {}"), @@ -37,14 +37,14 @@ struct GenericTypeNameRule: Rule { Example("func foo<↓type>() {}"), Example("typealias StringDictionary<↓T_Foo> = Dictionary"), Example("typealias BackwardTriple = (T3, T2_Bar, T1)"), - Example("typealias DictionaryOfStrings<↓T_Foo: Hashable> = Dictionary") + Example("typealias DictionaryOfStrings<↓T_Foo: Hashable> = Dictionary"), ] + ["class", "struct", "enum"].flatMap { type -> [Example] in - return [ + [ Example("\(type) Foo<↓T_Foo> {}"), Example("\(type) Foo {}"), Example("\(type) Foo<↓T_Foo, ↓U_Foo> {}"), Example("\(type) Foo<↓\(String(repeating: "T", count: 21))> {}"), - Example("\(type) Foo<↓type> {}") + Example("\(type) Foo<↓type> {}"), ] } ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ImplicitlyUnwrappedOptionalRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ImplicitlyUnwrappedOptionalRule.swift index 8bbad1c1c4..6b64ce7901 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ImplicitlyUnwrappedOptionalRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ImplicitlyUnwrappedOptionalRule.swift @@ -21,7 +21,7 @@ struct ImplicitlyUnwrappedOptionalRule: OptInRule { @IBOutlet weak var bar: SomeObject! } - """, configuration: ["mode": "all_except_iboutlets"], excludeFromDocumentation: true) + """, configuration: ["mode": "all_except_iboutlets"], excludeFromDocumentation: true), ], triggeringExamples: [ Example("let label: ↓UILabel!"), @@ -38,7 +38,7 @@ struct ImplicitlyUnwrappedOptionalRule: OptInRule { class MyClass { weak var bar: ↓SomeObject! } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/IsDisjointRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/IsDisjointRule.swift index 696c31fbe0..63709b730e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/IsDisjointRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/IsDisjointRule.swift @@ -13,11 +13,11 @@ struct IsDisjointRule: Rule { Example("_ = Set(syntaxKinds).isDisjoint(with: commentAndStringKindsSet)"), Example("let isObjc = !objcAttributes.isDisjoint(with: dictionary.enclosedSwiftAttributes)"), Example("_ = Set(syntaxKinds).intersection(commentAndStringKindsSet)"), - Example("_ = !objcAttributes.intersection(dictionary.enclosedSwiftAttributes)") + Example("_ = !objcAttributes.intersection(dictionary.enclosedSwiftAttributes)"), ], triggeringExamples: [ Example("_ = Set(syntaxKinds).↓intersection(commentAndStringKindsSet).isEmpty"), - Example("let isObjc = !objcAttributes.↓intersection(dictionary.enclosedSwiftAttributes).isEmpty") + Example("let isObjc = !objcAttributes.↓intersection(dictionary.enclosedSwiftAttributes).isEmpty"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/JoinedDefaultParameterRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/JoinedDefaultParameterRule.swift index d688abea81..d5e0a435c9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/JoinedDefaultParameterRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/JoinedDefaultParameterRule.swift @@ -12,7 +12,7 @@ struct JoinedDefaultParameterRule: OptInRule { nonTriggeringExamples: [ Example("let foo = bar.joined()"), Example("let foo = bar.joined(separator: \",\")"), - Example("let foo = bar.joined(separator: toto)") + Example("let foo = bar.joined(separator: toto)"), ], triggeringExamples: [ Example("let foo = bar.joined(↓separator: \"\")"), @@ -24,7 +24,7 @@ struct JoinedDefaultParameterRule: OptInRule { func foo() -> String { return ["1", "2"].joined(↓separator: "") } - """) + """), ], corrections: [ Example("let foo = bar.joined(↓separator: \"\")"): Example("let foo = bar.joined()"), @@ -33,7 +33,7 @@ struct JoinedDefaultParameterRule: OptInRule { Example("func foo() -> String {\n return [\"1\", \"2\"].joined(↓separator: \"\")\n}"): Example("func foo() -> String {\n return [\"1\", \"2\"].joined()\n}"), Example("class C {\n#if true\nlet foo = bar.joined(↓separator: \"\")\n#endif\n}"): - Example("class C {\n#if true\nlet foo = bar.joined()\n#endif\n}") + Example("class C {\n#if true\nlet foo = bar.joined()\n#endif\n}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyCGGeometryFunctionsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyCGGeometryFunctionsRule.swift index 69fa8e97c0..2cfecead4f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyCGGeometryFunctionsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyCGGeometryFunctionsRule.swift @@ -27,7 +27,7 @@ struct LegacyCGGeometryFunctionsRule: SwiftSyntaxCorrectableRule { // "rect.divide(atDistance: 10.2, fromEdge: edge)", No correction available for divide Example("rect1.contains(rect2)"), Example("rect.contains(point)"), - Example("rect1.intersects(rect2)") + Example("rect1.intersects(rect2)"), ], triggeringExamples: [ Example("↓CGRectGetWidth(rect)"), @@ -49,7 +49,7 @@ struct LegacyCGGeometryFunctionsRule: SwiftSyntaxCorrectableRule { Example("↓CGRectIntersection(rect1, rect2)"), Example("↓CGRectContainsRect(rect1, rect2)"), Example("↓CGRectContainsPoint(rect, point)"), - Example("↓CGRectIntersectsRect(rect1, rect2)") + Example("↓CGRectIntersectsRect(rect1, rect2)"), ], corrections: [ Example("↓CGRectGetWidth( rect )"): Example("rect.width"), @@ -73,7 +73,7 @@ struct LegacyCGGeometryFunctionsRule: SwiftSyntaxCorrectableRule { Example("↓CGRectContainsPoint(rect ,point)"): Example("rect.contains(point)"), Example("↓CGRectIntersectsRect( rect1,rect2 )"): Example("rect1.intersects(rect2)"), Example("↓CGRectIntersectsRect(rect1, rect2 )\n↓CGRectGetWidth(rect )"): - Example("rect1.intersects(rect2)\nrect.width") + Example("rect1.intersects(rect2)\nrect.width"), ] ) @@ -97,7 +97,7 @@ struct LegacyCGGeometryFunctionsRule: SwiftSyntaxCorrectableRule { "CGRectContainsRect": .function(name: "contains", argumentLabels: [""]), "CGRectContainsPoint": .function(name: "contains", argumentLabels: [""]), "CGRectIntersectsRect": .function(name: "intersects", argumentLabels: [""]), - "CGRectIntersection": .function(name: "intersection", argumentLabels: [""]) + "CGRectIntersection": .function(name: "intersection", argumentLabels: [""]), ] func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstantRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstantRuleExamples.swift index 81676f5557..b7267bae49 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstantRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstantRuleExamples.swift @@ -9,7 +9,7 @@ internal struct LegacyConstantRuleExamples { Example("NSSize.zero"), Example("CGRect.null"), Example("CGFloat.pi"), - Example("Float.pi") + Example("Float.pi"), ] static let triggeringExamples: [Example] = [ @@ -22,7 +22,7 @@ internal struct LegacyConstantRuleExamples { Example("↓NSZeroSize"), Example("↓CGRectNull"), Example("↓CGFloat(M_PI)"), - Example("↓Float(M_PI)") + Example("↓Float(M_PI)"), ] static let corrections: [Example: Example] = [ @@ -37,7 +37,7 @@ internal struct LegacyConstantRuleExamples { Example("↓CGRectInfinite\n↓CGRectNull"): Example("CGRect.infinite\nCGRect.null"), Example("↓CGFloat(M_PI)"): Example("CGFloat.pi"), Example("↓Float(M_PI)"): Example("Float.pi"), - Example("↓CGFloat(M_PI)\n↓Float(M_PI)"): Example("CGFloat.pi\nFloat.pi") + Example("↓CGFloat(M_PI)\n↓Float(M_PI)"): Example("CGFloat.pi\nFloat.pi"), ] static let patterns = [ @@ -48,6 +48,6 @@ internal struct LegacyConstantRuleExamples { "NSZeroPoint": "NSPoint.zero", "NSZeroRect": "NSRect.zero", "NSZeroSize": "NSSize.zero", - "CGRectNull": "CGRect.null" + "CGRectNull": "CGRect.null", ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstructorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstructorRule.swift index 3a2aabaa37..c61fb0bdeb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstructorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyConstructorRule.swift @@ -31,7 +31,7 @@ struct LegacyConstructorRule: Rule { Example("NSEdgeInsets(top: 0, left: 0, bottom: 10, right: 10)"), Example("NSEdgeInsets(top: aTop, left: aLeft, bottom: aBottom, right: aRight)"), Example("UIOffset(horizontal: 0, vertical: 10)"), - Example("UIOffset(horizontal: horizontal, vertical: vertical)") + Example("UIOffset(horizontal: horizontal, vertical: vertical)"), ], triggeringExamples: [ Example("↓CGPointMake(10, 10)"), @@ -57,7 +57,7 @@ struct LegacyConstructorRule: Rule { Example("↓NSEdgeInsetsMake(top, left, bottom, right)"), Example("↓CGVectorMake(10, 10)\n↓NSMakeRange(10, 1)"), Example("↓UIOffsetMake(0, 10)"), - Example("↓UIOffsetMake(horizontal, vertical)") + Example("↓UIOffsetMake(horizontal, vertical)"), ], corrections: [ Example("↓CGPointMake(10, 10)"): Example("CGPoint(x: 10, y: 10)"), @@ -95,33 +95,37 @@ struct LegacyConstructorRule: Rule { Example("↓CGPointMake(calculateX(), 10)"): Example("CGPoint(x: calculateX(), y: 10)"), Example("↓UIOffsetMake(0, 10)"): Example("UIOffset(horizontal: 0, vertical: 10)"), Example("↓UIOffsetMake(horizontal, vertical)"): - Example("UIOffset(horizontal: horizontal, vertical: vertical)") + Example("UIOffset(horizontal: horizontal, vertical: vertical)"), ] ) - private static let constructorsToArguments = ["CGRectMake": ["x", "y", "width", "height"], - "CGPointMake": ["x", "y"], - "CGSizeMake": ["width", "height"], - "CGVectorMake": ["dx", "dy"], - "NSMakePoint": ["x", "y"], - "NSMakeSize": ["width", "height"], - "NSMakeRect": ["x", "y", "width", "height"], - "NSMakeRange": ["location", "length"], - "UIEdgeInsetsMake": ["top", "left", "bottom", "right"], - "NSEdgeInsetsMake": ["top", "left", "bottom", "right"], - "UIOffsetMake": ["horizontal", "vertical"]] + private static let constructorsToArguments = [ + "CGRectMake": ["x", "y", "width", "height"], + "CGPointMake": ["x", "y"], + "CGSizeMake": ["width", "height"], + "CGVectorMake": ["dx", "dy"], + "NSMakePoint": ["x", "y"], + "NSMakeSize": ["width", "height"], + "NSMakeRect": ["x", "y", "width", "height"], + "NSMakeRange": ["location", "length"], + "UIEdgeInsetsMake": ["top", "left", "bottom", "right"], + "NSEdgeInsetsMake": ["top", "left", "bottom", "right"], + "UIOffsetMake": ["horizontal", "vertical"], + ] - private static let constructorsToCorrectedNames = ["CGRectMake": "CGRect", - "CGPointMake": "CGPoint", - "CGSizeMake": "CGSize", - "CGVectorMake": "CGVector", - "NSMakePoint": "NSPoint", - "NSMakeSize": "NSSize", - "NSMakeRect": "NSRect", - "NSMakeRange": "NSRange", - "UIEdgeInsetsMake": "UIEdgeInsets", - "NSEdgeInsetsMake": "NSEdgeInsets", - "UIOffsetMake": "UIOffset"] + private static let constructorsToCorrectedNames = [ + "CGRectMake": "CGRect", + "CGPointMake": "CGPoint", + "CGSizeMake": "CGSize", + "CGVectorMake": "CGVector", + "NSMakePoint": "NSPoint", + "NSMakeSize": "NSSize", + "NSMakeRect": "NSRect", + "NSMakeRange": "NSRange", + "UIEdgeInsetsMake": "UIEdgeInsets", + "NSEdgeInsetsMake": "NSEdgeInsets", + "UIOffsetMake": "UIOffset", + ] } private extension LegacyConstructorRule { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyHashingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyHashingRule.swift index 3d64c4ca1f..9ef7c6db57 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyHashingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyHashingRule.swift @@ -50,7 +50,7 @@ struct LegacyHashingRule: Rule { set { bar = newValue } } } - """) + """), ], triggeringExamples: [ Example(""" @@ -70,7 +70,7 @@ struct LegacyHashingRule: Rule { return bar } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyMultipleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyMultipleRule.swift index ea7b51a000..971647780e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyMultipleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyMultipleRule.swift @@ -22,7 +22,7 @@ struct LegacyMultipleRule: OptInRule { let constant = 56 let secret = value % constant == 5 """), - Example("let secretValue = (value % 3) + 2") + Example("let secretValue = (value % 3) + 2"), ], triggeringExamples: [ Example("cell.contentView.backgroundColor = indexPath.row ↓% 2 == 0 ? .gray : .white"), @@ -34,7 +34,7 @@ struct LegacyMultipleRule: OptInRule { Example(""" let constant = 56 let isMultiple = value ↓% constant == 0 - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyNSGeometryFunctionsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyNSGeometryFunctionsRule.swift index 4dcf209e85..e73654907c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyNSGeometryFunctionsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyNSGeometryFunctionsRule.swift @@ -24,7 +24,7 @@ struct LegacyNSGeometryFunctionsRule: SwiftSyntaxCorrectableRule { // "rect.divide(atDistance: 10.2, fromEdge: edge)", No correction available for divide Example("rect1.contains(rect2)"), Example("rect.contains(point)"), - Example("rect1.intersects(rect2)") + Example("rect1.intersects(rect2)"), ], triggeringExamples: [ Example("↓NSWidth(rect)"), @@ -47,7 +47,7 @@ struct LegacyNSGeometryFunctionsRule: SwiftSyntaxCorrectableRule { Example("↓NSIntersectionRect(rect1, rect2)"), Example("↓NSContainsRect(rect1, rect2)"), Example("↓NSPointInRect(rect, point)"), - Example("↓NSIntersectsRect(rect1, rect2)") + Example("↓NSIntersectsRect(rect1, rect2)"), ], corrections: [ Example("↓NSWidth( rect )"): Example("rect.width"), @@ -72,7 +72,7 @@ struct LegacyNSGeometryFunctionsRule: SwiftSyntaxCorrectableRule { Example("↓NSIntersectsRect( rect1,rect2 )"): Example("rect1.intersects(rect2)"), Example("↓NSIntersectsRect(rect1, rect2 )\n↓NSWidth(rect )"): Example("rect1.intersects(rect2)\nrect.width"), - Example("↓NSIntersectionRect(rect1, rect2)"): Example("rect1.intersection(rect2)") + Example("↓NSIntersectionRect(rect1, rect2)"): Example("rect1.intersection(rect2)"), ] ) @@ -97,7 +97,7 @@ struct LegacyNSGeometryFunctionsRule: SwiftSyntaxCorrectableRule { "NSContainsRect": .function(name: "contains", argumentLabels: [""]), "NSIntersectsRect": .function(name: "intersects", argumentLabels: [""]), "NSIntersectionRect": .function(name: "intersection", argumentLabels: [""]), - "NSPointInRect": .function(name: "contains", argumentLabels: [""], reversed: true) + "NSPointInRect": .function(name: "contains", argumentLabels: [""], reversed: true), ] func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyObjcTypeRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyObjcTypeRule.swift index 21b8a792ab..82da70f894 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyObjcTypeRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyObjcTypeRule.swift @@ -25,7 +25,7 @@ private let legacyObjcTypes = [ "NSURLComponents", "NSURLQueryItem", "NSURLRequest", - "NSUUID" + "NSUUID", ] @SwiftSyntaxRule @@ -43,7 +43,7 @@ struct LegacyObjcTypeRule: OptInRule { Example("var formatter: NSDataDetector"), Example("var className: String = NSStringFromClass(MyClass.self)"), Example("_ = URLRequest.CachePolicy.reloadIgnoringLocalCacheData"), - Example(#"_ = Notification.Name("com.apple.Music.playerInfo")"#) + Example(#"_ = Notification.Name("com.apple.Music.playerInfo")"#), ], triggeringExamples: [ Example("var array = ↓NSArray()"), @@ -63,7 +63,7 @@ struct LegacyObjcTypeRule: OptInRule { return Foundation.Notification.Name("org.wordpress.reachability.changed") } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyRandomRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyRandomRule.swift index 5b0686589b..d20cb859f7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyRandomRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/LegacyRandomRule.swift @@ -12,12 +12,12 @@ struct LegacyRandomRule: Rule { nonTriggeringExamples: [ Example("Int.random(in: 0..<10)"), Example("Double.random(in: 8.6...111.34)"), - Example("Float.random(in: 0 ..< 1)") + Example("Float.random(in: 0 ..< 1)"), ], triggeringExamples: [ Example("↓arc4random()"), Example("↓arc4random_uniform(83)"), - Example("↓drand48()") + Example("↓drand48()"), ] ) } @@ -27,7 +27,7 @@ private extension LegacyRandomRule { private static let legacyRandomFunctions: Set = [ "arc4random", "arc4random_uniform", - "drand48" + "drand48", ] override func visitPost(_ node: FunctionCallExprSyntax) { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NimbleOperatorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NimbleOperatorRule.swift index 884ab0011d..093c569751 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NimbleOperatorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NimbleOperatorRule.swift @@ -29,7 +29,7 @@ struct NimbleOperatorRule: OptInRule { expect(value).to(equal(expectedValue), description: "Failed") return Bar(value: ()) } - """) + """), ], triggeringExamples: [ Example("↓expect(seagull.squawk).toNot(equal(\"Hi\"))"), @@ -45,7 +45,7 @@ struct NimbleOperatorRule: OptInRule { Example("↓expect(success).to(beFalse())"), Example("↓expect(value).to(beNil())"), Example("↓expect(value).toNot(beNil())"), - Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))") + Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))"), ], corrections: [ Example("↓expect(seagull.squawk).toNot(equal(\"Hi\"))"): Example("expect(seagull.squawk) != \"Hi\""), @@ -65,7 +65,7 @@ struct NimbleOperatorRule: OptInRule { Example("↓expect(success).toNot(beTrue())"): Example("expect(success) != true"), Example("↓expect(value).to(beNil())"): Example("expect(value) == nil"), Example("↓expect(value).toNot(beNil())"): Example("expect(value) != nil"), - Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))"): Example("expect(10) > 2\n expect(10) > 2") + Example("expect(10) > 2\n ↓expect(10).to(beGreaterThan(2))"): Example("expect(10) > 2\n expect(10) > 2"), ] ) } @@ -92,10 +92,10 @@ private extension NimbleOperatorRule { correctionPositions.append(node.positionAfterSkippingLeadingTrivia) let elements = ExprListSyntax([ - expectation.baseExpr.with(\.trailingTrivia, .space).cast(ExprSyntax.self), - operatorExpr.with(\.trailingTrivia, .space).cast(ExprSyntax.self), - expectedValueExpr.with(\.trailingTrivia, node.trailingTrivia) - ]) + expectation.baseExpr.with(\.trailingTrivia, .space), + operatorExpr.with(\.trailingTrivia, .space), + expectedValueExpr.with(\.trailingTrivia, node.trailingTrivia), + ].map(ExprSyntax.init)) return super.visit(SequenceExprSyntax(elements: elements)) } } @@ -111,7 +111,7 @@ private extension NimbleOperatorRule { "beLessThanOrEqualTo": (to: "<=", toNot: nil, .withArguments), "beTrue": (to: "==", toNot: "!=", .nullary(analogueValue: BooleanLiteralExprSyntax(booleanLiteral: true))), "beFalse": (to: "==", toNot: "!=", .nullary(analogueValue: BooleanLiteralExprSyntax(booleanLiteral: false))), - "beNil": (to: "==", toNot: "!=", .nullary(analogueValue: NilLiteralExprSyntax(nilKeyword: .keyword(.nil)))) + "beNil": (to: "==", toNot: "!=", .nullary(analogueValue: NilLiteralExprSyntax(nilKeyword: .keyword(.nil)))), ] static func predicateDescription(for node: FunctionCallExprSyntax) -> PredicateDescription? { @@ -174,7 +174,7 @@ private struct Expectation { case .withArguments: expected case .nullary(let analogueValue): - analogueValue.cast(ExprSyntax.self) + ExprSyntax(analogueValue) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoEmptyBlockRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoEmptyBlockRule.swift new file mode 100644 index 0000000000..3691a708b6 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoEmptyBlockRule.swift @@ -0,0 +1,199 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule +struct NoEmptyBlockRule: OptInRule { + var configuration = NoEmptyBlockConfiguration() + + static let description = RuleDescription( + identifier: "no_empty_block", + name: "No Empty Block", + description: "Code blocks should contain at least one statement or comment", + kind: .idiomatic, + nonTriggeringExamples: [ + Example(""" + func f() { + /* do something */ + } + + var flag = true { + willSet { /* do something */ } + } + """), + + Example(""" + class Apple { + init() { /* do something */ } + + deinit { /* do something */ } + } + """), + + Example(""" + for _ in 0..<10 { /* do something */ } + + do { + /* do something */ + } catch { + /* do something */ + } + + func f() { + defer { + /* do something */ + } + print("other code") + } + + if flag { + /* do something */ + } else { + /* do something */ + } + + repeat { /* do something */ } while (flag) + + while i < 10 { /* do something */ } + """), + + Example(""" + func f() {} + + var flag = true { + willSet {} + } + """, configuration: ["disabled_block_types": ["function_bodies"]]), + + Example(""" + class Apple { + init() {} + + deinit {} + } + """, configuration: ["disabled_block_types": ["initializer_bodies"]]), + + Example(""" + for _ in 0..<10 {} + + do { + } catch { + } + + func f() { + defer {} + print("other code") + } + + if flag { + } else { + } + + repeat {} while (flag) + + while i < 10 {} + """, configuration: ["disabled_block_types": ["statement_blocks"]]), + Example(""" + f { _ in /* comment */ } + f { _ in // comment + } + f { _ in + // comment + } + """), + Example(""" + f {} + {}() + """, configuration: ["disabled_block_types": ["closure_blocks"]]), + ], + triggeringExamples: [ + Example(""" + func f() ↓{} + + var flag = true { + willSet ↓{} + } + """), + + Example(""" + class Apple { + init() ↓{} + + deinit ↓{} + } + """), + + Example(""" + for _ in 0..<10 ↓{} + + do ↓{ + } catch ↓{ + } + + func f() { + defer ↓{} + print("other code") + } + + if flag ↓{ + } else ↓{ + } + + repeat ↓{} while (flag) + + while i < 10 ↓{} + """), + Example(""" + f ↓{} + """), + Example(""" + Button ↓{} label: ↓{} + """), + ] + ) +} + +private extension NoEmptyBlockRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: CodeBlockSyntax) { + if let codeBlockType = node.codeBlockType, configuration.enabledBlockTypes.contains(codeBlockType) { + validate(node: node) + } + } + + override func visitPost(_ node: ClosureExprSyntax) { + if configuration.enabledBlockTypes.contains(.closureBlocks), + node.signature?.inKeyword.trailingTrivia.containsComments != true { + validate(node: node) + } + } + + func validate(node: some BracedSyntax & WithStatementsSyntax) { + guard node.statements.isEmpty, + !node.leftBrace.trailingTrivia.containsComments, + !node.rightBrace.leadingTrivia.containsComments else { + return + } + violations.append(node.leftBrace.positionAfterSkippingLeadingTrivia) + } + } +} + +private extension CodeBlockSyntax { + var codeBlockType: NoEmptyBlockConfiguration.CodeBlockType? { + switch parent?.kind { + case .functionDecl, .accessorDecl: + .functionBodies + case .initializerDecl, .deinitializerDecl: + .initializerBodies + case .forStmt, .doStmt, .whileStmt, .repeatStmt, .ifExpr, .catchClause, .deferStmt: + .statementBlocks + case .closureExpr: + .closureBlocks + case .guardStmt: + // No need to handle this case since Empty Block of `guard` is compile error. + nil + default: + nil + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoExtensionAccessModifierRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoExtensionAccessModifierRule.swift index 94234273c0..e6a8ae4680 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoExtensionAccessModifierRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoExtensionAccessModifierRule.swift @@ -11,14 +11,14 @@ struct NoExtensionAccessModifierRule: OptInRule { kind: .idiomatic, nonTriggeringExamples: [ Example("extension String {}"), - Example("\n\n extension String {}") + Example("\n\n extension String {}"), ], triggeringExamples: [ Example("↓private extension String {}"), Example("↓public \n extension String {}"), Example("↓open extension String {}"), Example("↓internal extension String {}"), - Example("↓fileprivate extension String {}") + Example("↓fileprivate extension String {}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoFallthroughOnlyRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoFallthroughOnlyRuleExamples.swift index ccb04a8ede..81b1b0637e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoFallthroughOnlyRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoFallthroughOnlyRuleExamples.swift @@ -78,7 +78,7 @@ internal struct NoFallthroughOnlyRuleExamples { @unknown default: print("it's not a") } - """) + """), ] static let triggeringExamples = [ @@ -154,6 +154,6 @@ internal struct NoFallthroughOnlyRuleExamples { case "abc": let two = 2 } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift index 9b0b45ac03..60c5ebcdcb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift @@ -12,20 +12,20 @@ struct NoGroupingExtensionRule: OptInRule { nonTriggeringExamples: [ Example("protocol Food {}\nextension Food {}"), Example("class Apples {}\nextension Oranges {}"), - Example("class Box {}\nextension Box where T: Vegetable {}") + Example("class Box {}\nextension Box where T: Vegetable {}"), ], triggeringExamples: [ Example("enum Fruit {}\n↓extension Fruit {}"), Example("↓extension Tea: Error {}\nstruct Tea {}"), Example("class Ham { class Spam {}}\n↓extension Ham.Spam {}"), - Example("extension External { struct Gotcha {}}\n↓extension External.Gotcha {}") + Example("extension External { struct Gotcha {}}\n↓extension External.Gotcha {}"), ] ) func validate(file: SwiftLintFile) -> [StyleViolation] { - return Visitor(configuration: configuration, file: file) + Visitor(configuration: configuration, file: file) .walk(tree: file.syntaxTree) { visitor in - return visitor.extensionDeclarations.compactMap { decl in + visitor.extensionDeclarations.compactMap { decl in guard visitor.typeDeclarations.contains(decl.name) else { return nil } @@ -55,7 +55,7 @@ private extension NoGroupingExtensionRule { FunctionDeclSyntax.self, VariableDeclSyntax.self, InitializerDeclSyntax.self, - SubscriptDeclSyntax.self + SubscriptDeclSyntax.self, ] } @@ -64,7 +64,7 @@ private extension NoGroupingExtensionRule { return .visitChildren } - override func visitPost(_ node: ActorDeclSyntax) { + override func visitPost(_: ActorDeclSyntax) { typeScope.removeLast() } @@ -73,7 +73,7 @@ private extension NoGroupingExtensionRule { return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { + override func visitPost(_: ClassDeclSyntax) { typeScope.removeLast() } @@ -82,7 +82,7 @@ private extension NoGroupingExtensionRule { return .visitChildren } - override func visitPost(_ node: EnumDeclSyntax) { + override func visitPost(_: EnumDeclSyntax) { typeScope.removeLast() } @@ -91,7 +91,7 @@ private extension NoGroupingExtensionRule { return .visitChildren } - override func visitPost(_ node: StructDeclSyntax) { + override func visitPost(_: StructDeclSyntax) { typeScope.removeLast() } @@ -110,7 +110,7 @@ private extension NoGroupingExtensionRule { return .visitChildren } - override func visitPost(_ node: ExtensionDeclSyntax) { + override func visitPost(_: ExtensionDeclSyntax) { typeScope.removeLast() } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoMagicNumbersRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoMagicNumbersRule.swift index 8756548670..85cad443b6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoMagicNumbersRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoMagicNumbersRule.swift @@ -80,7 +80,12 @@ struct NoMagicNumbersRule: OptInRule { Example("let range = 12..."), Example("let (lowerBound, upperBound) = (400, 599)"), Example("let a = (5, 10)"), - Example("let notFound = (statusCode: 404, description: \"Not Found\", isError: true)") + Example("let notFound = (statusCode: 404, description: \"Not Found\", isError: true)"), + Example(""" + #Preview { + ContentView(value: 5) + } + """), ], triggeringExamples: [ Example("foo(↓321)"), @@ -106,7 +111,12 @@ struct NoMagicNumbersRule: OptInRule { } """), Example("let imageHeight = (width - ↓24)"), - Example("return (↓5, ↓10, ↓15)") + Example("return (↓5, ↓10, ↓15)"), + Example(""" + #ExampleMacro { + ContentView(value: ↓5) + } + """), ] ) } @@ -121,6 +131,10 @@ private extension NoMagicNumbersRule { node.isSimpleTupleAssignment ? .skipChildren : .visitChildren } + override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { + node.macroName.text == "Preview" ? .skipChildren : .visitChildren + } + override func visitPost(_ node: ClassDeclSyntax) { let className = node.name.text if node.isXCTestCase(configuration.testParentClasses) { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift index 3986128c6b..4cb5a9eaa2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift @@ -17,7 +17,7 @@ struct ObjectLiteralRule: OptInRule { Example("let color = UIColor(red: value, green: value, blue: value, alpha: 1)"), Example("let image = NSImage(named: aVariable)"), Example("let image = NSImage(named: \"interpolated \\(variable)\")"), - Example("let color = NSColor(red: value, green: value, blue: value, alpha: 1)") + Example("let color = NSColor(red: value, green: value, blue: value, alpha: 1)"), ], triggeringExamples: ["", ".init"].flatMap { (method: String) -> [Example] in ["UI", "NS"].flatMap { (prefix: String) -> [Example] in @@ -26,7 +26,7 @@ struct ObjectLiteralRule: OptInRule { Example("let color = ↓\(prefix)Color\(method)(red: 0.3, green: 0.3, blue: 0.3, alpha: 1)"), // swiftlint:disable:next line_length Example("let color = ↓\(prefix)Color\(method)(red: 100 / 255.0, green: 50 / 255.0, blue: 0, alpha: 1)"), - Example("let color = ↓\(prefix)Color\(method)(white: 0.5, alpha: 1)") + Example("let color = ↓\(prefix)Color\(method)(white: 0.5, alpha: 1)"), ] } } @@ -66,16 +66,14 @@ private extension ObjectLiteralRule { return false } - return node.arguments.allSatisfy { elem in - elem.expression.canBeExpressedAsColorLiteralParams - } + return node.arguments.allSatisfy(\.expression.canBeExpressedAsColorLiteralParams) } private func inits(forClasses names: [String]) -> [String] { - return names.flatMap { name in + names.flatMap { name in [ name, - name + ".init" + name + ".init", ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift similarity index 92% rename from Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift rename to Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index 82b918e008..7094c9b82f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -1,7 +1,7 @@ import SwiftSyntax @SwiftSyntaxRule -struct OneDelarationPerFileRule: OptInRule { +struct OneDeclarationPerFileRule: OptInRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -11,7 +11,7 @@ struct OneDelarationPerFileRule: OptInRule { kind: .idiomatic, nonTriggeringExamples: [ Example(""" - class Foo {} + actor Foo {} """), Example(""" class Foo {} @@ -21,7 +21,7 @@ struct OneDelarationPerFileRule: OptInRule { struct S { struct N {} } - """) + """), ], triggeringExamples: [ Example(""" @@ -35,12 +35,12 @@ struct OneDelarationPerFileRule: OptInRule { Example(""" struct Foo {} ↓struct Bar {} - """) + """), ] ) } -private extension OneDelarationPerFileRule { +private extension OneDeclarationPerFileRule { final class Visitor: ViolationsSyntaxVisitor { private var declarationVisited = false override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PatternMatchingKeywordsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PatternMatchingKeywordsRule.swift index fe11d6f614..3bf67d9400 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PatternMatchingKeywordsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PatternMatchingKeywordsRule.swift @@ -21,7 +21,7 @@ struct PatternMatchingKeywordsRule: OptInRule { Example("case var (x, y)"), Example("case .foo(var x)"), Example("case var .foo(x, y)"), - Example("case (y, let x, z)") + Example("case (y, let x, z)"), ].map(wrapInSwitch), triggeringExamples: [ Example("case (↓let x, ↓let y)"), @@ -34,7 +34,7 @@ struct PatternMatchingKeywordsRule: OptInRule { Example("case (.yamlParsing(↓let x), .yamlParsing(↓let y))"), Example("case (↓var x, ↓var y)"), Example("case .foo(↓var x, ↓var y)"), - Example("case (.yamlParsing(↓var x), .yamlParsing(↓var y))") + Example("case (.yamlParsing(↓var x), .yamlParsing(↓var y))"), ].map(wrapInSwitch) ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferKeyPathRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferKeyPathRule.swift new file mode 100644 index 0000000000..6f3c2819f7 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferKeyPathRule.swift @@ -0,0 +1,246 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule(explicitRewriter: true) +struct PreferKeyPathRule: OptInRule { + var configuration = PreferKeyPathConfiguration() + + private static let extendedMode = ["restrict_to_standard_functions": false] + + static let description = RuleDescription( + identifier: "prefer_key_path", + name: "Prefer Key Path", + description: "Use a key path argument instead of a closure with property access", + kind: .idiomatic, + minSwiftVersion: .fiveDotTwo, + nonTriggeringExamples: [ + Example("f {}"), + Example("f { $0 }"), + Example("f { $0.a }"), + Example("let f = { $0.a }(b)"), + Example("f {}", configuration: extendedMode), + Example("f() { g() }", configuration: extendedMode), + Example("f { a.b.c }", configuration: extendedMode), + Example("f { a, b in a.b }", configuration: extendedMode), + Example("f { (a, b) in a.b }", configuration: extendedMode), + Example("f { $0.a } g: { $0.b }", configuration: extendedMode), + Example("[1, 2, 3].reduce(1) { $0 + $1 }", configuration: extendedMode), + Example("f.map(1) { $0.a }"), + Example("f.filter({ $0.a }, x)"), + Example("#Predicate { $0.a }"), + Example("let transform: (Int) -> Int = nil ?? { $0.a }"), + ], + triggeringExamples: [ + Example("f.map ↓{ $0.a }"), + Example("f.filter ↓{ $0.a }"), + Example("f.first ↓{ $0.a }"), + Example("f.contains ↓{ $0.a }"), + Example("f.contains(where: ↓{ $0.a })"), + Example("f(↓{ $0.a })", configuration: extendedMode), + Example("f(a: ↓{ $0.b })", configuration: extendedMode), + Example("f(a: ↓{ a in a.b }, x)", configuration: extendedMode), + Example("f.map ↓{ a in a.b.c }"), + Example("f.allSatisfy ↓{ (a: A) in a.b }"), + Example("f.first ↓{ (a b: A) in b.c }"), + Example("f.contains ↓{ $0.0.a }"), + Example("f.compactMap ↓{ $0.a.b.c.d }"), + Example("f.flatMap ↓{ $0.a.b }"), + Example("let f: (Int) -> Int = ↓{ $0.bigEndian }", configuration: extendedMode), + Example("transform = ↓{ $0.a }"), + ], + corrections: [ + Example("f.map { $0.a }"): + Example("f.map(\\.a)"), + Example(""" + // begin + f.map { $0.a } // end + """): + Example(""" + // begin + f.map(\\.a) // end + """), + Example("f.map({ $0.a })"): + Example("f.map(\\.a)"), + Example("f(a: { $0.a })", configuration: extendedMode): + Example("f(a: \\.a)"), + Example("f({ $0.a })", configuration: extendedMode): + Example("f(\\.a)"), + Example("let f = /* begin */ { $0.a } // end", configuration: extendedMode): + Example("let f = /* begin */ \\.a // end"), + Example("let f = { $0.a }(b)"): + Example("let f = { $0.a }(b)"), + Example("let f: (Int) -> Int = ↓{ $0.bigEndian }", configuration: extendedMode): + Example("let f: (Int) -> Int = \\.bigEndian"), + Example("f.partition ↓{ $0.a.b }"): + Example("f.partition(by: \\.a.b)"), + Example("f.contains ↓{ $0.a.b }"): + Example("f.contains(where: \\.a.b)"), + Example("f.first ↓{ element in element.a }"): + Example("f.first(where: \\.a)"), + Example("f.drop ↓{ element in element.a }"): + Example("f.drop(while: \\.a)"), + Example("f.compactMap ↓{ $0.a.b.c.d }"): + Example("f.compactMap(\\.a.b.c.d)"), + ] + ) +} + +private extension PreferKeyPathRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ClosureExprSyntax) { + if node.isInvalid(restrictToStandardFunctions: configuration.restrictToStandardFunctions) { + return + } + if let onlyStmt = node.onlyExprStmt, + SwiftVersion.current >= .six || !onlyStmt.is(DeclReferenceExprSyntax.self), + onlyStmt.accesses(identifier: node.onlyParameter) { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + } + } + + final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { + guard node.additionalTrailingClosures.isEmpty, + let closure = node.trailingClosure, + !closure.isInvalid(restrictToStandardFunctions: configuration.restrictToStandardFunctions), + let expr = closure.onlyExprStmt, + expr.accesses(identifier: closure.onlyParameter) == true, + let replacement = expr.asKeyPath, + let calleeName = node.calleeName else { + return super.visit(node) + } + correctionPositions.append(closure.positionAfterSkippingLeadingTrivia) + var node = node.with(\.calledExpression, node.calledExpression.with(\.trailingTrivia, [])) + if node.leftParen == nil { + node = node.with(\.leftParen, .leftParenToken()) + } + let newArg = LabeledExprSyntax( + label: argumentLabelByStandardFunction[calleeName, default: nil], + expression: replacement + ) + node = node.with(\.arguments, [newArg] + ) + if node.rightParen == nil { + node = node.with(\.rightParen, .rightParenToken()) + } + node = node + .with(\.trailingClosure, nil) + .with(\.trailingTrivia, node.trailingTrivia) + return super.visit(node) + } + + override func visit(_ node: ClosureExprSyntax) -> ExprSyntax { + if node.isInvalid(restrictToStandardFunctions: configuration.restrictToStandardFunctions) { + return super.visit(node) + } + if let expr = node.onlyExprStmt, + expr.accesses(identifier: node.onlyParameter) == true, + let replacement = expr.asKeyPath { + correctionPositions.append(node.positionAfterSkippingLeadingTrivia) + let node = replacement + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + return super.visit(node) + } + return super.visit(node) + } + } +} + +private extension ExprSyntax { + func accesses(identifier: String?) -> Bool { + if let base = `as`(MemberAccessExprSyntax.self)?.base { + return base.accesses(identifier: identifier) + } + if let declRef = `as`(DeclReferenceExprSyntax.self) { + return declRef.baseName.text == identifier ?? "$0" + } + return false + } +} + +private extension ClosureExprSyntax { + var onlyParameter: String? { + switch signature?.parameterClause { + case let .simpleInput(params): + return params.onlyElement?.name.text + case let .parameterClause(params): + let param = params.parameters.onlyElement + return param?.secondName?.text ?? param?.firstName.text + case nil: return nil + } + } + + var onlyExprStmt: ExprSyntax? { + if case let .expr(expr) = statements.onlyElement?.item { + return expr + } + return nil + } + + func isInvalid(restrictToStandardFunctions: Bool) -> Bool { + guard keyPathInParent != \FunctionCallExprSyntax.calledExpression, + let parent, + ![.macroExpansionExpr, .multipleTrailingClosureElement].contains(parent.kind), + previousToken(viewMode: .sourceAccurate)?.text != "??" else { + return true + } + if let call = parent.as(LabeledExprSyntax.self)?.parent?.parent?.as(FunctionCallExprSyntax.self) { + // Closure is function argument. + return restrictToStandardFunctions && !call.isStandardFunction + } + if let call = parent.as(FunctionCallExprSyntax.self) { + // Trailing closure. + return call.additionalTrailingClosures.isNotEmpty || restrictToStandardFunctions && !call.isStandardFunction + } + return false + } +} + +private let argumentLabelByStandardFunction: [String: String?] = [ + "allSatisfy": nil, + "contains": "where", + "compactMap": nil, + "drop": "while", + "filter": nil, + "first": "where", + "flatMap": nil, + "map": nil, + "partition": "by", + "prefix": "while", +] + +private extension FunctionCallExprSyntax { + var isStandardFunction: Bool { + if let calleeName, argumentLabelByStandardFunction.keys.contains(calleeName) { + return arguments.count + (trailingClosure == nil ? 0 : 1) == 1 + } + return false + } + + var calleeName: String? { + (calledExpression.as(DeclReferenceExprSyntax.self) + ?? calledExpression.as(MemberAccessExprSyntax.self)?.declName)?.baseName.text + } +} + +private extension ExprSyntax { + var asKeyPath: ExprSyntax? { + if let memberAccess = `as`(MemberAccessExprSyntax.self) { + var this = memberAccess.base + var elements = [memberAccess.declName] + while this?.is(DeclReferenceExprSyntax.self) != true { + if let memberAccess = this?.as(MemberAccessExprSyntax.self) { + elements.append(memberAccess.declName) + this = memberAccess.base + } + } + return "\\.\(raw: elements.reversed().map(\.baseName.text).joined(separator: "."))" as ExprSyntax + } + if SwiftVersion.current >= .six, `is`(DeclReferenceExprSyntax.self) { + return "\\.self" + } + return nil + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferNimbleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferNimbleRule.swift index 9b2993c352..18d9d28952 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferNimbleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferNimbleRule.swift @@ -11,7 +11,7 @@ struct PreferNimbleRule: OptInRule { kind: .idiomatic, nonTriggeringExamples: [ Example("expect(foo) == 1"), - Example("expect(foo).to(equal(1))") + Example("expect(foo).to(equal(1))"), ], triggeringExamples: [ Example("↓XCTAssertTrue(foo)"), @@ -19,7 +19,7 @@ struct PreferNimbleRule: OptInRule { Example("↓XCTAssertNotEqual(foo, 2)"), Example("↓XCTAssertNil(foo)"), Example("↓XCTAssert(foo)"), - Example("↓XCTAssertGreaterThan(foo, 10)") + Example("↓XCTAssertGreaterThan(foo, 10)"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferTypeCheckingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferTypeCheckingRule.swift new file mode 100644 index 0000000000..c595dd0db8 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferTypeCheckingRule.swift @@ -0,0 +1,99 @@ +import SwiftSyntax +import SwiftSyntaxBuilder + +@SwiftSyntaxRule(foldExpressions: true, explicitRewriter: true) +struct PreferTypeCheckingRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "prefer_type_checking", + name: "Prefer Type Checking", + description: "Prefer `a is X` to `a as? X != nil`", + kind: .idiomatic, + nonTriggeringExamples: [ + Example("let foo = bar as? Foo"), + Example("bar is Foo"), + Example("2*x is X"), + Example(""" + if foo is Bar { + doSomeThing() + } + """), + Example(""" + if let bar = foo as? Bar { + foo.run() + } + """), + Example("bar as Foo != nil"), + Example("nil != bar as Foo"), + Example("bar as Foo? != nil"), + Example("bar as? Foo? != nil"), + ], + triggeringExamples: [ + Example("bar ↓as? Foo != nil"), + Example("2*x as? X != nil"), + Example(""" + if foo ↓as? Bar != nil { + doSomeThing() + } + """), + Example("nil != bar ↓as? Foo"), + Example("nil != 2*x ↓as? X"), + ], + corrections: [ + Example("bar ↓as? Foo != nil"): Example("bar is Foo"), + Example("nil != bar ↓as? Foo"): Example("bar is Foo"), + Example("2*x ↓as? X != nil"): Example("2*x is X"), + Example(""" + if foo ↓as? Bar != nil { + doSomeThing() + } + """): Example(""" + if foo is Bar { + doSomeThing() + } + """), + ] + ) +} + +private extension PreferTypeCheckingRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: InfixOperatorExprSyntax) { + if let asExpr = node.asExprWithOptionalTypeChecking { + violations.append(asExpr.asKeyword.positionAfterSkippingLeadingTrivia) + } + } + } + + final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax { + guard let asExpr = node.asExprWithOptionalTypeChecking else { + return super.visit(node) + } + + correctionPositions.append(asExpr.asKeyword.positionAfterSkippingLeadingTrivia) + + let expression = asExpr.expression.trimmed + let type = asExpr.type.trimmed + + return ExprSyntax(stringLiteral: "\(expression) is \(type)") + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + } + } +} + +private extension InfixOperatorExprSyntax { + var asExprWithOptionalTypeChecking: AsExprSyntax? { + if let asExpr = leftOperand.as(AsExprSyntax.self) ?? rightOperand.as(AsExprSyntax.self), + asExpr.questionOrExclamationMark?.tokenKind == .postfixQuestionMark, + !asExpr.type.is(OptionalTypeSyntax.self), + self.operator.as(BinaryOperatorExprSyntax.self)?.operator.tokenKind == .binaryOperator("!="), + rightOperand.is(NilLiteralExprSyntax.self) || leftOperand.is(NilLiteralExprSyntax.self) { + asExpr + } else { + nil + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift index cc8061f73a..c9f0bd9064 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift @@ -14,7 +14,7 @@ struct PreferZeroOverExplicitInitRule: OptInRule { Example("CGPoint(x: 0, y: -1)"), Example("CGSize(width: 2, height: 4)"), Example("CGVector(dx: -5, dy: 0)"), - Example("UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1)") + Example("UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1)"), ], triggeringExamples: [ Example("↓CGPoint(x: 0, y: 0)"), @@ -23,7 +23,7 @@ struct PreferZeroOverExplicitInitRule: OptInRule { Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"), Example("↓CGSize(width: 0, height: 0)"), Example("↓CGVector(dx: 0, dy: 0)"), - Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)") + Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)"), ], corrections: [ Example("↓CGPoint(x: 0, y: 0)"): Example("CGPoint.zero"), @@ -31,7 +31,7 @@ struct PreferZeroOverExplicitInitRule: OptInRule { Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"): Example("CGRect.zero"), Example("↓CGSize(width: 0, height: 0.000)"): Example("CGSize.zero"), Example("↓CGVector(dx: 0, dy: 0)"): Example("CGVector.zero"), - Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)"): Example("UIEdgeInsets.zero") + Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)"): Example("UIEdgeInsets.zero"), ] ) } @@ -74,31 +74,31 @@ private extension FunctionCallExprSyntax { } var isCGPointZeroCall: Bool { - return name == "CGPoint" && + name == "CGPoint" && argumentNames == ["x", "y"] && argumentsAreAllZero } var isCGSizeCall: Bool { - return name == "CGSize" && + name == "CGSize" && argumentNames == ["width", "height"] && argumentsAreAllZero } var isCGRectCall: Bool { - return name == "CGRect" && + name == "CGRect" && argumentNames == ["x", "y", "width", "height"] && argumentsAreAllZero } var isCGVectorCall: Bool { - return name == "CGVector" && + name == "CGVector" && argumentNames == ["dx", "dy"] && argumentsAreAllZero } var isUIEdgeInsetsCall: Bool { - return name == "UIEdgeInsets" && + name == "UIEdgeInsets" && argumentNames == ["top", "left", "bottom", "right"] && argumentsAreAllZero } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PrivateOverFilePrivateRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PrivateOverFilePrivateRule.swift index ebb812c631..f0ffcc60b7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PrivateOverFilePrivateRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PrivateOverFilePrivateRule.swift @@ -1,7 +1,7 @@ import SwiftSyntax -@SwiftSyntaxRule(explicitRewriter: true) -struct PrivateOverFilePrivateRule: Rule { +@SwiftSyntaxRule +struct PrivateOverFilePrivateRule: SwiftSyntaxCorrectableRule { var configuration = PrivateOverFilePrivateConfiguration() static let description = RuleDescription( @@ -12,9 +12,10 @@ struct PrivateOverFilePrivateRule: Rule { nonTriggeringExamples: [ Example("extension String {}"), Example("private extension String {}"), - Example("public \n enum MyEnum {}"), + Example("public protocol P {}"), Example("open extension \n String {}"), Example("internal extension String {}"), + Example("package typealias P = Int"), Example(""" extension String { fileprivate func Something(){} @@ -26,17 +27,22 @@ struct PrivateOverFilePrivateRule: Rule { } """), Example(""" + actor MyActor { + fileprivate let myInt = 4 + } + """), + Example(""" class MyClass { fileprivate(set) var myInt = 4 } """), Example(""" - struct Outter { + struct Outer { struct Inter { fileprivate struct Inner {} } } - """) + """), ], triggeringExamples: [ Example("↓fileprivate enum MyEnum {}"), @@ -44,190 +50,83 @@ struct PrivateOverFilePrivateRule: Rule { ↓fileprivate class MyClass { fileprivate(set) var myInt = 4 } - """) + """), + Example(""" + ↓fileprivate actor MyActor { + fileprivate let myInt = 4 + } + """), + Example(""" + ↓fileprivate func f() {} + ↓fileprivate var x = 0 + """), ], corrections: [ - Example("↓fileprivate enum MyEnum {}"): Example("private enum MyEnum {}"), + Example("↓fileprivate enum MyEnum {}"): + Example("private enum MyEnum {}"), Example("↓fileprivate enum MyEnum { fileprivate class A {} }"): Example("private enum MyEnum { fileprivate class A {} }"), - Example("↓fileprivate class MyClass {\nfileprivate(set) var myInt = 4\n}"): - Example("private class MyClass {\nfileprivate(set) var myInt = 4\n}") + Example("↓fileprivate class MyClass { fileprivate(set) var myInt = 4 }"): + Example("private class MyClass { fileprivate(set) var myInt = 4 }"), + Example("↓fileprivate actor MyActor { fileprivate(set) var myInt = 4 }"): + Example("private actor MyActor { fileprivate(set) var myInt = 4 }"), ] ) } private extension PrivateOverFilePrivateRule { final class Visitor: ViolationsSyntaxVisitor { - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren - } + override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } - override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { - if configuration.validateExtensions, let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren + override func visitPost(_ node: ActorDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren + override func visitPost(_ node: ClassDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren + override func visitPost(_ node: EnumDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) + override func visitPost(_ node: ExtensionDeclSyntax) { + if configuration.validateExtensions { + visit(withModifier: node) } - return .skipChildren } - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren + override func visitPost(_ node: FunctionDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren + override func visitPost(_ node: ProtocolDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind { - if let privateModifier = node.modifiers.fileprivateModifier { - violations.append(privateModifier.positionAfterSkippingLeadingTrivia) - } - return .skipChildren + override func visitPost(_ node: StructDeclSyntax) { + visit(withModifier: node) } - } - final class Rewriter: ViolationsSyntaxRewriter { - // don't call super in any of the `visit` methods to avoid digging into the children - override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax { - guard configuration.validateExtensions, let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) + override func visitPost(_ node: TypeAliasDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: ClassDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) + override func visitPost(_ node: VariableDeclSyntax) { + visit(withModifier: node) } - override func visit(_ node: StructDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) - } - - override func visit(_ node: EnumDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) - } - - override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) - } - - override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) - } - - override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) - } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) - } - - override func visit(_ node: TypeAliasDeclSyntax) -> DeclSyntax { - guard let modifier = node.modifiers.fileprivateModifier, - let modifierIndex = node.modifiers.fileprivateModifierIndex else { - return DeclSyntax(node) + private func visit(withModifier node: some WithModifiersSyntax) { + if let modifier = node.modifiers.first(where: { $0.name.tokenKind == .keyword(.fileprivate) }) { + violations.append( + at: modifier.positionAfterSkippingLeadingTrivia, + correction: .init( + start: modifier.positionAfterSkippingLeadingTrivia, + end: modifier.endPositionBeforeTrailingTrivia, + replacement: "private" + ) + ) } - - correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia) - let newNode = node.with(\.modifiers, node.modifiers.replacing(fileprivateModifierIndex: modifierIndex)) - return DeclSyntax(newNode) } } } - -private extension DeclModifierListSyntax { - var fileprivateModifierIndex: DeclModifierListSyntax.Index? { - firstIndex(where: { $0.name.tokenKind == .keyword(.fileprivate) }) - } - - var fileprivateModifier: DeclModifierSyntax? { - fileprivateModifierIndex.flatMap { self[$0] } - } - - func replacing(fileprivateModifierIndex: DeclModifierListSyntax.Index) -> DeclModifierListSyntax { - let fileprivateModifier = self[fileprivateModifierIndex] - return with( - \.[fileprivateModifierIndex], - fileprivateModifier.with( - \.name, - .keyword( - .private, - leadingTrivia: fileprivateModifier.leadingTrivia, - trailingTrivia: fileprivateModifier.trailingTrivia - ) - ) - ) - } -} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantNilCoalescingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantNilCoalescingRule.swift index 0aa0e0ae6f..093556e41d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantNilCoalescingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantNilCoalescingRule.swift @@ -18,7 +18,7 @@ struct RedundantNilCoalescingRule: OptInRule { ], corrections: [ Example("var myVar: Int? = nil; let foo = myVar ↓?? nil"): - Example("var myVar: Int? = nil; let foo = myVar") + Example("var myVar: Int? = nil; let foo = myVar"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRuleExamples.swift index 08ef48ede0..1b82b89f99 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRuleExamples.swift @@ -129,7 +129,7 @@ struct RedundantObjcAttributeRuleExamples { case bar } } - """) + """), ] static let triggeringExamples = [ @@ -195,7 +195,7 @@ struct RedundantObjcAttributeRuleExamples { return 0 } } - """) + """), ] static let corrections = [ @@ -335,6 +335,6 @@ struct RedundantObjcAttributeRuleExamples { return 0 } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantOptionalInitializationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantOptionalInitializationRule.swift index 6bd29aa345..6937a51dc8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantOptionalInitializationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantOptionalInitializationRule.swift @@ -43,7 +43,7 @@ struct RedundantOptionalInitializationRule: Rule { func funcName() { let myVar: String? = nil } - """) + """), ], triggeringExamples: triggeringExamples, corrections: corrections @@ -63,7 +63,7 @@ struct RedundantOptionalInitializationRule: Rule { func funcName() { var myVar: String?↓ = nil } - """) + """), ] private static let corrections: [Example: Example] = [ @@ -102,7 +102,7 @@ struct RedundantOptionalInitializationRule: Rule { func foo() { var myVar: String?, b: Int } - """) + """), ] } @@ -119,7 +119,7 @@ private extension RedundantOptionalInitializationRule { } final class Rewriter: ViolationsSyntaxRewriter { - override func visitAny(_ node: Syntax) -> Syntax? { nil } + override func visitAny(_: Syntax) -> Syntax? { nil } override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { guard node.bindingSpecifier.tokenKind == .keyword(.var), diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantSetAccessControlRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantSetAccessControlRule.swift index c2b4dce9f7..c7af821b59 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantSetAccessControlRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantSetAccessControlRule.swift @@ -29,7 +29,7 @@ struct RedundantSetAccessControlRule: Rule { extension Color { public internal(set) static var someColor = Color.anotherColor } - """) + """), ], triggeringExamples: [ Example("↓private(set) private var foo: Int"), @@ -55,7 +55,7 @@ struct RedundantSetAccessControlRule: Rule { fileprivate class A { ↓fileprivate(set) var value: Int } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantStringEnumValueRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantStringEnumValueRule.swift index a20647b78e..e39ffed5a9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantStringEnumValueRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantStringEnumValueRule.swift @@ -38,7 +38,7 @@ struct RedundantStringEnumValueRule: Rule { enum Numbers: String { case one, two } - """) + """), ], triggeringExamples: [ Example(""" @@ -56,7 +56,7 @@ struct RedundantStringEnumValueRule: Rule { enum Numbers: String { case one, two = ↓"two" } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantTypeAnnotationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantTypeAnnotationRule.swift index 130066ede8..ac7b3047bc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantTypeAnnotationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantTypeAnnotationRule.swift @@ -1,8 +1,9 @@ -import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax -struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { - var configuration = SeverityConfiguration(.warning) +@SwiftSyntaxRule +struct RedundantTypeAnnotationRule: OptInRule, SwiftSyntaxCorrectableRule { + var configuration = RedundantTypeAnnotationConfiguration() static let description = RuleDescription( identifier: "redundant_type_annotation", @@ -12,7 +13,23 @@ struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { nonTriggeringExamples: [ Example("var url = URL()"), Example("var url: CustomStringConvertible = URL()"), - Example("@IBInspectable var color: UIColor = UIColor.white"), + Example("var one: Int = 1, two: Int = 2, three: Int"), + Example("guard let url = URL() else { return }"), + Example("if let url = URL() { return }"), + Example("let alphanumerics = CharacterSet.alphanumerics"), + Example("var set: Set = Set([])"), + Example("var set: Set = Set.init([])"), + Example("var set = Set([])"), + Example("var set = Set.init([])"), + Example("guard var set: Set = Set([]) else { return }"), + Example("if var set: Set = Set.init([]) { return }"), + Example("guard var set = Set([]) else { return }"), + Example("if var set = Set.init([]) { return }"), + Example("var one: A = B()"), + Example("var one: A = B()"), + Example("var one: A = B()"), + Example("let a = A.b.c.d"), + Example("let a: B = A.b.c.d"), Example(""" enum Direction { case up @@ -28,7 +45,24 @@ struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { } var direction = Direction.up - """) + """), + Example("@IgnoreMe var a: Int = Int(5)", configuration: ["ignore_attributes": ["IgnoreMe"]]), + Example(""" + var a: Int { + @IgnoreMe let i: Int = Int(1) + return i + } + """, configuration: ["ignore_attributes": ["IgnoreMe"]]), + Example("var bol: Bool = true"), + Example("var dbl: Double = 0.0"), + Example("var int: Int = 0"), + Example("var str: String = \"str\""), + Example(""" + struct Foo { + var url: URL = URL() + let myVar: Int? = 0, s: String = "" + } + """, configuration: ["ignore_properties": true]), ], triggeringExamples: [ Example("var url↓:URL=URL()"), @@ -36,7 +70,23 @@ struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { Example("var url↓: URL = URL()"), Example("let url↓: URL = URL()"), Example("lazy var url↓: URL = URL()"), + Example("let url↓: URL = URL()!"), + Example("var one: Int = 1, two↓: Int = Int(5), three: Int"), + Example("guard let url↓: URL = URL() else { return }"), + Example("if let url↓: URL = URL() { return }"), Example("let alphanumerics↓: CharacterSet = CharacterSet.alphanumerics"), + Example("var set↓: Set = Set([])"), + Example("var set↓: Set = Set.init([])"), + Example("var set↓: Set = Set([])"), + Example("var set↓: Set = Set.init([])"), + Example("guard var set↓: Set = Set([]) else { return }"), + Example("if var set↓: Set = Set.init([]) { return }"), + Example("guard var set↓: Set = Set([]) else { return }"), + Example("if var set↓: Set = Set.init([]) { return }"), + Example("var set↓: Set = Set([]), otherSet: Set"), + Example("var num↓: Int = Int.random(0..<10)"), + Example("let a↓: A = A.b.c.d"), + Example("let a↓: A = A.f().b"), Example(""" class ViewController: UIViewController { func someMethod() { @@ -44,7 +94,15 @@ struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { } } """), - Example("var isEnabled↓: Bool = true"), + Example(""" + class ViewController: UIViewController { + func someMethod() { + let myVar↓: Int = Int(5) + } + } + """, configuration: ["ignore_properties": true]), + Example("let a↓: [Int] = [Int]()"), + Example("let a↓: A.B = A.B()"), Example(""" enum Direction { case up @@ -52,13 +110,47 @@ struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { } var direction↓: Direction = Direction.up - """) + """), + Example("@DontIgnoreMe var a↓: Int = Int(5)", configuration: ["ignore_attributes": ["IgnoreMe"]]), + Example(""" + @IgnoreMe + var a: Int { + let i↓: Int = Int(1) + return i + } + """, configuration: ["ignore_attributes": ["IgnoreMe"]]), + Example("var bol↓: Bool = true", configuration: ["consider_default_literal_types_redundant": true]), + Example("var dbl↓: Double = 0.0", configuration: ["consider_default_literal_types_redundant": true]), + Example("var int↓: Int = 0", configuration: ["consider_default_literal_types_redundant": true]), + Example("var str↓: String = \"str\"", configuration: ["consider_default_literal_types_redundant": true]), ], corrections: [ Example("var url↓: URL = URL()"): Example("var url = URL()"), Example("let url↓: URL = URL()"): Example("let url = URL()"), + Example("var one: Int = 1, two↓: Int = Int(5), three: Int"): + Example("var one: Int = 1, two = Int(5), three: Int"), + Example("guard let url↓: URL = URL() else { return }"): + Example("guard let url = URL() else { return }"), + Example("if let url↓: URL = URL() { return }"): + Example("if let url = URL() { return }"), Example("let alphanumerics↓: CharacterSet = CharacterSet.alphanumerics"): Example("let alphanumerics = CharacterSet.alphanumerics"), + Example("var set↓: Set = Set([])"): + Example("var set = Set([])"), + Example("var set↓: Set = Set.init([])"): + Example("var set = Set.init([])"), + Example("var set↓: Set = Set([])"): + Example("var set = Set([])"), + Example("var set↓: Set = Set.init([])"): + Example("var set = Set.init([])"), + Example("guard var set↓: Set = Set([]) else { return }"): + Example("guard var set = Set([]) else { return }"), + Example("if var set↓: Set = Set.init([]) { return }"): + Example("if var set = Set.init([]) { return }"), + Example("var set↓: Set = Set([]), otherSet: Set"): + Example("var set = Set([]), otherSet: Set"), + Example("let a↓: A = A.b.c.d"): + Example("let a = A.b.c.d"), Example(""" class ViewController: UIViewController { func someMethod() { @@ -72,97 +164,123 @@ struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule { let myVar = Int(5) } } - """) + """), + Example("var num: Int = Int.random(0..<10)"): Example("var num = Int.random(0..<10)"), + Example(""" + @IgnoreMe + var a: Int { + let i↓: Int = Int(1) + return i + } + """, configuration: ["ignore_attributes": ["IgnoreMe"]]): + Example(""" + @IgnoreMe + var a: Int { + let i = Int(1) + return i + } + """), + Example("var bol: Bool = true", configuration: ["consider_default_literal_types_redundant": true]): + Example("var bol = true"), + Example("var dbl: Double = 0.0", configuration: ["consider_default_literal_types_redundant": true]): + Example("var dbl = 0.0"), + Example("var int: Int = 0", configuration: ["consider_default_literal_types_redundant": true]): + Example("var int = 0"), + Example("var str: String = \"str\"", configuration: ["consider_default_literal_types_redundant": true]): + Example("var str = \"str\""), ] ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(in: file).map { range in - StyleViolation( - ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, characterOffset: range.location) +private extension RedundantTypeAnnotationRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: PatternBindingSyntax) { + if let varDecl = node.parent?.parent?.as(VariableDeclSyntax.self), + !configuration.shouldSkipRuleCheck(for: varDecl), + let typeAnnotation = node.typeAnnotation, + let initializer = node.initializer?.value { + collectViolation(forType: typeAnnotation, withInitializer: initializer) + } + } + + override func visitPost(_ node: OptionalBindingConditionSyntax) { + if let typeAnnotation = node.typeAnnotation, + let initializer = node.initializer?.value { + collectViolation(forType: typeAnnotation, withInitializer: initializer) + } + } + + private func collectViolation(forType type: TypeAnnotationSyntax, withInitializer initializer: ExprSyntax) { + let validateLiterals = configuration.considerDefaultLiteralTypesRedundant + let isLiteralRedundant = validateLiterals && initializer.hasRedundant(literalType: type.type) + guard isLiteralRedundant || initializer.hasRedundant(type: type.type) else { + return + } + violations.append( + at: type.positionAfterSkippingLeadingTrivia, + correction: .init( + start: type.position, + end: type.endPositionBeforeTrailingTrivia, + replacement: "" + ) ) } } +} - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { - return (violationRange, "") +private extension ExprSyntax { + /// An expression can represent an access to an identifier in one or another way depending on the exact underlying + /// expression type. E.g. the expression `A` accesses `A` while `f()` accesses `f` and `a.b.c` accesses `a` in the + /// sense of this property. In the context of this rule, `Set()` accesses `Set` as well as `Set`. + var accessedNames: [String] { + if let declRef = `as`(DeclReferenceExprSyntax.self) { + [declRef.trimmedDescription] + } else if let memberAccess = `as`(MemberAccessExprSyntax.self) { + (memberAccess.base?.accessedNames ?? []) + [memberAccess.trimmedDescription] + } else if let genericSpecialization = `as`(GenericSpecializationExprSyntax.self) { + [genericSpecialization.trimmedDescription] + genericSpecialization.expression.accessedNames + } else if let call = `as`(FunctionCallExprSyntax.self) { + call.calledExpression.accessedNames + } else if let arrayExpr = `as`(ArrayExprSyntax.self) { + [arrayExpr.trimmedDescription] + } else { + [] + } } - private let typeAnnotationPattern: String - private let expressionPattern: String - - init() { - typeAnnotationPattern = - ":\\s*" + // semicolon and any number of whitespaces - "\\w+" // type name - - expressionPattern = - "(var|let)" + // var or let - "\\s+" + // at least single whitespace - "\\w+" + // variable name - "\\s*" + // possible whitespaces - typeAnnotationPattern + - "\\s*=\\s*" + // assignment operator with possible surrounding whitespaces - "\\w+" + // assignee name (type or keyword) - "[\\(\\.]?" // possible opening parenthesis or dot + func hasRedundant(literalType type: TypeSyntax) -> Bool { + type.trimmedDescription == kind.compilerInferredLiteralType } - func violationRanges(in file: SwiftLintFile) -> [NSRange] { - return file - .match(pattern: expressionPattern) - .filter { - $0.1 == [.keyword, .identifier, .typeidentifier, .identifier] || - $0.1 == [.keyword, .identifier, .typeidentifier, .keyword] - } - .filter { !isFalsePositive(file: file, range: $0.0) } - .filter { !isIBInspectable(file: file, range: $0.0) } - .compactMap { - file.match(pattern: typeAnnotationPattern, - excludingSyntaxKinds: SyntaxKind.commentAndStringKinds, range: $0.0).first - } + func hasRedundant(type: TypeSyntax) -> Bool { + `as`(ForceUnwrapExprSyntax.self)?.expression.hasRedundant(type: type) + ?? accessedNames.contains(type.trimmedDescription) } +} - private func isFalsePositive(file: SwiftLintFile, range: NSRange) -> Bool { - guard let typeNames = getPartsOfExpression(in: file, range: range) else { return false } - - let lhs = typeNames.variableTypeName - let rhs = typeNames.assigneeName - - if lhs == rhs || (lhs == "Bool" && (rhs == "true" || rhs == "false")) { - return false +private extension SyntaxKind { + var compilerInferredLiteralType: String? { + switch self { + case .booleanLiteralExpr: + "Bool" + case .floatLiteralExpr: + "Double" + case .integerLiteralExpr: + "Int" + case .stringLiteralExpr: + "String" + default: + nil } - return true } +} - private func getPartsOfExpression( - in file: SwiftLintFile, range: NSRange - ) -> (variableTypeName: String, assigneeName: String)? { - let substring = file.stringView.substring(with: range) - let components = substring.components(separatedBy: "=") - - guard - components.count == 2, - let variableTypeName = components[0].components(separatedBy: ":").last?.trimmingCharacters(in: .whitespaces) - else { - return nil +extension RedundantTypeAnnotationConfiguration { + func shouldSkipRuleCheck(for varDecl: VariableDeclSyntax) -> Bool { + if ignoreAttributes.contains(where: { varDecl.attributes.contains(attributeNamed: $0) }) { + return true } - let charactersToTrimFromRhs = CharacterSet(charactersIn: ".(").union(.whitespaces) - let assigneeName = components[1].trimmingCharacters(in: charactersToTrimFromRhs) - - return (variableTypeName, assigneeName) - } - - private func isIBInspectable(file: SwiftLintFile, range: NSRange) -> Bool { - guard - let byteRange = file.stringView.NSRangeToByteRange(start: range.location, length: range.length), - let dict = file.structureDictionary.structures(forByteOffset: byteRange.location).last, - let kind = dict.declarationKind, - SwiftDeclarationKind.variableKinds.contains(kind) - else { return false } - - return dict.enclosedSwiftAttributes.contains(.ibinspectable) + return ignoreProperties && varDecl.parent?.is(MemberBlockItemSyntax.self) == true } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift index 369a7459fa..432dc89f0d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift @@ -32,7 +32,7 @@ struct RedundantVoidReturnRule: Rule { doSomething { arg -> Void in print(arg) } - """, configuration: ["include_closures": false]) + """, configuration: ["include_closures": false]), ], triggeringExamples: [ Example("func foo()↓ -> Void {}"), @@ -57,7 +57,7 @@ struct RedundantVoidReturnRule: Rule { doSomething { arg↓ -> Void in print(arg) } - """) + """), ], corrections: [ Example("func foo()↓ -> Void {}"): Example("func foo() {}"), @@ -65,7 +65,7 @@ struct RedundantVoidReturnRule: Rule { Example("func foo()↓ -> () {}"): Example("func foo() {}"), Example("protocol Foo {\n func foo()↓ -> ()\n}"): Example("protocol Foo {\n func foo()\n}"), Example("protocol Foo {\n #if true\n func foo()↓ -> Void\n #endif\n}"): - Example("protocol Foo {\n #if true\n func foo()\n #endif\n}") + Example("protocol Foo {\n #if true\n func foo()\n #endif\n}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRule.swift index 58ea304639..7b033ecb4d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRule.swift @@ -44,7 +44,7 @@ private extension ReturnValueFromVoidFunctionRule { \.leadingTrivia, .newline + (returnStmt.leadingTrivia.indentation(isOnNewline: false) ?? [])) .with(\.trailingTrivia, returnStmt.trailingTrivia) - ))) + ))), ] return super.visit(CodeBlockItemListSyntax(newStmtList)) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRuleExamples.swift index bcbbbcf4df..5dd45d712f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ReturnValueFromVoidFunctionRuleExamples.swift @@ -146,7 +146,7 @@ internal struct ReturnValueFromVoidFunctionRuleExamples { } } } - """#, excludeFromDocumentation: true) + """#, excludeFromDocumentation: true), ] static let triggeringExamples = [ @@ -289,7 +289,7 @@ internal struct ReturnValueFromVoidFunctionRuleExamples { } ↓return foo() } - """) + """), ] static let corrections = [ @@ -320,6 +320,6 @@ internal struct ReturnValueFromVoidFunctionRuleExamples { return } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ShorthandOptionalBindingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ShorthandOptionalBindingRule.swift index 220d73be0a..98a1187298 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ShorthandOptionalBindingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ShorthandOptionalBindingRule.swift @@ -19,12 +19,12 @@ struct ShorthandOptionalBindingRule: OptInRule { if let i = i as? Foo {} guard let `self` = self else {} while var i { i = nil } - """), + """), Example(""" if let i, var i = a, j > 0 {} - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example(""" @@ -33,49 +33,49 @@ struct ShorthandOptionalBindingRule: OptInRule { if ↓var `self` = `self` {} if i > 0, ↓let j = j {} if ↓let i = i, ↓var j = j {} - """), + """), Example(""" if ↓let i = i, ↓var j = j, j > 0 {} - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" guard ↓let i = i else {} guard ↓let self = self else {} guard ↓var `self` = `self` else {} guard i > 0, ↓let j = j else {} guard ↓let i = i, ↓var j = j else {} - """), + """), Example(""" while ↓var i = i { i = nil } - """) + """), ], corrections: [ Example(""" if ↓let i = i {} - """): Example(""" - if let i {} - """), + """): Example(""" + if let i {} + """), Example(""" if ↓let self = self {} - """): Example(""" - if let self {} - """), + """): Example(""" + if let self {} + """), Example(""" if ↓var `self` = `self` {} - """): Example(""" - if var `self` {} - """), + """): Example(""" + if var `self` {} + """), Example(""" guard ↓let i = i, ↓var j = j , ↓let k =k else {} - """): Example(""" - guard let i, var j , let k else {} - """), + """): Example(""" + guard let i, var j , let k else {} + """), Example(""" while j > 0, ↓var i = i { i = nil } - """): Example(""" - while j > 0, var i { i = nil } - """) + """): Example(""" + while j > 0, var i { i = nil } + """), ], deprecatedAliases: ["if_let_shadowing"] ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOperatorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOperatorRule.swift index 910189b6c2..419085b2b8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOperatorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOperatorRule.swift @@ -43,7 +43,7 @@ struct StaticOperatorRule: OptInRule { } } } - """) + """), ], triggeringExamples: [ Example(""" @@ -73,7 +73,7 @@ struct StaticOperatorRule: OptInRule { return false } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOverFinalClassRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOverFinalClassRule.swift new file mode 100644 index 0000000000..b0cf8be5f1 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StaticOverFinalClassRule.swift @@ -0,0 +1,125 @@ +import SwiftSyntax + +@SwiftSyntaxRule +struct StaticOverFinalClassRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "static_over_final_class", + name: "Static Over Final Class", + description: """ + Prefer `static` over `class` when the declaration is not allowed to be overridden \ + in child classes due to its context being final. Likewise, the compiler complains \ + about `open` being used in `final` classes. + """, + kind: .idiomatic, + nonTriggeringExamples: [ + Example(""" + class C { + static func f() {} + } + """), + Example(""" + class C { + static var i: Int { 0 } + } + """), + Example(""" + class C { + static subscript(_: Int) -> Int { 0 } + } + """), + Example(""" + class C { + class func f() {} + } + """), + Example(""" + final class C {} + """), + Example(""" + final class C { + class D { + class func f() {} + } + } + """), + ], + triggeringExamples: [ + Example(""" + class C { + ↓final class func f() {} + } + """), + Example(""" + class C { + ↓final class var i: Int { 0 } + } + """), + Example(""" + class C { + ↓final class subscript(_: Int) -> Int { 0 } + } + """), + Example(""" + final class C { + ↓class func f() {} + } + """), + Example(""" + class C { + final class D { + ↓class func f() {} + } + } + """), + ] + ) +} + +private extension StaticOverFinalClassRule { + final class Visitor: ViolationsSyntaxVisitor { + private var classContexts = Stack() + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + classContexts.push(node.modifiers.contains(keyword: .final)) + return .visitChildren + } + + override func visitPost(_: ClassDeclSyntax) { + classContexts.pop() + } + + override func visitPost(_ node: FunctionDeclSyntax) { + validateNode(at: node.positionAfterSkippingLeadingTrivia, with: node.modifiers) + } + + override func visitPost(_ node: VariableDeclSyntax) { + validateNode(at: node.positionAfterSkippingLeadingTrivia, with: node.modifiers) + } + + override func visitPost(_ node: SubscriptDeclSyntax) { + validateNode(at: node.positionAfterSkippingLeadingTrivia, with: node.modifiers) + } + + // MARK: - + private func validateNode(at position: AbsolutePosition, with modifiers: DeclModifierListSyntax) { + let reason: String? = if modifiers.contains(keyword: .final), modifiers.contains(keyword: .class) { + "Prefer `static` over `final class`" + } else if modifiers.contains(keyword: .class), classContexts.peek() == true { + "Prefer `static` over `class` in a final class" + } else { + nil + } + if let reason { + violations.append( + ReasonedRuleViolation( + position: position, + reason: reason, + severity: configuration.severity + ) + ) + } + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StrictFilePrivateRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StrictFilePrivateRule.swift index d36aa2bd7c..918999604b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StrictFilePrivateRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/StrictFilePrivateRule.swift @@ -55,7 +55,7 @@ struct StrictFilePrivateRule: OptInRule { protocol P { func f() } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] + ["actor", "class", "enum", "extension", "struct"].map { type in Example(""" \(type) T: P { @@ -100,7 +100,7 @@ struct StrictFilePrivateRule: OptInRule { """), Example(""" ↓fileprivate func f() {} - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] + ["actor", "class", "enum", "extension", "struct"].map { type in Example(""" \(type) T: P { @@ -205,7 +205,7 @@ private extension StrictFilePrivateRule { private final class ProtocolCollector: ViolationsSyntaxVisitor { private(set) var protocols = [String: [ProtocolRequirementType]]() - private var currentProtocolName: String = "" + private var currentProtocolName = "" override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .allExcept(ProtocolDeclSyntax.self) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRule.swift index b398598357..2ae8d41499 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRule.swift @@ -20,15 +20,15 @@ struct SyntacticSugarRule: CorrectableRule, SourceKitFreeRule { return visitor.walk(file: file) { visitor in flattenViolations(visitor.violations) }.map { violation in - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: ByteCount(violation.position)), - reason: violation.type.violationReason) + StyleViolation(ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, byteOffset: ByteCount(violation.position)), + reason: violation.type.violationReason) } } private func flattenViolations(_ violations: [SyntacticSugarRuleViolation]) -> [SyntacticSugarRuleViolation] { - return violations.flatMap { [$0] + flattenViolations($0.children) } + violations.flatMap { [$0] + flattenViolations($0.children) } } func correct(file: SwiftLintFile) -> [Correction] { @@ -107,7 +107,7 @@ private struct SyntacticSugarRuleViolation { let correction: Correction - var children: [SyntacticSugarRuleViolation] = [] + var children: [Self] = [] } private final class SyntacticSugarRuleVisitor: SyntaxVisitor { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRuleExamples.swift index 2da01b60ca..6b1a57c35a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/SyntacticSugarRuleExamples.swift @@ -28,7 +28,7 @@ internal enum SyntacticSugarRuleExamples { Example("let x = case Optional.none = obj"), Example("let a = Swift.Optional.none"), - Example("func f() -> [Array.Index] { [Array.Index]() }", excludeFromDocumentation: true) + Example("func f() -> [Array.Index] { [Array.Index]() }", excludeFromDocumentation: true), ] static let triggering = [ @@ -62,7 +62,7 @@ internal enum SyntacticSugarRuleExamples { Example(""" let dict: [String: Any] = [:] _ = dict["key"] as? ↓Optional ?? Optional.none - """) + """), ] static let corrections = [ @@ -81,6 +81,6 @@ internal enum SyntacticSugarRuleExamples { Example("let x:↓Dictionary<↓Dictionary<↓Dictionary, Int>, String>"): Example("let x:[[[Int: Int]: Int]: String]"), Example("let x:↓Array<↓Dictionary>"): Example("let x:[[Int: Int]]"), - Example("let x:↓Optional<↓Dictionary>"): Example("let x:[Int: Int]?") + Example("let x:↓Optional<↓Dictionary>"): Example("let x:[Int: Int]?"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ToggleBoolRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ToggleBoolRule.swift index 12b9fe4945..cc97758ccf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ToggleBoolRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ToggleBoolRule.swift @@ -5,7 +5,7 @@ import SwiftSyntaxBuilder struct ToggleBoolRule: OptInRule { var configuration = SeverityConfiguration(.warning) - static var description = RuleDescription( + static let description = RuleDescription( identifier: "toggle_bool", name: "Toggle Bool", description: "Prefer `someBool.toggle()` over `someBool = !someBool`", @@ -16,17 +16,17 @@ struct ToggleBoolRule: OptInRule { Example("func foo() { abc.toggle() }"), Example("view.clipsToBounds = !clipsToBounds"), Example("disconnected = !connected"), - Example("result = !result.toggle()") + Example("result = !result.toggle()"), ], triggeringExamples: [ Example("↓isHidden = !isHidden"), Example("↓view.clipsToBounds = !view.clipsToBounds"), - Example("func foo() { ↓abc = !abc }") + Example("func foo() { ↓abc = !abc }"), ], corrections: [ Example("↓isHidden = !isHidden"): Example("isHidden.toggle()"), Example("↓view.clipsToBounds = !view.clipsToBounds"): Example("view.clipsToBounds.toggle()"), - Example("func foo() { ↓abc = !abc }"): Example("func foo() { abc.toggle() }") + Example("func foo() { ↓abc = !abc }"): Example("func foo() { abc.toggle() }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TrailingSemicolonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TrailingSemicolonRule.swift index 6e6b397ca6..72a50c3cef 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TrailingSemicolonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TrailingSemicolonRule.swift @@ -11,17 +11,17 @@ struct TrailingSemicolonRule: Rule { kind: .idiomatic, nonTriggeringExamples: [ Example("let a = 0"), - Example("let a = 0; let b = 0") + Example("let a = 0; let b = 0"), ], triggeringExamples: [ Example("let a = 0↓;\n"), Example("let a = 0↓;\nlet b = 1"), - Example("let a = 0↓; // a comment\n") + Example("let a = 0↓; // a comment\n"), ], corrections: [ Example("let a = 0↓;\n"): Example("let a = 0\n"), Example("let a = 0↓;\nlet b = 1"): Example("let a = 0\nlet b = 1"), - Example("let foo = 12↓; // comment\n"): Example("let foo = 12 // comment\n") + Example("let foo = 12↓; // comment\n"): Example("let foo = 12 // comment\n"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRuleExamples.swift index 6a8b9a25a1..fcd8ae214f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRuleExamples.swift @@ -25,7 +25,7 @@ internal struct TypeNameRuleExamples { case x, y, z } } - """) + """), ] static let triggeringExamples: [Example] = [ @@ -57,6 +57,6 @@ internal struct TypeNameRuleExamples { associatedtype ↓\(repeatElement("A", count: 41).joined()) } """), - Example("protocol ↓X {}") + Example("protocol ↓X {}"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableConditionRule.swift index 694924d14c..2a6c8954b7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableConditionRule.swift @@ -38,7 +38,7 @@ struct UnavailableConditionRule: Rule { } else if i < 2, #available(macOS 11.0, *) { print("something else") } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example(""" @@ -66,7 +66,7 @@ struct UnavailableConditionRule: Rule { } else if i < 2 { loadMainWindow() } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableFunctionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableFunctionRule.swift index ae81a1a4f5..ca5959a666 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableFunctionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnavailableFunctionRule.swift @@ -36,7 +36,7 @@ struct UnavailableFunctionRule: OptInRule { // Crash the app to re-start the onboarding flow. fatalError("Onboarding re-start crash.") } - """) + """), ], triggeringExamples: [ Example(""" @@ -67,7 +67,7 @@ struct UnavailableFunctionRule: OptInRule { // Crash the app to re-start the onboarding flow. fatalError("Onboarding re-start crash.") } - """) + """), ] ) } @@ -131,7 +131,7 @@ private extension CodeBlockSyntax? { let terminatingFunctions: Set = [ "abort", "fatalError", - "preconditionFailure" + "preconditionFailure", ] return statements.contains { item in @@ -157,15 +157,15 @@ private extension CodeBlockSyntax? { private final class ReturnFinderVisitor: SyntaxVisitor { private(set) var containsReturn = false - override func visitPost(_ node: ReturnStmtSyntax) { + override func visitPost(_: ReturnStmtSyntax) { containsReturn = true } - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } - override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift index 85019f1d7a..ea46cba797 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift @@ -3,8 +3,9 @@ import SwiftSyntax private func embedInSwitch( _ text: String, case: String = "case .bar", - file: StaticString = #file, line: UInt = #line) -> Example { - return Example(""" + file: StaticString = #filePath, + line: UInt = #line) -> Example { + Example(""" switch foo { \(`case`): \(text) @@ -39,13 +40,13 @@ struct UnneededBreakInSwitchRule: Rule { } } } - """) + """), ], triggeringExamples: [ embedInSwitch("something()\n ↓break"), embedInSwitch("something()\n ↓break // comment"), embedInSwitch("something()\n ↓break", case: "default"), - embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition") + embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition"), ], corrections: [ embedInSwitch("something()\n ↓break") @@ -83,7 +84,7 @@ struct UnneededBreakInSwitchRule: Rule { embedInSwitch("something()\n ↓break", case: "default") : embedInSwitch("something()", case: "default"), embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition") - : embedInSwitch("something()", case: "case .foo, .foo2 where condition") + : embedInSwitch("something()", case: "case .foo, .foo2 where condition"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRule.swift index 92456c40cd..ddd73209a8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRule.swift @@ -49,7 +49,7 @@ private extension UnneededSynthesizedInitializerRule { final class Rewriter: ViolationsSyntaxRewriter { private var unneededInitializers: [InitializerDeclSyntax] = [] - override func visitAny(_ node: Syntax) -> Syntax? { nil } + override func visitAny(_: Syntax) -> Syntax? { nil } override func visit(_ node: StructDeclSyntax) -> DeclSyntax { unneededInitializers = node.unneededInitializers.filter { @@ -71,42 +71,48 @@ private extension UnneededSynthesizedInitializerRule { } } +private final class ElementCollector: SyntaxAnyVisitor { + var initializers = [InitializerDeclSyntax]() + var varDecls = [VariableDeclSyntax]() + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + node.isProtocol((any NamedDeclSyntax).self) ? .skipChildren : .visitChildren + } + + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { + initializers.append(node) + return .skipChildren + } + + override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + varDecls.append(node) + return .skipChildren + } +} + private extension StructDeclSyntax { var unneededInitializers: [InitializerDeclSyntax] { - let unneededInitializers = findUnneededInitializers() - let initializersCount = memberBlock.members.filter { $0.decl.is(InitializerDeclSyntax.self) }.count - if unneededInitializers.count == initializersCount { + let collector = ElementCollector(viewMode: .sourceAccurate) + collector.walk(memberBlock) + let unneededInitializers = findUnneededInitializers(in: collector) + if unneededInitializers.count == collector.initializers.count { return unneededInitializers } return [] } - // Collects all of the initializers that could be replaced by the synthesized + // Finds all of the initializers that could be replaced by the synthesized // memberwise or default initializer(s). - private func findUnneededInitializers() -> [InitializerDeclSyntax] { - var storedProperties: [VariableDeclSyntax] = [] - var initializers: [InitializerDeclSyntax] = [] - - for memberItem in memberBlock.members { - let member = memberItem.decl - // Collect all stored variables into a list. - if let varDecl = member.as(VariableDeclSyntax.self) { - if !varDecl.modifiers.contains(keyword: .static) { - storedProperties.append(varDecl) - } - } else if let initDecl = member.as(InitializerDeclSyntax.self), - initDecl.optionalMark == nil, - !initDecl.hasThrowsOrRethrowsKeyword { - // Collect any possible redundant initializers into a list. - initializers.append(initDecl) - } + private func findUnneededInitializers(in collector: ElementCollector) -> [InitializerDeclSyntax] { + let initializers = collector.initializers.filter { + $0.optionalMark == nil && !$0.hasThrowsOrRethrowsKeyword } - + let varDecls = collector.varDecls.filter { !$0.modifiers.contains(keyword: .static) } return initializers.filter { - self.initializerParameters($0.parameterList, match: storedProperties) && + self.initializerParameters($0.parameterList, match: varDecls) && (($0.parameterList.isEmpty && hasNoSideEffects($0.body)) || - initializerBody($0.body, matches: storedProperties)) && - initializerModifiers($0.modifiers, match: storedProperties) && !$0.isInlinable + initializerBody($0.body, matches: varDecls)) && + initializerModifiers($0.modifiers, match: varDecls) && $0.attributes.isEmpty } } @@ -126,7 +132,7 @@ private extension StructDeclSyntax { } for (idx, parameter) in initializerParameters.enumerated() { - guard parameter.secondName == nil else { + guard parameter.secondName == nil, parameter.attributes.isEmpty else { return false } let property = storedProperties[idx] @@ -228,11 +234,9 @@ private extension StructDeclSyntax { private extension InitializerDeclSyntax { var hasThrowsOrRethrowsKeyword: Bool { - signature.effectSpecifiers?.throwsSpecifier != nil - } - var isInlinable: Bool { - attributes.contains(attributeNamed: "inlinable") + signature.effectSpecifiers?.throwsClause?.throwsSpecifier != nil } + var parameterList: FunctionParameterListSyntax { signature.parameterClause.parameters } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRuleExamples.swift index dfdb0d4437..ebeec84c4e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRuleExamples.swift @@ -197,7 +197,59 @@ enum UnneededSynthesizedInitializerRuleExamples { print("perform side effect") } } - """) + """), + Example(""" + struct Foo { + var bar: Int + + init(@Clamped bar: Int) { + self.bar = bar + } + } + """), + Example(""" + struct Foo { + let bar: Int + + init(bar: Int) { + self.bar = bar + } + init?() { + return nil + } + } + """), + // Treat conditional code as if it was active. + Example(""" + struct Foo { + var bar: String + + init(bar: String) { + self.bar = bar + } + + #if DEBUG + init() { + self.bar = "" + } + #endif + } + """, excludeFromDocumentation: true), + Example(""" + struct Foo { + #if DEBUG + var bar: String + #endif + + init() {} + } + """, excludeFromDocumentation: true), + Example(""" + struct Foo { + @available(*, unavailable) + init() {} + } + """), ] static let triggering = [ @@ -328,7 +380,21 @@ enum UnneededSynthesizedInitializerRuleExamples { } } } - """) + """), + Example(""" + struct Foo { + let i: Int + struct Bar { + let j: Int + ↓init(j: Int) { + self.j = j + } + } + ↓init(i: Int) { + self.i = i + } + } + """), ] static let corrections = [ @@ -479,6 +545,6 @@ enum UnneededSynthesizedInitializerRuleExamples { let prop: Int } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UntypedErrorInCatchRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UntypedErrorInCatchRule.swift index 386366dc3e..fb793a1406 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UntypedErrorInCatchRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UntypedErrorInCatchRule.swift @@ -41,7 +41,7 @@ struct UntypedErrorInCatchRule: OptInRule { } catch { print(error) } - """) + """), ], triggeringExamples: [ Example(""" @@ -78,12 +78,12 @@ struct UntypedErrorInCatchRule: OptInRule { do { try foo() } ↓catch (let error) {} - """) + """), ], corrections: [ Example("do {\n try foo() \n} ↓catch let error {}"): Example("do {\n try foo() \n} catch {}"), Example("do {\n try foo() \n} ↓catch(let error) {}"): Example("do {\n try foo() \n} catch {}"), - Example("do {\n try foo() \n} ↓catch (let error) {}"): Example("do {\n try foo() \n} catch {}") + Example("do {\n try foo() \n} ↓catch (let error) {}"): Example("do {\n try foo() \n} catch {}"), ]) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift index f3a2de8d99..da2c628ba9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift @@ -18,57 +18,238 @@ struct UnusedEnumeratedRule: Rule { Example("for (idx, _) in bar.enumerated().something() { }"), Example("for (idx, _) in bar.something() { }"), Example("for idx in bar.indices { }"), - Example("for (section, (event, _)) in data.enumerated() {}") + Example("for (section, (event, _)) in data.enumerated() {}"), + Example("list.enumerated().map { idx, elem in \"\\(idx): \\(elem)\" }"), + Example("list.enumerated().map { $0 + $1 }"), + Example("list.enumerated().something().map { _, elem in elem }"), + Example("list.enumerated().map { ($0.offset, $0.element) }"), + Example("list.enumerated().map { ($0.0, $0.1) }"), + Example(""" + list.enumerated().map { + $1.enumerated().forEach { print($0, $1) } + return $0 + } + """), + Example(""" + list.enumerated().forEach { + f($0) + let (i, e) = $0 + print(i) + } + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example("for (↓_, foo) in bar.enumerated() { }"), Example("for (↓_, foo) in abc.bar.enumerated() { }"), Example("for (↓_, foo) in abc.something().enumerated() { }"), - Example("for (idx, ↓_) in bar.enumerated() { }") + Example("for (idx, ↓_) in bar.enumerated() { }"), + Example("list.enumerated().map { idx, ↓_ in idx }"), + Example("list.enumerated().map { ↓_, elem in elem }"), + Example("list.↓enumerated().forEach { print($0) }"), + Example("list.↓enumerated().map { $1 }"), + Example(""" + list.enumerated().map { + $1.↓enumerated().forEach { print($1) } + return $0 + } + """), + Example(""" + list.↓enumerated().map { + $1.enumerated().forEach { print($0, $1) } + return 1 + } + """), + Example(""" + list.enumerated().map { + $1.enumerated().filter { + print($0, $1) + $1.↓enumerated().forEach { + if $1 == 2 { + return true + } + } + return false + } + return $0 + } + """, excludeFromDocumentation: true) + , + Example(""" + list.↓enumerated().map { + $1.forEach { print($0) } + return $1 + } + """, excludeFromDocumentation: true), + Example(""" + list.↓enumerated().forEach { + let (i, _) = $0 + } + """), ] ) } private extension UnusedEnumeratedRule { + private struct Closure { + let enumeratedPosition: AbsolutePosition? + var zeroPosition: AbsolutePosition? + var onePosition: AbsolutePosition? + + init(enumeratedPosition: AbsolutePosition? = nil) { + self.enumeratedPosition = enumeratedPosition + } + } + final class Visitor: ViolationsSyntaxVisitor { + private var nextClosureId: SyntaxIdentifier? + private var lastEnumeratedPosition: AbsolutePosition? + private var closures = Stack() + override func visitPost(_ node: ForStmtSyntax) { guard let tuplePattern = node.pattern.as(TuplePatternSyntax.self), tuplePattern.elements.count == 2, let functionCall = node.sequence.asFunctionCall, functionCall.isEnumerated, let firstElement = tuplePattern.elements.first, - let secondElement = tuplePattern.elements.last, - case let firstTokenIsUnderscore = firstElement.isUnderscore, - case let lastTokenIsUnderscore = secondElement.isUnderscore, - firstTokenIsUnderscore || lastTokenIsUnderscore else { + let secondElement = tuplePattern.elements.last + else { return } - let position: AbsolutePosition - let reason: String - if firstTokenIsUnderscore { - position = firstElement.positionAfterSkippingLeadingTrivia - reason = "When the index is not used, `.enumerated()` can be removed" + let firstTokenIsUnderscore = firstElement.isUnderscore + let lastTokenIsUnderscore = secondElement.isUnderscore + guard firstTokenIsUnderscore || lastTokenIsUnderscore else { + return + } + + addViolation( + zeroPosition: firstTokenIsUnderscore ? firstElement.positionAfterSkippingLeadingTrivia : nil, + onePosition: firstTokenIsUnderscore ? nil : secondElement.positionAfterSkippingLeadingTrivia + ) + } + + override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { + guard node.isEnumerated, + let parent = node.parent, + parent.as(MemberAccessExprSyntax.self)?.declName.baseName.text != "filter", + let trailingClosure = parent.parent?.as(FunctionCallExprSyntax.self)?.trailingClosure + else { + return .visitChildren + } + + if let parameterClause = trailingClosure.signature?.parameterClause { + guard let parameterClause = parameterClause.as(ClosureShorthandParameterListSyntax.self), + parameterClause.count == 2, + let firstElement = parameterClause.first, + let secondElement = parameterClause.last + else { + return .visitChildren + } + + let firstTokenIsUnderscore = firstElement.isUnderscore + let lastTokenIsUnderscore = secondElement.isUnderscore + guard firstTokenIsUnderscore || lastTokenIsUnderscore else { + return .visitChildren + } + + addViolation( + zeroPosition: firstTokenIsUnderscore ? firstElement.positionAfterSkippingLeadingTrivia : nil, + onePosition: firstTokenIsUnderscore ? nil : secondElement.positionAfterSkippingLeadingTrivia + ) + } else { + nextClosureId = trailingClosure.id + lastEnumeratedPosition = node.enumeratedPosition + } + + return .visitChildren + } + + override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + if let nextClosureId, nextClosureId == node.id, let lastEnumeratedPosition { + closures.push(Closure(enumeratedPosition: lastEnumeratedPosition)) + self.nextClosureId = nil + self.lastEnumeratedPosition = nil } else { - position = secondElement.positionAfterSkippingLeadingTrivia + closures.push(Closure()) + } + return .visitChildren + } + + override func visitPost(_: ClosureExprSyntax) { + if let closure = closures.pop(), (closure.zeroPosition != nil) != (closure.onePosition != nil) { + addViolation( + zeroPosition: closure.onePosition, + onePosition: closure.zeroPosition, + enumeratedPosition: closure.enumeratedPosition + ) + } + } + + override func visitPost(_ node: DeclReferenceExprSyntax) { + guard + let closure = closures.peek(), + closure.enumeratedPosition != nil, + node.baseName.text == "$0" || node.baseName.text == "$1" + else { + return + } + closures.modifyLast { + if node.baseName.text == "$0" { + let member = node.parent?.as(MemberAccessExprSyntax.self)?.declName.baseName.text + if member == "element" || member == "1" { + $0.onePosition = node.positionAfterSkippingLeadingTrivia + } else { + $0.zeroPosition = node.positionAfterSkippingLeadingTrivia + if node.isUnpacked { + $0.onePosition = node.positionAfterSkippingLeadingTrivia + } + } + } else { + $0.onePosition = node.positionAfterSkippingLeadingTrivia + } + } + } + + private func addViolation( + zeroPosition: AbsolutePosition?, + onePosition: AbsolutePosition?, + enumeratedPosition: AbsolutePosition? = nil + ) { + var position: AbsolutePosition? + var reason: String? + if let zeroPosition { + position = zeroPosition + reason = "When the index is not used, `.enumerated()` can be removed" + } else if let onePosition { + position = onePosition reason = "When the item is not used, `.indices` should be used instead of `.enumerated()`" } - violations.append(ReasonedRuleViolation(position: position, reason: reason)) + if let enumeratedPosition { + position = enumeratedPosition + } + + if let position, let reason { + violations.append(ReasonedRuleViolation(position: position, reason: reason)) + } } } } private extension FunctionCallExprSyntax { var isEnumerated: Bool { - guard let memberAccess = calledExpression.as(MemberAccessExprSyntax.self), + enumeratedPosition != nil + } + + var enumeratedPosition: AbsolutePosition? { + if let memberAccess = calledExpression.as(MemberAccessExprSyntax.self), memberAccess.base != nil, memberAccess.declName.baseName.text == "enumerated", - hasNoArguments else { - return false + hasNoArguments { + return memberAccess.declName.positionAfterSkippingLeadingTrivia } - return true + return nil } var hasNoArguments: Bool { @@ -83,3 +264,20 @@ private extension TuplePatternElementSyntax { pattern.is(WildcardPatternSyntax.self) } } + +private extension ClosureShorthandParameterSyntax { + var isUnderscore: Bool { + name.tokenKind == .wildcard + } +} + +private extension DeclReferenceExprSyntax { + var isUnpacked: Bool { + if let initializer = parent?.as(InitializerClauseSyntax.self), + let binding = initializer.parent?.as(PatternBindingSyntax.self), + let elements = binding.pattern.as(TuplePatternSyntax.self)?.elements { + return elements.count == 2 && elements.allSatisfy { !$0.pattern.is(WildcardPatternSyntax.self) } + } + return false + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift index b569de22c7..8d6cc648c5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift @@ -59,7 +59,7 @@ struct VoidFunctionInTernaryConditionRule: Rule { Example(""" subscript(index: Int) -> Int { index == 0 ? defaultValue() : compute(index) - """) + """), ], triggeringExamples: [ Example("success ↓? askQuestion() : exit()"), @@ -103,7 +103,7 @@ struct VoidFunctionInTernaryConditionRule: Rule { index == 0 ↓? something() : somethingElse(index) return index } - """) + """), ] ) } @@ -144,7 +144,7 @@ private extension VoidFunctionInTernaryConditionRule { private extension ExprListSyntax { var containsAssignment: Bool { - return children(viewMode: .sourceAccurate).contains(where: { $0.is(AssignmentExprSyntax.self) }) + children(viewMode: .sourceAccurate).contains(where: { $0.is(AssignmentExprSyntax.self) }) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTFailMessageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTFailMessageRule.swift index 1c3c2fd866..13309b5b44 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTFailMessageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTFailMessageRule.swift @@ -19,7 +19,7 @@ struct XCTFailMessageRule: Rule { func testFoo() { XCTFail(bar) } - """) + """), ], triggeringExamples: [ Example(""" @@ -31,7 +31,7 @@ struct XCTFailMessageRule: Rule { func testFoo() { ↓XCTFail("") } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift index 7979adc1cc..2a762a01b9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift @@ -122,9 +122,9 @@ private enum TwoArgsXCTAssert: String { // let arguments = node.arguments .prefix(2) - .map { $0.expression.trimmedDescription } + .map(\.expression.trimmedDescription) .sorted { arg1, _ -> Bool in - return protectedArguments.contains(arg1) + protectedArguments.contains(arg1) } // diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift index accabe8f77..cade2be89b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRuleExamples.swift @@ -63,7 +63,7 @@ internal struct XCTSpecificMatcherRuleExamples { // Skip if one operand might be a type or a tuple Example("XCTAssert(foo.self == bar)"), Example("XCTAssertTrue(type(of: foo) != Int.self)"), - Example("XCTAssertTrue(a == (1, 3, 5)") + Example("XCTAssertTrue(a == (1, 3, 5)"), ] static let triggeringExamples = [ @@ -136,6 +136,6 @@ internal struct XCTSpecificMatcherRuleExamples { Example("↓XCTAssert(nil == foo"), Example("↓XCTAssertTrue( foo != nil)"), Example("↓XCTAssertFalse(nil != foo"), - Example("↓XCTAssert(foo == nil, \"toto\")") + Example("↓XCTAssert(foo == nil, \"toto\")"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift index 18dfda9d8e..c946cef5e4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift @@ -25,7 +25,8 @@ struct AccessibilityLabelForImageRule: ASTRule, OptInRule { // MARK: AST Rule - func validate(file: SwiftLintFile, kind: SwiftDeclarationKind, + func validate(file: SwiftLintFile, + kind: SwiftDeclarationKind, dictionary: SourceKittenDictionary) -> [StyleViolation] { // Only proceed to check View structs. guard kind == .struct, @@ -97,7 +98,7 @@ private extension SourceKittenDictionary { // Image(decorative: "myImage").resizable().frame // --> Image(decorative: "myImage").resizable // --> Image - return substructure.contains(where: { $0.isImage }) + return substructure.contains(where: \.isImage) } /// Whether or not the dictionary represents a SwiftUI Image using the `Image(decorative:)` constructor (hides @@ -118,13 +119,13 @@ private extension SourceKittenDictionary { // Image(decorative: "myImage").resizable().frame // --> Image(decorative: "myImage").resizable // --> Image - return substructure.contains(where: { $0.isDecorativeOrLabeledImage }) + return substructure.contains(where: \.isDecorativeOrLabeledImage) } /// Whether or not the dictionary represents a SwiftUI View with an `accesibilityLabel(_:)` /// or `accessibility(label:)` modifier. func hasAccessibilityLabelModifier(in file: SwiftLintFile) -> Bool { - return hasModifier( + hasModifier( anyOf: [ SwiftUIModifier( name: "accessibilityLabel", @@ -133,7 +134,7 @@ private extension SourceKittenDictionary { SwiftUIModifier( name: "accessibility", arguments: [.init(name: "label", values: [])] - ) + ), ], in: file ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift index 8ba3214e16..ede1e90e84 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift @@ -148,7 +148,7 @@ internal struct AccessibilityLabelForImageRuleExamples { .accessibilityLabel(Text("Label for my image")) } } - """) + """), ] static let triggeringExamples = [ @@ -264,6 +264,6 @@ internal struct AccessibilityLabelForImageRuleExamples { ↓Image(systemName: "circle.plus") } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift index 62ab87460f..c14ffb7b8c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift @@ -23,7 +23,8 @@ struct AccessibilityTraitForButtonRule: ASTRule, OptInRule { // MARK: AST Rule - func validate(file: SwiftLintFile, kind: SwiftDeclarationKind, + func validate(file: SwiftLintFile, + kind: SwiftDeclarationKind, dictionary: SourceKittenDictionary) -> [StyleViolation] { // Only proceed to check View structs. guard kind == .struct, @@ -88,7 +89,7 @@ private extension SourceKittenDictionary { /// or by a `gesture`, `simultaneousGesture`, or `highPriorityGesture` modifier with an argument /// starting with a `TapGesture` object with a count of 1 (default value is 1). func hasOnSingleTapModifier(in file: SwiftLintFile) -> Bool { - return hasModifier( + hasModifier( anyOf: [ SwiftUIModifier( name: "onTapGesture", @@ -111,7 +112,7 @@ private extension SourceKittenDictionary { arguments: [ .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) ] - ) + ), ], in: file ) @@ -120,7 +121,7 @@ private extension SourceKittenDictionary { /// Whether or not the dictionary represents a SwiftUI View with an `accessibilityAddTraits()` or /// `accessibility(addTraits:)` modifier with the specified trait (specify trait as a String). func hasAccessibilityTrait(_ trait: String, in file: SwiftLintFile) -> Bool { - return hasModifier( + hasModifier( anyOf: [ SwiftUIModifier( name: "accessibilityAddTraits", @@ -129,7 +130,7 @@ private extension SourceKittenDictionary { SwiftUIModifier( name: "accessibility", arguments: [.init(name: "addTraits", values: [trait], matchType: .substring)] - ) + ), ], in: file ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRuleExamples.swift index ea987cf1ed..570411695d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRuleExamples.swift @@ -168,7 +168,7 @@ internal struct AccessibilityTraitForButtonRuleExamples { }) } } - """) + """), ] static let triggeringExamples = [ @@ -265,6 +265,6 @@ internal struct AccessibilityTraitForButtonRuleExamples { }) } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AnyObjectProtocolRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AnyObjectProtocolRule.swift deleted file mode 100644 index 325f68b121..0000000000 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AnyObjectProtocolRule.swift +++ /dev/null @@ -1,77 +0,0 @@ -import SwiftSyntax - -// TODO: [09/07/2024] Remove deprecation warning after ~2 years. -private let warnDeprecatedOnceImpl: Void = { - Issue.ruleDeprecated(ruleID: AnyObjectProtocolRule.description.identifier).print() -}() - -private func warnDeprecatedOnce() { - _ = warnDeprecatedOnceImpl -} - -struct AnyObjectProtocolRule: SwiftSyntaxCorrectableRule, OptInRule { - var configuration = SeverityConfiguration(.warning) - - static let description = RuleDescription( - identifier: "anyobject_protocol", - name: "AnyObject Protocol", - description: "Prefer using `AnyObject` over `class` for class-only protocols", - kind: .lint, - nonTriggeringExamples: [ - Example("protocol SomeProtocol {}"), - Example("protocol SomeClassOnlyProtocol: AnyObject {}"), - Example("protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {}"), - Example("@objc protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {}") - ], - triggeringExamples: [ - Example("protocol SomeClassOnlyProtocol: ↓class {}"), - Example("protocol SomeClassOnlyProtocol: ↓class, SomeInheritedProtocol {}"), - Example("@objc protocol SomeClassOnlyProtocol: ↓class, SomeInheritedProtocol {}") - ], - corrections: [ - Example("protocol SomeClassOnlyProtocol: ↓class {}"): - Example("protocol SomeClassOnlyProtocol: AnyObject {}"), - Example("protocol SomeClassOnlyProtocol: ↓class, SomeInheritedProtocol {}"): - Example("protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {}"), - Example("@objc protocol SomeClassOnlyProtocol: ↓class, SomeInheritedProtocol {}"): - Example("@objc protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {}") - ] - ) - - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - warnDeprecatedOnce() - return Visitor(configuration: configuration, file: file) - } - - func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? { - Rewriter(configuration: configuration, file: file) - } -} - -private extension AnyObjectProtocolRule { - final class Visitor: ViolationsSyntaxVisitor { - override func visitPost(_ node: ClassRestrictionTypeSyntax) { - violations.append(node.positionAfterSkippingLeadingTrivia) - } - } - - final class Rewriter: ViolationsSyntaxRewriter { - override func visit(_ node: InheritedTypeSyntax) -> InheritedTypeSyntax { - let typeName = node.type - guard typeName.is(ClassRestrictionTypeSyntax.self) else { - return super.visit(node) - } - correctionPositions.append(node.positionAfterSkippingLeadingTrivia) - return super.visit( - node.with( - \.type, - TypeSyntax( - IdentifierTypeSyntax(name: .identifier("AnyObject"), genericArgumentClause: nil) - .with(\.leadingTrivia, typeName.leadingTrivia) - .with(\.trailingTrivia, typeName.trailingTrivia) - ) - ) - ) - } - } -} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift index fc8bf2def1..9d36b2c15d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift @@ -1,7 +1,7 @@ import SwiftSyntax @SwiftSyntaxRule -struct ArrayInitRule: OptInRule { +struct ArrayInitRule: OptInRule, @unchecked Sendable { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -19,7 +19,7 @@ struct ArrayInitRule: OptInRule { Example("foo.map { $0! /* force unwrap */ }"), Example("foo.something { RouteMapper.map($0) }"), Example("foo.map { !$0 }"), - Example("foo.map { /* a comment */ !$0 }") + Example("foo.map { /* a comment */ !$0 }"), ], triggeringExamples: [ Example("foo.↓map({ $0 })"), @@ -29,24 +29,24 @@ struct ArrayInitRule: OptInRule { foo.↓map { elem in elem } - """), + """), Example(""" foo.↓map { elem in return elem } - """), + """), Example(""" foo.↓map { (elem: String) in elem } - """), + """), Example(""" foo.↓map { elem -> String in elem } - """), + """), Example("foo.↓map { $0 /* a comment */ }"), - Example("foo.↓map { /* a comment */ $0 }") + Example("foo.↓map { /* a comment */ $0 }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift new file mode 100644 index 0000000000..0604a9fc60 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift @@ -0,0 +1,156 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule +struct AsyncWithoutAwaitRule: SwiftSyntaxCorrectableRule, OptInRule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "async_without_await", + name: "Async Without Await", + description: "Declaration should not be async if it doesn't use await", + kind: .lint, + nonTriggeringExamples: AsyncWithoutAwaitRuleExamples.nonTriggeringExamples, + triggeringExamples: AsyncWithoutAwaitRuleExamples.triggeringExamples, + corrections: AsyncWithoutAwaitRuleExamples.corrections + ) +} +private extension AsyncWithoutAwaitRule { + private struct FuncInfo { + var containsAwait = false + let asyncToken: TokenSyntax? + + init(asyncToken: TokenSyntax?) { + self.asyncToken = asyncToken + } + } + + final class Visitor: ViolationsSyntaxVisitor { + private var functionScopes = Stack() + private var pendingAsync: TokenSyntax? + + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.body != nil else { + return .visitChildren + } + + let asyncToken = node.signature.effectSpecifiers?.asyncSpecifier + functionScopes.push(.init(asyncToken: asyncToken)) + + return .visitChildren + } + + override func visitPost(_ node: FunctionDeclSyntax) { + if node.body != nil { + checkViolation() + } + } + + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + functionScopes.push(.init(asyncToken: pendingAsync)) + pendingAsync = nil + + return .visitChildren + } + + override func visitPost(_: ClosureExprSyntax) { + checkViolation() + } + + override func visitPost(_: AwaitExprSyntax) { + functionScopes.modifyLast { + $0.containsAwait = true + } + } + + override func visitPost(_ node: FunctionTypeSyntax) { + if let asyncNode = node.effectSpecifiers?.asyncSpecifier { + pendingAsync = asyncNode + } + } + + override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.body != nil else { + return .visitChildren + } + + let asyncToken = node.effectSpecifiers?.asyncSpecifier + functionScopes.push(.init(asyncToken: asyncToken)) + + return .visitChildren + } + + override func visitPost(_ node: AccessorDeclSyntax) { + if node.body != nil { + checkViolation() + } + } + + override func visitPost(_: PatternBindingSyntax) { + pendingAsync = nil + } + + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.body != nil else { + return .visitChildren + } + + let asyncToken = node.signature.effectSpecifiers?.asyncSpecifier + functionScopes.push(.init(asyncToken: asyncToken)) + + return .visitChildren + } + + override func visitPost(_ node: InitializerDeclSyntax) { + if node.body != nil { + checkViolation() + } + } + + override func visitPost(_: TypeAliasDeclSyntax) { + pendingAsync = nil + } + + override func visitPost(_ node: ForStmtSyntax) { + if node.awaitKeyword != nil { + functionScopes.modifyLast { + $0.containsAwait = true + } + } + } + + override func visitPost(_ node: VariableDeclSyntax) { + if node.bindingSpecifier.tokenKind == .keyword(.let), + node.modifiers.contains(keyword: .async) { + functionScopes.modifyLast { + $0.containsAwait = true + } + } + } + + override func visit(_: FunctionParameterSyntax) -> SyntaxVisitorContinueKind { + .skipChildren + } + + override func visitPost(_: ReturnClauseSyntax) { + pendingAsync = nil + } + + private func checkViolation() { + guard let info = functionScopes.pop(), + let asyncToken = info.asyncToken, + !info.containsAwait else { + return + } + + violations.append( + at: asyncToken.positionAfterSkippingLeadingTrivia, + correction: .init( + start: asyncToken.positionAfterSkippingLeadingTrivia, + end: asyncToken.endPosition, + replacement: "" + ) + ) + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift new file mode 100644 index 0000000000..205dcae8fc --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRuleExamples.swift @@ -0,0 +1,338 @@ +internal struct AsyncWithoutAwaitRuleExamples { + static let nonTriggeringExamples = [ + Example(""" + func test() { + func test() async { + await test() + } + }, + """), + Example(""" + func test() { + func test() async { + await test().value + } + }, + """), + Example(""" + func test() async { + await scheduler.task { foo { bar() } } + } + """), + Example(""" + func test() async { + perform(await try foo().value) + } + """), + Example(""" + func test() async { + perform(try await foo()) + } + """), + Example(""" + func test() async { + await perform() + func baz() { + qux() + } + } + """), + Example(""" + let x: () async -> Void = { + await test() + } + """), + Example(""" + let x: () async -> Void = { + await { await test() }() + } + """), + Example(""" + func test() async { + await foo() + } + let x = { bar() } + """), + Example(""" + let x: (() async -> Void)? = { + await { await test() }() + } + """), + Example(""" + let x: (() async -> Void)? = nil + let x: () -> Void = { test() } + """), + Example(""" + var test: Int { + get async throws { + try await foo() + } + } + var foo: Int { + get throws { + try bar() + } + } + """), + Example(""" + init() async { + await foo() + } + """), + Example(""" + init() async { + func test() async { + await foo() + } + await { await foo() }() + } + """), + Example(""" + subscript(row: Int) -> Double { + get async { + await foo() + } + } + """), + Example(""" + func foo() async -> Int + func bar() async -> Int + """), + Example(""" + var foo: Int { get async } + var bar: Int { get async } + """), + Example(""" + init(foo: bar) async + init(baz: qux) async + let baz = { qux() } + """), + Example(""" + typealias Foo = () async -> Void + typealias Bar = () async -> Void + let baz = { qux() } + """), + Example(""" + func test() async { + for await foo in bar {} + } + """), + Example(""" + func test() async { + while let foo = await bar() {} + } + """), + Example(""" + func test() async { + async let foo = bar() + let baz = await foo + } + """), + Example(""" + func test() async { + async let foo = bar() + await foo + } + """), + Example(""" + func test() async { + async let foo = bar() + } + """), + Example("func foo(bar: () async -> Void) { { } }"), + Example("func foo(bar: () async -> Void = { await baz() }) { {} }"), + Example("func foo() -> (() async -> Void)? { {} }"), + Example(""" + func foo( + bar: () async -> Void, + baz: () -> Void = {} + ) { { } } + """), + Example("func foo(bar: () async -> Void = {}) { }"), + ] + + static let triggeringExamples = [ + Example(""" + func test() ↓async { + perform() + } + """), + Example(""" + func test() { + func baz() ↓async { + qux() + } + perform() + func baz() { + qux() + } + } + """), + Example(""" + func test() ↓async { + func baz() async { + await qux() + } + } + """), + Example(""" + func test() ↓async { + func foo() ↓async {} + let bar = { await foo() } + } + """), + Example(""" + func test() ↓async { + let bar = { + func foo() ↓async {} + } + } + """), + Example("let x: (() ↓async -> Void)? = { test() }"), + Example(""" + var test: Int { + get ↓async throws { + foo() + } + } + """), + Example(""" + var test: Int { + get ↓async throws { + func foo() ↓async {} + let bar = { await foo() } + } + } + """), + Example(""" + var test: Int { + get throws { + func foo() {} + let bar: () ↓async -> Void = { foo() } + } + } + """), + Example("init() ↓async {}"), + Example(""" + init() ↓async { + func foo() ↓async {} + let bar: () ↓async -> Void = { foo() } + } + """), + Example(""" + subscript(row: Int) -> Double { + get ↓async { + 1.0 + } + } + """), + Example(""" + func test() ↓async { + for foo in bar {} + } + """), + Example(""" + func test() ↓async { + while let foo = bar() {} + } + """), + ] + + static let corrections = [ + Example("func test() ↓async {}"): Example("func test() {}"), + Example("func test() ↓async throws {}"): Example("func test() throws {}"), + Example(""" + func test() { + func baz() ↓async { + qux() + } + perform() + func baz() { + qux() + } + } + """): + Example(""" + func test() { + func baz() { + qux() + } + perform() + func baz() { + qux() + } + } + """), + Example(""" + func test() ↓async{ + func baz() async { + await qux() + } + } + """): + Example(""" + func test() { + func baz() async { + await qux() + } + } + """), + Example(""" + func test() ↓async { + func foo() ↓async {} + let bar = { await foo() } + } + """): + Example(""" + func test() { + func foo() {} + let bar = { await foo() } + } + """), + Example("let x: () ↓async -> Void = { test() }"): + Example("let x: () -> Void = { test() }"), + Example(""" + var test: Int { + get ↓async throws { + func foo() ↓async {} + let bar = { await foo() } + } + } + """): + Example(""" + var test: Int { + get throws { + func foo() {} + let bar = { await foo() } + } + } + """), + Example("init() ↓async {}"): Example("init() {}"), + Example(""" + init() ↓async { + func foo() ↓async {} + let bar: () ↓async -> Void = { foo() } + } + """): + Example(""" + init() { + func foo() {} + let bar: () -> Void = { foo() } + } + """), + Example(""" + subscript(row: Int) -> Double { + get ↓async { + foo() + } + } + """): + Example(""" + subscript(row: Int) -> Double { + get { + foo() + } + } + """), + ] +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/BalancedXCTestLifecycleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/BalancedXCTestLifecycleRule.swift index e1dbbebd0a..9c29b21dd0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/BalancedXCTestLifecycleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/BalancedXCTestLifecycleRule.swift @@ -56,7 +56,7 @@ struct BalancedXCTestLifecycleRule: OptInRule { class func setUp() {} class func tearDown() {} } - """#) + """#), ], triggeringExamples: [ Example(#""" @@ -101,7 +101,7 @@ struct BalancedXCTestLifecycleRule: OptInRule { final class ↓BarTests: XCTestCase { override func tearDownWithError() throws {} } - """#) + """#), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift index 3229b27f30..2a576740f7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift @@ -4,7 +4,11 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { static let description = RuleDescription( identifier: "blanket_disable_command", name: "Blanket Disable Command", - description: "swiftlint:disable commands should be re-enabled before the end of the file", + description: """ + `swiftlint:disable` commands should use `next`, `this` or `previous` to disable rules for a \ + single line, or `swiftlint:enable` to re-enable the rules immediately after the violations \ + to be ignored, instead of disabling the rule for the rest of the file. + """, kind: .lint, nonTriggeringExamples: [ Example(""" @@ -18,7 +22,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { """), Example("// swiftlint:disable:this unused_import"), Example("// swiftlint:disable:next unused_import"), - Example("// swiftlint:disable:previous unused_import") + Example("// swiftlint:disable:previous unused_import"), ], triggeringExamples: [ Example("// swiftlint:disable ↓unused_import"), @@ -33,7 +37,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { """), Example(""" // swiftlint:enable ↓unused_import - """) + """), ].skipWrappingInCommentTests().skipDisableCommandTests() ) @@ -144,8 +148,11 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { } if let command = ruleIdentifierToCommandMap[disabledRuleIdentifier] { - let reason = "The disabled '\(disabledRuleIdentifier.stringRepresentation)' rule " + - "should be re-enabled before the end of the file" + let reason = """ + Use 'next', 'this' or 'previous' instead to disable the \ + '\(disabledRuleIdentifier.stringRepresentation)' rule once, \ + or re-enable it as soon as possible` + """ return violation(for: command, ruleIdentifier: disabledRuleIdentifier, in: file, reason: reason) } return nil @@ -160,7 +167,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { } for command in file.commands { - let ruleIdentifiers: Set = Set(command.ruleIdentifiers.map { $0.stringRepresentation }) + let ruleIdentifiers: Set = Set(command.ruleIdentifiers.map(\.stringRepresentation)) let intersection = ruleIdentifiers.intersection(configuration.alwaysBlanketDisableRuleIdentifiers) if command.action == .enable { violations.append(contentsOf: intersection.map { diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift index 388c0e96a1..bbf8d915ff 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/CaptureVariableRule.swift @@ -72,7 +72,7 @@ struct CaptureVariableRule: AnalyzerRule, CollectingRule { closure() j = 1 closure() - """) + """), ], triggeringExamples: [ Example(""" @@ -147,18 +147,19 @@ struct CaptureVariableRule: AnalyzerRule, CollectingRule { func test(_ completionHandler: @escaping (Int) -> Void) { } } - """) + """), ], requiresFileOnDisk: true ) var configuration = SeverityConfiguration(.warning) - func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> CaptureVariableRule.FileInfo { + func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> Self.FileInfo { file.declaredVariables(compilerArguments: compilerArguments) } - func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: CaptureVariableRule.FileInfo], + func validate(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: Self.FileInfo], compilerArguments: [String]) -> [StyleViolation] { file.captureListVariables(compilerArguments: compilerArguments) .filter { capturedVariable in collectedInfo.values.contains { $0.contains(capturedVariable.usr) } } @@ -206,13 +207,13 @@ private extension SwiftLintFile { let offsets = self.captureListVariableOffsets() guard !offsets.isEmpty, let indexEntities = index(compilerArguments: compilerArguments) else { return Set() } - return Set(indexEntities.traverseEntitiesDepthFirst { + return Set(indexEntities.traverseEntitiesDepthFirst { _, entity in guard - let kind = $0.kind, + let kind = entity.kind, kind.hasPrefix("source.lang.swift.ref.var."), - let usr = $0.usr, - let line = $0.line, - let column = $0.column, + let usr = entity.usr, + let line = entity.line, + let column = entity.column, let offset = stringView.byteOffset(forLine: line, bytePosition: column) else { return nil } return offsets.contains(offset) ? CaptureVariableRule.Variable(usr: usr, offset: offset) : nil @@ -245,16 +246,16 @@ private extension SwiftLintFile { let offsets = self.declaredVariableOffsets() guard !offsets.isEmpty, let indexEntities = index(compilerArguments: compilerArguments) else { return Set() } - return Set(indexEntities.traverseEntitiesDepthFirst { + return Set(indexEntities.traverseEntitiesDepthFirst { _, entity in guard - let declarationKind = $0.declarationKind, + let declarationKind = entity.declarationKind, Self.checkedDeclarationKinds.contains(declarationKind), - let line = $0.line, - let column = $0.column, + let line = entity.line, + let column = entity.column, let offset = stringView.byteOffset(forLine: line, bytePosition: column), offsets.contains(offset) else { return nil } - return $0.usr + return entity.usr }) } @@ -263,7 +264,7 @@ private extension SwiftLintFile { let path = self.path, let response = try? Request.index(file: path, arguments: compilerArguments).sendIfNotDisabled() else { - Issue.indexingError(path: path, ruleID: CaptureVariableRule.description.identifier).print() + Issue.indexingError(path: path, ruleID: CaptureVariableRule.identifier).print() return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift index 07fde3470a..ce556449b9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift @@ -21,12 +21,12 @@ struct ClassDelegateProtocolRule: Rule { Example("protocol FooDelegate: NSObjectProtocol {}"), Example("protocol FooDelegate where Self: BarDelegate {}"), Example("protocol FooDelegate where Self: AnyObject {}"), - Example("protocol FooDelegate where Self: NSObjectProtocol {}") + Example("protocol FooDelegate where Self: NSObjectProtocol {}"), ], triggeringExamples: [ Example("↓protocol FooDelegate {}"), Example("↓protocol FooDelegate: Bar {}"), - Example("↓protocol FooDelegate where Self: StringProtocol {}") + Example("↓protocol FooDelegate where Self: StringProtocol {}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/CommentSpacingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/CommentSpacingRule.swift index 464f15f134..edfd3f2bc0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/CommentSpacingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/CommentSpacingRule.swift @@ -57,8 +57,8 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { */ """), Example(""" - /*#-editable-code Swift Platground editable area*/default/*#-end-editable-code*/ - """) + /*#-editable-code Swift Playground editable area*/default/*#-end-editable-code*/ + """), ], triggeringExamples: [ Example(""" @@ -89,7 +89,7 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { """), Example(""" //:↓Swift Playground prose section - """) + """), ], corrections: [ Example("//↓Something"): Example("// Something"), @@ -113,7 +113,7 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { print("Something") } // We should improve above function - """) + """), ] ) @@ -123,7 +123,7 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { .filter(\.kind.isComment) .map { $0.range.toSourceKittenByteRange() } .compactMap { (range: ByteRange) -> [NSRange]? in - return file.stringView + file.stringView .substringWithByteRange(range) .map(StringView.init) .map { commentBody in @@ -134,7 +134,7 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { .compactMap { result in // Set the location to be directly before the first non-slash, // non-whitespace character which was matched - return file.stringView.byteRangeToNSRange( + file.stringView.byteRangeToNSRange( ByteRange( // Safe to mix NSRange offsets with byte offsets here because the regex can't // contain multi-byte characters @@ -149,7 +149,7 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { } func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(in: file).map { range in + violationRanges(in: file).map { range in StyleViolation( ruleDescription: Self.description, severity: configuration.severity, @@ -158,7 +158,7 @@ struct CommentSpacingRule: SourceKitFreeRule, SubstitutionCorrectableRule { } } - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { - return (violationRange, " ") + func substitution(for violationRange: NSRange, in _: SwiftLintFile) -> (NSRange, String)? { + (violationRange, " ") } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift index e3a41a0b4c..0227dcf8fe 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift @@ -12,13 +12,13 @@ struct CompilerProtocolInitRule: Rule { kind: .lint, nonTriggeringExamples: [ Example("let set: Set = [1, 2]"), - Example("let set = Set(array)") + Example("let set = Set(array)"), ], triggeringExamples: [ Example("let set = ↓Set(arrayLiteral: 1, 2)"), Example("let set = ↓Set (arrayLiteral: 1, 2)"), Example("let set = ↓Set.init(arrayLiteral: 1, 2)"), - Example("let set = ↓Set.init(arrayLiteral : 1, 2)") + Example("let set = ↓Set.init(arrayLiteral : 1, 2)"), ] ) } @@ -85,10 +85,12 @@ private struct ExpressibleByCompiler { initCallNames = Set(types.flatMap { [$0, "\($0).init"] }) } - static let allProtocols = [byArrayLiteral, byNilLiteral, byBooleanLiteral, - byFloatLiteral, byIntegerLiteral, byUnicodeScalarLiteral, - byExtendedGraphemeClusterLiteral, byStringLiteral, - byStringInterpolation, byDictionaryLiteral] + static let allProtocols = [ + byArrayLiteral, byNilLiteral, byBooleanLiteral, + byFloatLiteral, byIntegerLiteral, byUnicodeScalarLiteral, + byExtendedGraphemeClusterLiteral, byStringLiteral, + byStringInterpolation, byDictionaryLiteral, + ] static let possibleNumberOfArguments: Set = { allProtocols.reduce(into: Set()) { partialResult, entry in @@ -103,7 +105,7 @@ private struct ExpressibleByCompiler { }() func match(arguments: [String]) -> Bool { - return self.arguments.contains(arguments) + self.arguments.contains(arguments) } private static let byArrayLiteral: ExpressibleByCompiler = { @@ -121,7 +123,7 @@ private struct ExpressibleByCompiler { "NSSet", "SBElementArray", "Set", - "IndexSet" + "IndexSet", ] return Self(protocolName: "ExpressibleByArrayLiteral", types: types, arguments: [["arrayLiteral"]]) }() diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift index 33be403ff1..45a18ed6a7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift @@ -1,7 +1,7 @@ import SwiftSyntax @SwiftSyntaxRule -struct DeploymentTargetRule { +struct DeploymentTargetRule: Rule { fileprivate typealias Version = DeploymentTargetConfiguration.Version var configuration = DeploymentTargetConfiguration() @@ -46,7 +46,7 @@ private extension DeploymentTargetRule { "tvOS": configuration.tvOSDeploymentTarget, "tvOSApplicationExtension": configuration.tvOSAppExtensionDeploymentTarget, "watchOS": configuration.watchOSDeploymentTarget, - "watchOSApplicationExtension": configuration.watchOSAppExtensionDeploymentTarget + "watchOSApplicationExtension": configuration.watchOSAppExtensionDeploymentTarget, ] } @@ -108,7 +108,7 @@ private extension DeploymentTargetRule { return nil } - guard let version = try? Version(platform: platform, rawValue: versionString), + guard let version = try? Version(platform: platform, value: versionString), version <= minVersion else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRuleExamples.swift index a22c0c8c3d..3d85368f6f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRuleExamples.swift @@ -10,7 +10,7 @@ internal enum DeploymentTargetRuleExamples { Example("if #available(iOS 10, *) {}"), Example("guard #available(iOS 12.0, *) else { return }"), Example("#if #unavailable(iOS 15.0) {}"), - Example("#guard #unavailable(iOS 15.0) {} else { return }") + Example("#guard #unavailable(iOS 15.0) {} else { return }"), ] static let triggeringExamples: [Example] = [ @@ -32,6 +32,6 @@ internal enum DeploymentTargetRuleExamples { Example("guard ↓#available(iOS 6.0, *) else { return }"), Example("if ↓#unavailable(iOS 7.0) {}"), Example("if ↓#unavailable(iOS 6.9) {}"), - Example("guard ↓#unavailable(iOS 7.0) {} else { return }") + Example("guard ↓#unavailable(iOS 7.0) {} else { return }"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DiscardedNotificationCenterObserverRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DiscardedNotificationCenterObserverRule.swift index 3e3cd6dcf4..5b42b2a113 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DiscardedNotificationCenterObserverRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DiscardedNotificationCenterObserverRule.swift @@ -38,8 +38,8 @@ struct DiscardedNotificationCenterObserverRule: OptInRule { obs.append(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })) """), Example(""" - func foo(_ notif: Any) { - obs.append(notif) + func foo(_ notify: Any) { + obs.append(notify) } foo(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })) """), @@ -54,7 +54,7 @@ struct DiscardedNotificationCenterObserverRule: OptInRule { """), Example(""" f { return nc.addObserver(forName: $0, object: object, queue: queue, using: block) } - """) + """), ], triggeringExamples: [ Example("↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }"), @@ -88,7 +88,7 @@ struct DiscardedNotificationCenterObserverRule: OptInRule { ↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }) } } - """) + """), ] ) } @@ -105,12 +105,12 @@ private extension DiscardedNotificationCenterObserverRule { final class Visitor: ViolationsSyntaxVisitor { var scopes = Stack() - override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: SourceFileSyntax) -> SyntaxVisitorContinueKind { scopes.push(.file) return .visitChildren } - override func visitPost(_ node: SourceFileSyntax) { + override func visitPost(_: SourceFileSyntax) { scopes.pop() } @@ -119,7 +119,7 @@ private extension DiscardedNotificationCenterObserverRule { return .visitChildren } - override func visitPost(_ node: FunctionDeclSyntax) { + override func visitPost(_: FunctionDeclSyntax) { scopes.pop() } @@ -130,7 +130,7 @@ private extension DiscardedNotificationCenterObserverRule { return .visitChildren } - override func visitPost(_ node: AccessorBlockSyntax) { + override func visitPost(_: AccessorBlockSyntax) { scopes.pop() } @@ -143,7 +143,7 @@ private extension DiscardedNotificationCenterObserverRule { return .visitChildren } - override func visitPost(_ node: AccessorDeclSyntax) { + override func visitPost(_: AccessorDeclSyntax) { scopes.pop() } @@ -152,7 +152,7 @@ private extension DiscardedNotificationCenterObserverRule { return .visitChildren } - override func visitPost(_ node: ClosureExprSyntax) { + override func visitPost(_: ClosureExprSyntax) { scopes.pop() } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DiscouragedDirectInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DiscouragedDirectInitRule.swift index 2bbe11bc18..4ae0db9ac8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DiscouragedDirectInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DiscouragedDirectInitRule.swift @@ -18,7 +18,7 @@ struct DiscouragedDirectInitRule: Rule { Example("let foo = Bundle.init(identifier: \"bar\")"), Example("let foo = NSError(domain: \"bar\", code: 0)"), Example("let foo = NSError.init(domain: \"bar\", code: 0)"), - Example("func testNSError()") + Example("func testNSError()"), ], triggeringExamples: [ Example("↓UIDevice()"), @@ -32,7 +32,7 @@ struct DiscouragedDirectInitRule: Rule { Example("↓NSError.init()"), Example("let foo = ↓UIDevice.init()"), Example("let foo = ↓Bundle.init()"), - Example("let foo = bar(bundle: ↓Bundle.init(), device: ↓UIDevice.init(), error: ↓NSError.init())") + Example("let foo = bar(bundle: ↓Bundle.init(), device: ↓UIDevice.init(), error: ↓NSError.init())"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateConditionsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateConditionsRule.swift index e24e7009a6..04c5f512ce 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateConditionsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateConditionsRule.swift @@ -12,147 +12,147 @@ struct DuplicateConditionsRule: Rule { nonTriggeringExamples: [ Example(""" if x < 5 { - foo() + foo() } else if y == "s" { - bar() + bar() } - """), + """), Example(""" if x < 5 { - foo() + foo() } if x < 5 { - bar() + bar() } - """), + """), Example(""" if x < 5, y == "s" { - foo() + foo() } else if x < 5 { - bar() + bar() } - """), + """), Example(""" switch x { case \"a\": - foo() - bar() + foo() + bar() } - """), + """), Example(""" switch x { case \"a\" where y == "s": - foo() + foo() case \"a\" where y == "t": - bar() + bar() } - """), + """), Example(""" if let x = maybeAbc { - foo() + foo() } else if let x = maybePqr { - bar() + bar() } - """), + """), Example(""" if let x = maybeAbc, let z = x.maybeY { - foo() + foo() } else if let x = maybePqr, let z = x.maybeY { - bar() + bar() } - """), + """), Example(""" if case .p = x { - foo() + foo() } else if case .q = x { - bar() + bar() } - """), + """), Example(""" if true { - if true { foo() } + if true { foo() } } - """) + """), ], triggeringExamples: [ Example(""" if ↓x < 5 { - foo() + foo() } else if y == "s" { - bar() + bar() } else if ↓x < 5 { - baz() + baz() } - """), + """), Example(""" if z { - if ↓x < 5 { - foo() - } else if y == "s" { - bar() - } else if ↓x < 5 { - baz() - } + if ↓x < 5 { + foo() + } else if y == "s" { + bar() + } else if ↓x < 5 { + baz() + } } - """), + """), Example(""" if ↓x < 5, y == "s" { - foo() + foo() } else if x < 10 { - bar() + bar() } else if ↓y == "s", x < 5 { - baz() + baz() } - """), + """), Example(""" switch x { case ↓\"a\", \"b\": - foo() + foo() case \"c\", ↓\"a\": - bar() + bar() } - """), + """), Example(""" switch x { case ↓\"a\" where y == "s": - foo() + foo() case ↓\"a\" where y == "s": - bar() + bar() } - """), + """), Example(""" if ↓let xyz = maybeXyz { - foo() + foo() } else if ↓let xyz = maybeXyz { - bar() + bar() } - """), + """), Example(""" if ↓let x = maybeAbc, let z = x.maybeY { - foo() + foo() } else if ↓let x = maybeAbc, let z = x.maybeY { - bar() + bar() } - """), + """), Example(""" if ↓#available(macOS 10.15, *) { - foo() + foo() } else if ↓#available(macOS 10.15, *) { - bar() + bar() } - """), + """), Example(""" if ↓case .p = x { - foo() + foo() } else if ↓case .p = x { - bar() + bar() } - """), + """), Example(""" if ↓x < 5 {} else if ↓x < 5 {} else if ↓x < 5 {} - """) + """), ] ) } @@ -174,7 +174,7 @@ private extension DuplicateConditionsRule { let positionsByConditions = statementChain .reduce(into: [Set: [AbsolutePosition]]()) { acc, elt in - let conditions = elt.conditions.map { $0.condition.trimmedDescription } + let conditions = elt.conditions.map(\.condition.trimmedDescription) let location = elt.conditions.positionAfterSkippingLeadingTrivia acc[Set(conditions), default: []].append(location) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateEnumCasesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateEnumCasesRule.swift index 04c942c50f..616db6f70c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateEnumCasesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicateEnumCasesRule.swift @@ -43,7 +43,7 @@ struct DuplicateEnumCasesRule: Rule { case file(URL) #endif } - """) + """), ], triggeringExamples: [ Example(""" @@ -52,7 +52,7 @@ struct DuplicateEnumCasesRule: Rule { case addURL(url: URL) case ↓add(data: Data) } - """) + """), ] ) } @@ -76,7 +76,7 @@ private extension DuplicateEnumCasesRule { let duplicatedElementPositions = elementsByName .filter { $0.value.count > 1 } - .flatMap { $0.value } + .flatMap(\.value) violations.append(contentsOf: duplicatedElementPositions) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicatedKeyInDictionaryLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicatedKeyInDictionaryLiteralRule.swift index 5c2c1ac2be..d44220f214 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicatedKeyInDictionaryLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DuplicatedKeyInDictionaryLiteralRule.swift @@ -15,31 +15,31 @@ struct DuplicatedKeyInDictionaryLiteralRule: Rule { 1: "1", 2: "2" ] - """), + """), Example(""" [ "1": 1, "2": 2 ] - """), + """), Example(""" [ foo: "1", bar: "2" ] - """), + """), Example(""" [ UUID(): "1", UUID(): "2" ] - """), + """), Example(""" [ #line: "1", #line: "2" ] - """) + """), ], triggeringExamples: [ Example(""" @@ -48,14 +48,14 @@ struct DuplicatedKeyInDictionaryLiteralRule: Rule { 2: "2", ↓1: "one" ] - """), + """), Example(""" [ "1": 1, "2": 2, ↓"2": 2 ] - """), + """), Example(""" [ foo: "1", @@ -64,7 +64,7 @@ struct DuplicatedKeyInDictionaryLiteralRule: Rule { ↓foo: "4", zaz: "5" ] - """), + """), Example(""" [ .one: "1", @@ -74,7 +74,7 @@ struct DuplicatedKeyInDictionaryLiteralRule: Rule { .four: "4", .five: "5" ] - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DynamicInlineRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DynamicInlineRule.swift index e12f6b5027..232cb208cb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DynamicInlineRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DynamicInlineRule.swift @@ -12,14 +12,14 @@ struct DynamicInlineRule: Rule { nonTriggeringExamples: [ Example("class C {\ndynamic func f() {}}"), Example("class C {\n@inline(__always) func f() {}}"), - Example("class C {\n@inline(never) dynamic func f() {}}") + Example("class C {\n@inline(never) dynamic func f() {}}"), ], triggeringExamples: [ Example("class C {\n@inline(__always) dynamic ↓func f() {}\n}"), Example("class C {\n@inline(__always) public dynamic ↓func f() {}\n}"), Example("class C {\n@inline(__always) dynamic internal ↓func f() {}\n}"), Example("class C {\n@inline(__always)\ndynamic ↓func f() {}\n}"), - Example("class C {\n@inline(__always)\ndynamic\n↓func f() {}\n}") + Example("class C {\n@inline(__always)\ndynamic\n↓func f() {}\n}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRuleExamples.swift index beafdfb36c..ee5fcdaaec 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRuleExamples.swift @@ -98,7 +98,7 @@ internal struct EmptyXCTestMethodRuleExamples { enum E { override func foo(a: Int) {} } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] static let triggeringExamples = [ @@ -187,6 +187,6 @@ internal struct EmptyXCTestMethodRuleExamples { class BarTests: XCTestCase { ↓func testFoo() {} } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift index 851128a234..c64c93c681 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift @@ -33,14 +33,14 @@ struct ExpiringTodoRule: OptInRule { Example("/* FIXME: */"), Example("/* TODO: */"), Example("/** FIXME: */"), - Example("/** TODO: */") + Example("/** TODO: */"), ], triggeringExamples: [ Example("// TODO: [↓10/14/2019]"), Example("// FIXME: [↓10/14/2019]"), Example("// FIXME: [↓1/14/2019]"), Example("// FIXME: [↓10/14/2019]"), - Example("// TODO: [↓9999/14/10]") + Example("// TODO: [↓9999/14/10]"), ].skipWrappingInCommentTests() ) @@ -56,11 +56,11 @@ struct ExpiringTodoRule: OptInRule { return file.matchesAndSyntaxKinds(matching: regex).compactMap { checkingResult, syntaxKinds in guard - syntaxKinds.allSatisfy({ $0.isCommentLike }), + syntaxKinds.allSatisfy(\.isCommentLike), checkingResult.numberOfRanges > 1, case let range = checkingResult.range(at: 1), - let violationLevel = self.violationLevel(for: expiryDate(file: file, range: range)), - let severity = self.severity(for: violationLevel) else { + let violationLevel = violationLevel(for: expiryDate(file: file, range: range)), + let severity = severity(for: violationLevel) else { return nil } @@ -117,13 +117,13 @@ struct ExpiringTodoRule: OptInRule { private extension Date { var isAfterToday: Bool { - return Calendar.current.compare(.init(), to: self, toGranularity: .day) == .orderedAscending + Calendar.current.compare(.init(), to: self, toGranularity: .day) == .orderedAscending } } private extension SyntaxKind { /// Returns if the syntax kind is comment-like. var isCommentLike: Bool { - return Self.commentKinds.contains(self) + Self.commentKinds.contains(self) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IBInspectableInExtensionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IBInspectableInExtensionRule.swift index 46a1d85db9..686edfdf07 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/IBInspectableInExtensionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IBInspectableInExtensionRule.swift @@ -14,14 +14,14 @@ struct IBInspectableInExtensionRule: OptInRule { class Foo { @IBInspectable private var x: Int } - """) + """), ], triggeringExamples: [ Example(""" extension Foo { ↓@IBInspectable private var x: Int } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/IdenticalOperandsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/IdenticalOperandsRule.swift index dac4e997da..4849b0570c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/IdenticalOperandsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/IdenticalOperandsRule.swift @@ -32,7 +32,7 @@ struct IdenticalOperandsRule: OptInRule { """), Example("num \(operation) num!.byteSwapped"), Example("1 + 1 \(operation) 1 + 2"), - Example("f( i : 2) \(operation) f (i: 3 )") + Example("f( i : 2) \(operation) f (i: 3 )"), ] } + [ Example("func evaluate(_ mode: CommandMode) -> Result>>"), @@ -42,7 +42,7 @@ struct IdenticalOperandsRule: OptInRule { Example("type(of: model).cachePrefix == cachePrefix"), Example("histogram[156].0 == 0x003B8D96 && histogram[156].1 == 1"), Example(#"[Wrapper(type: .three), Wrapper(type: .one)].sorted { "\($0.type)" > "\($1.type)"}"#), - Example(#"array.sorted { "\($0)" < "\($1)" }"#) + Example(#"array.sorted { "\($0)" < "\($1)" }"#), ], triggeringExamples: operators.flatMap { operation in [ @@ -56,7 +56,7 @@ struct IdenticalOperandsRule: OptInRule { Example("XCTAssertTrue(↓s3 \(operation) s3)"), Example("if let tab = tabManager.selectedTab, ↓tab.webView \(operation) tab.webView"), Example("↓1 + 1 \(operation) 1 + 1"), - Example(" ↓f( i : 2) \(operation) f (i: \n 2 )") + Example(" ↓f( i : 2) \(operation) f (i: \n 2 )"), ] } + [ Example(""" @@ -66,7 +66,7 @@ struct IdenticalOperandsRule: OptInRule { Example(""" return lhs.foo == rhs.foo && ↓lhs.bar == lhs.bar - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/InertDeferRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/InertDeferRule.swift index d49b72fd75..87a8775ebd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/InertDeferRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/InertDeferRule.swift @@ -2,7 +2,7 @@ import SwiftSyntax // TODO: [12/23/2024] Remove deprecation warning after ~2 years. private let warnDeprecatedOnceImpl: Void = { - Issue.ruleDeprecated(ruleID: InertDeferRule.description.identifier).print() + Issue.ruleDeprecated(ruleID: InertDeferRule.identifier).print() }() private func warnDeprecatedOnce() { @@ -42,7 +42,7 @@ struct InertDeferRule: SwiftSyntaxRule, OptInRule { #endif print(1) } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example(""" @@ -79,7 +79,7 @@ struct InertDeferRule: SwiftSyntaxRule, OptInRule { ↓defer { print(5) } #endif } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/InvalidSwiftLintCommandRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/InvalidSwiftLintCommandRule.swift index 71fed8fc04..52d8146bb0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/InvalidSwiftLintCommandRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/InvalidSwiftLintCommandRule.swift @@ -12,7 +12,7 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule { Example("// swiftlint:disable:next unused_import"), Example("// swiftlint:disable:previous unused_import"), Example("// swiftlint:disable:this unused_import"), - Example("//swiftlint:disable:this unused_import") + Example("//swiftlint:disable:this unused_import"), ], triggeringExamples: [ Example("// ↓swiftlint:"), @@ -29,7 +29,7 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule { Example("// ↓swiftlint:enable:"), Example("// ↓swiftlint:enable: "), Example("// ↓swiftlint:disable: unused_import"), - Example("// s↓swiftlint:disable unused_import") + Example("// s↓swiftlint:disable unused_import"), ].skipWrappingInCommentTests() ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/LocalDocCommentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/LocalDocCommentRule.swift index 927a32c869..bf9dc3b8ca 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/LocalDocCommentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/LocalDocCommentRule.swift @@ -28,7 +28,7 @@ struct LocalDocCommentRule: SwiftSyntaxRule, OptInRule { /// Look here for more info: /// https://github.com. var myGreatProperty: String! - """) + """), ], triggeringExamples: [ Example(""" @@ -36,7 +36,7 @@ struct LocalDocCommentRule: SwiftSyntaxRule, OptInRule { ↓/// Docstring inside a function declaration print("foo") } - """) + """), ] ) @@ -53,7 +53,8 @@ private extension LocalDocCommentRule { final class Visitor: ViolationsSyntaxVisitor { private let docCommentRanges: [ByteSourceRange] - init(configuration: ConfigurationType, file: SwiftLintFile, + init(configuration: ConfigurationType, + file: SwiftLintFile, classifications: [SyntaxClassifiedRange]) { self.docCommentRanges = classifications .filter { $0.kind == .docLineComment || $0.kind == .docBlockComment } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/LowerACLThanParentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/LowerACLThanParentRule.swift index 7e38328ba1..624ebf0369 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/LowerACLThanParentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/LowerACLThanParentRule.swift @@ -25,7 +25,7 @@ struct LowerACLThanParentRule: OptInRule { Example("public extension Foo { struct Bar { public func baz() {} }}"), Example("public extension Foo { struct Bar { internal func baz() {} }}"), Example("internal extension Foo { struct Bar { internal func baz() {} }}"), - Example("extension Foo { struct Bar { internal func baz() {} }}") + Example("extension Foo { struct Bar { internal func baz() {} }}"), ], triggeringExamples: [ Example("struct Foo { ↓public func bar() {} }"), @@ -47,7 +47,7 @@ struct LowerACLThanParentRule: OptInRule { Example("private extension Foo { struct Bar { ↓internal func baz() {} }}"), Example("fileprivate extension Foo { struct Bar { ↓internal func baz() {} }}"), Example("public extension Foo { struct Bar { struct Baz { ↓public func qux() {} }}}"), - Example("final class Foo { ↓public func bar() {} }") + Example("final class Foo { ↓public func bar() {} }"), ], corrections: [ Example("struct Foo { ↓public func bar() {} }"): @@ -77,7 +77,7 @@ struct LowerACLThanParentRule: OptInRule { struct Foo { func bar() {} } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift index 95379a476a..e7b92a8d17 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift @@ -47,9 +47,8 @@ private struct ViolationResult { private extension TokenSyntax { private enum Mark { static func lint(in text: String) -> [() -> String] { - let range = NSRange(text.startIndex.. Bool { - regex(goodPattern).firstMatch(in: text, range: range) != nil + range.lowerBound != 0 || regex(goodPattern).firstMatch(in: text, range: text.fullNSRange) != nil } private static let goodPattern = [ @@ -71,13 +70,13 @@ private extension TokenSyntax { "^// MARK:$", // comment start with `Mark ...` is ignored - "^\(twoOrThreeSlashes) +[Mm]ark[^:]" + "^\(twoOrThreeSlashes) +[Mm]ark[^:]", ].map(nonCapturingGroup).joined(separator: "|") private static let badPattern = capturingGroup([ "MARK[^\\s:]", "[Mm]ark", - "MARK" + "MARK", ].map(basePattern).joined(separator: "|")) + capturingGroup(hyphenOrEmpty) private static let anySpace = " *" diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRuleExamples.swift index 5686bb1a51..3aa174113e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRuleExamples.swift @@ -20,7 +20,15 @@ internal struct MarkRuleExamples { // MARK: good } """), - issue1749Example + Example(""" + /// Comment + /// `xxx://marketingOptIn` + struct S {} + + /// //marketingOptIn + struct T {} + """, excludeFromDocumentation: true), + issue1749Example, ] static let triggeringExamples = [ @@ -54,7 +62,7 @@ internal struct MarkRuleExamples { ↓//MARK: bad } """), - issue1029Example + issue1029Example, ] static let corrections = [ @@ -77,7 +85,7 @@ internal struct MarkRuleExamples { Example("↓/// MARK:"): Example("// MARK:"), Example("↓/// MARK comment"): Example("// MARK: comment"), issue1029Example: issue1029Correction, - issue1749Example: issue1749Correction + issue1749Example: issue1749Correction, ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift index 9b388c3b8f..ed0cd61822 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift @@ -1,139 +1,323 @@ -import SourceKittenFramework - -private extension SwiftLintFile { - func missingDocOffsets(in dictionary: SourceKittenDictionary, - acls: [AccessControlLevel], - excludesExtensions: Bool, - excludesInheritedTypes: Bool, - excludesTrivialInit: Bool) -> [(ByteCount, AccessControlLevel)] { - if dictionary.enclosedSwiftAttributes.contains(.override) || - (dictionary.inheritedTypes.isNotEmpty && excludesInheritedTypes) { - return [] - } - let substructureOffsets = dictionary.substructure.flatMap { - missingDocOffsets( - in: $0, - acls: acls, - excludesExtensions: excludesExtensions, - excludesInheritedTypes: excludesInheritedTypes, - excludesTrivialInit: excludesTrivialInit - ) +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule +struct MissingDocsRule: OptInRule { + var configuration = MissingDocsConfiguration() + + static let description = RuleDescription( + identifier: "missing_docs", + name: "Missing Docs", + description: "Declarations should be documented.", + kind: .lint, + nonTriggeringExamples: MissingDocsRuleExamples.nonTriggeringExamples, + triggeringExamples: MissingDocsRuleExamples.triggeringExamples + ) +} + +private extension MissingDocsRule { + final class Visitor: ViolationsSyntaxVisitor { + private var aclScope = Stack() + + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + defer { + aclScope.push( + behavior: .actor(node.modifiers.accessibility), + evalEffectiveAcl: configuration.evaluateEffectiveAccessControlLevel + ) + } + if node.inherits, configuration.excludesInheritedTypes { + return .skipChildren + } + collectViolation(from: node, on: node.actorKeyword) + return .visitChildren + } + + override func visitPost(_: ActorDeclSyntax) { + aclScope.pop() + } + + override func visitPost(_ node: AssociatedTypeDeclSyntax) { + collectViolation(from: node, on: node.associatedtypeKeyword) + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + defer { + aclScope.push( + behavior: .class(node.modifiers.accessibility), + evalEffectiveAcl: configuration.evaluateEffectiveAccessControlLevel + ) + } + if node.inherits, configuration.excludesInheritedTypes { + return .skipChildren + } + collectViolation(from: node, on: node.classKeyword) + return .visitChildren + } + + override func visitPost(_: ClassDeclSyntax) { + aclScope.pop() } - let isTrivialInit = dictionary.declarationKind == .functionMethodInstance && - dictionary.name == "init()" && - dictionary.enclosedVarParameters.isEmpty - if isTrivialInit && excludesTrivialInit { - return substructureOffsets + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + .skipChildren } - guard let kind = dictionary.declarationKind, - !SwiftDeclarationKind.extensionKinds.contains(kind) || !excludesExtensions, - case let isDeinit = kind == .functionMethodInstance && dictionary.name == "deinit", - !isDeinit, - let offset = dictionary.offset, - let acl = dictionary.accessibility, - acls.contains(acl) else { - return substructureOffsets + override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + .skipChildren } - if dictionary.docLength != nil { - return substructureOffsets + + override func visitPost(_ node: EnumCaseDeclSyntax) { + guard !node.hasDocComment, case let .enum(enumAcl) = aclScope.peek() else { + return + } + let acl = enumAcl ?? .internal + if let parameter = configuration.parameters.first(where: { $0.value == acl }) { + violations.append( + ReasonedRuleViolation( + position: node.caseKeyword.positionAfterSkippingLeadingTrivia, + reason: "\(acl) declarations should be documented", + severity: parameter.severity + ) + ) + } + } + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + defer { + aclScope.push( + behavior: .enum(node.modifiers.accessibility), + evalEffectiveAcl: configuration.evaluateEffectiveAccessControlLevel + ) + } + if node.inherits, configuration.excludesInheritedTypes { + return .skipChildren + } + collectViolation(from: node, on: node.enumKeyword) + return .visitChildren + } + + override func visitPost(_: EnumDeclSyntax) { + aclScope.pop() + } + + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + defer { aclScope.push(.extension(node.modifiers.accessibility)) } + if node.inherits, configuration.excludesInheritedTypes { + return .skipChildren + } + if !configuration.excludesExtensions { + collectViolation(from: node, on: node.extensionKeyword) + } + return .visitChildren + } + + override func visitPost(_: ExtensionDeclSyntax) { + aclScope.pop() + } + + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { + collectViolation(from: node, on: node.funcKeyword) + return .skipChildren + } + + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { + if node.signature.parameterClause.parameters.isNotEmpty || !configuration.excludesTrivialInit { + collectViolation(from: node, on: node.initKeyword) + } + return .skipChildren + } + + override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + defer { + aclScope.push( + behavior: .protocol(node.modifiers.accessibility), + evalEffectiveAcl: configuration.evaluateEffectiveAccessControlLevel + ) + } + if node.inherits, configuration.excludesInheritedTypes { + return .skipChildren + } + collectViolation(from: node, on: node.protocolKeyword) + return .visitChildren + } + + override func visitPost(_: ProtocolDeclSyntax) { + aclScope.pop() + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + defer { + aclScope.push( + behavior: .struct(node.modifiers.accessibility), + evalEffectiveAcl: configuration.evaluateEffectiveAccessControlLevel + ) + } + if node.inherits, configuration.excludesInheritedTypes { + return .skipChildren + } + collectViolation(from: node, on: node.structKeyword) + return .visitChildren + } + + override func visitPost(_: StructDeclSyntax) { + aclScope.pop() + } + + override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind { + collectViolation(from: node, on: node.subscriptKeyword) + return .skipChildren + } + + override func visitPost(_ node: TypeAliasDeclSyntax) { + collectViolation(from: node, on: node.typealiasKeyword) + } + + override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + collectViolation(from: node, on: node.bindingSpecifier) + return .skipChildren + } + + private func collectViolation(from node: some WithModifiersSyntax, on token: TokenSyntax) { + if node.modifiers.contains(keyword: .override) || node.hasDocComment { + return + } + let acl = aclScope.computeAcl( + givenExplicitAcl: node.modifiers.accessibility, + evalEffectiveAcl: configuration.evaluateEffectiveAccessControlLevel + ) + if let parameter = configuration.parameters.first(where: { $0.value == acl }) { + violations.append( + ReasonedRuleViolation( + position: token.positionAfterSkippingLeadingTrivia, + reason: "\(acl) declarations should be documented", + severity: parameter.severity + ) + ) + } } - return substructureOffsets + [(offset, acl)] } } -struct MissingDocsRule: OptInRule { - init() { - configuration = MissingDocsConfiguration() +private extension DeclGroupSyntax { + var inherits: Bool { + if let types = inheritanceClause?.inheritedTypes, types.isNotEmpty { + return types.contains { !$0.type.is(SuppressedTypeSyntax.self) } + } + return false + } +} + +private extension SyntaxProtocol { + var hasDocComment: Bool { + switch leadingTrivia.pieces.last(where: { !$0.isWhitespace }) { + case .docBlockComment, .docLineComment: + return true + default: + guard let item = parent?.as(CodeBlockItemSyntax.self), + let itemList = item.parent?.as(CodeBlockItemListSyntax.self), + itemList.first == item else { + return false + } + let ifConfigDecl = itemList + .parent?.as(IfConfigClauseSyntax.self)? + .parent?.as(IfConfigClauseListSyntax.self)? + .parent?.as(IfConfigDeclSyntax.self) + if let ifConfigDecl { + return ifConfigDecl.hasDocComment + } + return false + } } +} - typealias ConfigurationType = MissingDocsConfiguration +private extension DeclModifierListSyntax { + var accessibility: AccessControlLevel? { + filter { $0.detail == nil }.compactMap { AccessControlLevel(description: $0.name.text) }.first + } +} - var configuration: MissingDocsConfiguration +private enum AccessControlBehavior { + case `actor`(AccessControlLevel?) + case local + case `class`(AccessControlLevel?) + case `enum`(AccessControlLevel?) + case `extension`(AccessControlLevel?) + case `protocol`(AccessControlLevel?) + case `struct`(AccessControlLevel?) - static let description = RuleDescription( - identifier: "missing_docs", - name: "Missing Docs", - description: "Declarations should be documented.", - kind: .lint, - nonTriggeringExamples: [ - // locally-defined superclass member is documented, but subclass member is not - Example(""" - /// docs - public class A { - /// docs - public func b() {} - } - // no docs - public class B: A { override public func b() {} } - """), - // externally-defined superclass member is documented, but subclass member is not - Example(""" - import Foundation - // no docs - public class B: NSObject { - // no docs - override public var description: String { fatalError() } } - """), - Example(""" - /// docs - public class A { - deinit {} - } - """), - Example(""" - public extension A {} - """), - Example(""" - /// docs - public class A { - public init() {} - } - """, configuration: ["excludes_trivial_init": true]) - ], - triggeringExamples: [ - // public, undocumented - Example("public func a() {}"), - // public, undocumented - Example("// regular comment\npublic func a() {}"), - // public, undocumented - Example("/* regular comment */\npublic func a() {}"), - // protocol member and inherited member are both undocumented - Example(""" - /// docs - public protocol A { - // no docs - var b: Int { get } } - /// docs - public struct C: A { - - public let b: Int - } - """), - Example(""" - /// docs - public class A { - public init(argument: String) {} - } - """, configuration: ["excludes_trivial_init": true]) - ] - ) + var effectiveAcl: AccessControlLevel { + explicitAcl ?? .internal + } + + var explicitAcl: AccessControlLevel? { + switch self { + case let .actor(acl): acl + case .local: nil + case let .class(acl): acl + case let .enum(acl): acl + case let .extension(acl): acl + case let .protocol(acl): acl + case let .struct(acl): acl + } + } + + func sameWith(acl: AccessControlLevel) -> Self { + switch self { + case .actor: .actor(acl) + case .local: .local + case .class: .class(acl) + case .enum: .enum(acl) + case .extension: .extension(acl) + case .protocol: .protocol(acl) + case .struct: .struct(acl) + } + } +} + +/// Implementation of Swift's effective ACL logic. Should be moved to a specialized syntax visitor for reuse some time. +private extension Stack { + mutating func push(behavior: AccessControlBehavior, evalEffectiveAcl: Bool) { + if let parentBehavior = peek() { + switch parentBehavior { + case .local: + push(.local) + case .actor, .class, .struct, .enum: + if behavior.effectiveAcl <= parentBehavior.effectiveAcl || !evalEffectiveAcl { + push(behavior) + } else { + push(behavior.sameWith(acl: parentBehavior.effectiveAcl)) + } + case .extension, .protocol: + if behavior.explicitAcl != nil { + push(behavior) + } else { + push(behavior.sameWith(acl: parentBehavior.effectiveAcl)) + } + } + } else { + push(behavior) + } + } - func validate(file: SwiftLintFile) -> [StyleViolation] { - let acls = configuration.parameters.map { $0.value } - let dict = file.structureDictionary - return file.missingDocOffsets( - in: dict, - acls: acls, - excludesExtensions: configuration.excludesExtensions, - excludesInheritedTypes: configuration.excludesInheritedTypes, - excludesTrivialInit: configuration.excludesTrivialInit - ).map { offset, acl in - StyleViolation(ruleDescription: Self.description, - severity: configuration.parameters.first { $0.value == acl }?.severity ?? .warning, - location: Location(file: file, byteOffset: offset), - reason: "\(acl.description) declarations should be documented") + func computeAcl(givenExplicitAcl acl: AccessControlLevel?, evalEffectiveAcl: Bool) -> AccessControlLevel { + if let parentBehavior = peek() { + switch parentBehavior { + case .local: + .private + case .actor, .class, .struct, .enum: + if let acl { + acl < parentBehavior.effectiveAcl || !evalEffectiveAcl ? acl : parentBehavior.effectiveAcl + } else { + parentBehavior.effectiveAcl >= .internal ? .internal : parentBehavior.effectiveAcl + } + case .protocol: + parentBehavior.effectiveAcl + case .extension: + acl ?? parentBehavior.effectiveAcl + } + } else { + acl ?? .internal } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRuleExamples.swift new file mode 100644 index 0000000000..0fd4ada975 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRuleExamples.swift @@ -0,0 +1,223 @@ +struct MissingDocsRuleExamples { + static let nonTriggeringExamples = [ + // locally-defined superclass member is documented, but subclass member is not + Example(""" + /// docs + public class A { + /// docs + public func b() {} + } + // no docs + public class B: A { override public func b() {} } + """), + // externally-defined superclass member is documented, but subclass member is not + Example(""" + import Foundation + // no docs + public class B: NSObject { + // no docs + override public var description: String { fatalError() } } + """), + Example(""" + /// docs + public class A { + var j = 1 + var i: Int { 1 } + func f() {} + deinit {} + } + """), + Example(""" + public extension A {} + """), + Example(""" + enum E { + case A + } + """), + Example(""" + /// docs + public class A { + public init() {} + } + """, configuration: ["excludes_trivial_init": true]), + Example(""" + class C { + public func f() {} + } + """, configuration: ["evaluate_effective_access_control_level": true]), + Example(""" + public struct S: ~Copyable, P { + public init() {} + } + """), + Example(""" + /// my doc + #if os(macOS) + public func f() {} + #else + public func f() async {} + #endif + """, excludeFromDocumentation: true), + Example(""" + /// my doc + #if os(macOS) + #if is(iOS) + public func f() {} + #endif + #else + public func f() async {} + #endif + """, excludeFromDocumentation: true), + ] + + static let triggeringExamples = [ + // public, undocumented + Example("public ↓func a() {}"), + // public, undocumented + Example("// regular comment\npublic ↓func a() {}"), + // public, undocumented + Example("/* regular comment */\npublic ↓func a() {}"), + // protocol member and inherited member are both undocumented + Example(""" + /// docs + public protocol A { + // no docs + ↓var b: Int { get } + } + /// docs + public struct C: A { + public let b: Int + } + """), + // Violation marker is on `static` keyword. + Example(""" + /// a doc + public class C { + public static ↓let i = 1 + } + """), + // `excludes_extensions` only excludes the extension declaration itself; not its children. + Example(""" + public extension A { + public ↓func f() {} + static ↓var i: Int { 1 } + ↓struct S { + func f() {} + } + ↓class C { + func f() {} + } + ↓actor A { + func f() {} + } + ↓enum E { + ↓case a + func f() {} + } + } + """), + Example(""" + public extension A { + ↓enum E { + enum Inner { + case a + } + } + } + """), + Example(""" + extension E { + public ↓struct S { + public static ↓let i = 1 + } + } + """), + Example(""" + extension E { + public ↓func f() {} + } + """), + Example(""" + /// docs + public class A { + public ↓init(argument: String) {} + } + """, configuration: ["excludes_trivial_init": true]), + Example(""" + public ↓struct C: A { + public ↓let b: Int + } + """, configuration: ["excludes_inherited_types": false]), + Example(""" + public ↓extension A { + public ↓func f() {} + } + """, configuration: ["excludes_extensions": false]), + Example(""" + public extension E { + ↓var i: Int { + let j = 1 + func f() {} + return j + } + } + """), + Example(""" + #if os(macOS) + public ↓func f() {} + #endif + """), + Example(""" + public ↓enum E { + ↓case A, B + func f() {} + init(_ i: Int) { self = .A } + } + """), + Example(""" + open ↓class C { + public ↓enum E { + ↓case A + func f() {} + init(_ i: Int) { self = .A } + } + } + """, excludeFromDocumentation: true), + /// Nested types inherit the ACL from the declaring extension. + Example(""" + /// a doc + public struct S {} + public extension S { + ↓enum E { + ↓case A + } + } + """), + Example(""" + public extension URL { + fileprivate enum E { + static let source = "" + } + static ↓var a: Int { 1 } + } + """, excludeFromDocumentation: true), + Example(""" + class C { + public ↓func f() {} + } + """, configuration: ["evaluate_effective_access_control_level": false]), + Example(""" + public ↓struct S: ~Copyable, ~Escapable { + public ↓init() {} + } + """), + Example(""" + /// my doc + #if os(macOS) + public func f() {} + public ↓func g() {} + #endif + """, excludeFromDocumentation: true), + ] +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringKeyRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringKeyRule.swift index 0af4f6fb10..77fb9b7ed1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringKeyRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringKeyRule.swift @@ -22,13 +22,13 @@ struct NSLocalizedStringKeyRule: OptInRule { let format = NSLocalizedString("%@, %@.", comment: "Accessibility label for a post in the post list." + " The parameters are the title, and date respectively." + " For example, \"Let it Go, 1 hour ago.\"") - """) + """), ], triggeringExamples: [ Example("NSLocalizedString(↓method(), comment: \"\")"), Example("NSLocalizedString(↓\"key_\\(param)\", comment: \"\")"), Example("NSLocalizedString(\"key\", comment: ↓\"comment with \\(param)\")"), - Example("NSLocalizedString(↓\"key_\\(param)\", comment: ↓method())") + Example("NSLocalizedString(↓\"key_\\(param)\", comment: ↓method())"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringRequireBundleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringRequireBundleRule.swift index bbb8d0f22a..22a515971a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringRequireBundleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NSLocalizedStringRequireBundleRule.swift @@ -25,7 +25,7 @@ struct NSLocalizedStringRequireBundleRule: OptInRule { """), Example(""" arbitraryFunctionCall("something") - """) + """), ], triggeringExamples: [ Example(""" @@ -37,7 +37,7 @@ struct NSLocalizedStringRequireBundleRule: OptInRule { Example(""" ↓NSLocalizedString("someKey", tableName: "xyz", value: "test", comment: "test") - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NSNumberInitAsFunctionReferenceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NSNumberInitAsFunctionReferenceRule.swift index 780c311350..bc600d0601 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NSNumberInitAsFunctionReferenceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NSNumberInitAsFunctionReferenceRule.swift @@ -16,11 +16,11 @@ struct NSNumberInitAsFunctionReferenceRule: Rule { Example("let value = NSNumber.init(value: 0.0)"), Example("[0, 0.2].map { NSNumber(value: $0) }"), Example("[0, 0.2].map(NSDecimalNumber.init(value:))"), - Example("[0, 0.2].map { NSDecimalNumber(value: $0) }") + Example("[0, 0.2].map { NSDecimalNumber(value: $0) }"), ], triggeringExamples: [ Example("[0, 0.2].map(↓NSNumber.init)"), - Example("[0, 0.2].map(↓NSDecimalNumber.init)") + Example("[0, 0.2].map(↓NSDecimalNumber.init)"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NSObjectPreferIsEqualRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NSObjectPreferIsEqualRuleExamples.swift index 8213de49a2..acce2b7b15 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NSObjectPreferIsEqualRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NSObjectPreferIsEqualRuleExamples.swift @@ -82,7 +82,7 @@ internal struct NSObjectPreferIsEqualRuleExamples { } } } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] static let triggeringExamples: [Example] = [ @@ -157,6 +157,6 @@ internal struct NSObjectPreferIsEqualRuleExamples { } } } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift index 24cd22d7a1..5141717370 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift @@ -5,16 +5,14 @@ struct NonOptionalStringDataConversionRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( identifier: "non_optional_string_data_conversion", - name: "Non-Optional String <-> Data Conversion", - description: "Prefer using UTF-8 encoded strings when converting between `String` and `Data`", + name: "Non-optional String -> Data Conversion", + description: "Prefer non-optional `Data(_:)` initializer when converting `String` to `Data`", kind: .lint, nonTriggeringExamples: [ - Example("Data(\"foo\".utf8)"), - Example("String(decoding: data, as: UTF8.self)") + Example("Data(\"foo\".utf8)") ], triggeringExamples: [ - Example("\"foo\".data(using: .utf8)"), - Example("String(data: data, encoding: .utf8)") + Example("\"foo\".data(using: .utf8)") ] ) } @@ -31,15 +29,6 @@ private extension NonOptionalStringDataConversionRule { violations.append(node.positionAfterSkippingLeadingTrivia) } } - - override func visitPost(_ node: DeclReferenceExprSyntax) { - if node.baseName.text == "String", - let parent = node.parent?.as(FunctionCallExprSyntax.self), - parent.arguments.map({ $0.label?.text }) == ["data", "encoding"], - parent.arguments.last?.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true { - violations.append(node.positionAfterSkippingLeadingTrivia) - } - } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRule.swift index dea3e74c33..9849ba9b5d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRule.swift @@ -28,7 +28,7 @@ private extension NotificationCenterDetachmentRule { violations.append(node.positionAfterSkippingLeadingTrivia) } - override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { .skipChildren } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRuleExamples.swift index bd1b60ab0e..7aa9c683aa 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NotificationCenterDetachmentRuleExamples.swift @@ -13,7 +13,7 @@ internal struct NotificationCenterDetachmentRuleExamples { NotificationCenter.default.removeObserver(otherObject) } } - """) + """), ] static let triggeringExamples = [ @@ -23,6 +23,6 @@ internal struct NotificationCenterDetachmentRuleExamples { ↓NotificationCenter.default.removeObserver(self) } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift new file mode 100644 index 0000000000..1aa44801ec --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift @@ -0,0 +1,38 @@ +import SwiftSyntax + +@SwiftSyntaxRule +struct OptionalDataStringConversionRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "optional_data_string_conversion", + name: "Optional Data -> String Conversion", + description: "Prefer failable `String(bytes:encoding:)` initializer when converting `Data` to `String`", + kind: .lint, + nonTriggeringExamples: [ + Example("String(data: data, encoding: .utf8)"), + Example("String(bytes: data, encoding: .utf8)"), + Example("String(UTF8.self)"), + Example("String(a, b, c, UTF8.self)"), + Example("String(decoding: data, encoding: UTF8.self)"), + ], + triggeringExamples: [ + Example("String(decoding: data, as: UTF8.self)") + ] + ) +} + +private extension OptionalDataStringConversionRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: DeclReferenceExprSyntax) { + if node.baseName.text == "String", + let parent = node.parent?.as(FunctionCallExprSyntax.self), + parent.arguments.map(\.label?.text) == ["decoding", "as"], + let expr = parent.arguments.last?.expression.as(MemberAccessExprSyntax.self), + expr.base?.description == "UTF8", + expr.declName.baseName.description == "self" { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift index acd440a5ae..428b79721e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift @@ -29,7 +29,7 @@ struct OrphanedDocCommentRule: Rule { /// Look here for more info: /// https://github.com. var myGreatProperty: String! - """) + """), ], triggeringExamples: [ Example(""" @@ -64,7 +64,7 @@ struct OrphanedDocCommentRule: Rule { // Not a doc string var myGreatProperty: String! } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OverriddenSuperCallRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OverriddenSuperCallRule.swift index dd1bd23003..84f8c0d3a2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OverriddenSuperCallRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OverriddenSuperCallRule.swift @@ -46,7 +46,7 @@ struct OverriddenSuperCallRule: OptInRule { } } } - """) + """), ], triggeringExamples: [ Example(""" @@ -71,7 +71,7 @@ struct OverriddenSuperCallRule: OptInRule { override func didReceiveMemoryWarning() {↓ } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OverrideInExtensionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OverrideInExtensionRule.swift index fe02f72cf7..f1554b2580 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OverrideInExtensionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OverrideInExtensionRule.swift @@ -25,11 +25,11 @@ struct OverrideInExtensionRule: OptInRule, SwiftSyntaxRule { extension Foo.Bar { override var description: String { return "" } } - """) + """), ], triggeringExamples: [ Example("extension Person {\n override ↓var age: Int { return 42 }\n}"), - Example("extension Person {\n override ↓func celebrateBirthday() {}\n}") + Example("extension Person {\n override ↓func celebrateBirthday() {}\n}"), ] ) @@ -50,7 +50,8 @@ private extension OverrideInExtensionRule { final class Visitor: ViolationsSyntaxVisitor { private let allowedExtensions: Set - init(configuration: ConfigurationType, file: SwiftLintFile, + init(configuration: ConfigurationType, + file: SwiftLintFile, allowedExtensions: Set) { self.allowedExtensions = allowedExtensions super.init(configuration: configuration, file: file) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PeriodSpacingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PeriodSpacingRule.swift index e53bd22288..249d67c53e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PeriodSpacingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PeriodSpacingRule.swift @@ -27,7 +27,7 @@ struct PeriodSpacingRule: SourceKitFreeRule, OptInRule, SubstitutionCorrectableR - Sentence 2 new line characters after. **/ - """) + """), ], triggeringExamples: [ Example("/* Only god knows why. ↓ This symbol does nothing. */", testWrappingInComment: false), @@ -35,7 +35,7 @@ struct PeriodSpacingRule: SourceKitFreeRule, OptInRule, SubstitutionCorrectableR Example("// Single. Double. ↓ End.", testWrappingInComment: false), Example("// Single. Double. ↓ Triple. ↓ End.", testWrappingInComment: false), Example("// Triple. ↓ Quad. ↓ End.", testWrappingInComment: false), - Example("/// - code: Identifier of the error. ↓ Integer.", testWrappingInComment: false) + Example("/// - code: Identifier of the error. ↓ Integer.", testWrappingInComment: false), ], corrections: [ Example("/* Why. ↓ Symbol does nothing. */"): Example("/* Why. Symbol does nothing. */"), @@ -43,7 +43,7 @@ struct PeriodSpacingRule: SourceKitFreeRule, OptInRule, SubstitutionCorrectableR Example("// Single. Double. ↓ End."): Example("// Single. Double. End."), Example("// Single. Double. ↓ Triple. ↓ End."): Example("// Single. Double. Triple. End."), Example("// Triple. ↓ Quad. ↓ End."): Example("// Triple. Quad. End."), - Example("/// - code: Identifier. ↓ Integer."): Example("/// - code: Identifier. Integer.") + Example("/// - code: Identifier. ↓ Integer."): Example("/// - code: Identifier. Integer."), ] ) @@ -53,16 +53,16 @@ struct PeriodSpacingRule: SourceKitFreeRule, OptInRule, SubstitutionCorrectableR .filter(\.kind.isComment) .map { $0.range.toSourceKittenByteRange() } .compactMap { (range: ByteRange) -> [NSRange]? in - return file.stringView + file.stringView .substringWithByteRange(range) .map(StringView.init) .map { commentBody in // Look for a period followed by two or more whitespaces but not new line or carriage returns - return regex(#"\.[^\S\r\n]{2,}"#) + regex(#"\.[^\S\r\n]{2,}"#) .matches(in: commentBody) .compactMap { result in // Set the location to start from the second whitespace till the last one. - return file.stringView.byteRangeToNSRange( + file.stringView.byteRangeToNSRange( ByteRange( // Safe to mix NSRange offsets with byte offsets here because the // regex can't contain multi-byte characters @@ -77,7 +77,7 @@ struct PeriodSpacingRule: SourceKitFreeRule, OptInRule, SubstitutionCorrectableR } func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(in: file).map { range in + violationRanges(in: file).map { range in StyleViolation( ruleDescription: Self.description, severity: configuration.severity, @@ -86,7 +86,7 @@ struct PeriodSpacingRule: SourceKitFreeRule, OptInRule, SubstitutionCorrectableR } } - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { - return (violationRange, "") + func substitution(for violationRange: NSRange, in _: SwiftLintFile) -> (NSRange, String)? { + (violationRange, "") } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateActionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateActionRule.swift index bc180be017..6e2a40a1f1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateActionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateActionRule.swift @@ -15,7 +15,7 @@ struct PrivateActionRule: OptInRule { Example("class Foo {\n\t@IBAction fileprivate func barButtonTapped(_ sender: UIButton) {}\n}"), Example("struct Foo {\n\t@IBAction fileprivate func barButtonTapped(_ sender: UIButton) {}\n}"), Example("private extension Foo {\n\t@IBAction func barButtonTapped(_ sender: UIButton) {}\n}"), - Example("fileprivate extension Foo {\n\t@IBAction func barButtonTapped(_ sender: UIButton) {}\n}") + Example("fileprivate extension Foo {\n\t@IBAction func barButtonTapped(_ sender: UIButton) {}\n}"), ], triggeringExamples: [ Example("class Foo {\n\t@IBAction ↓func barButtonTapped(_ sender: UIButton) {}\n}"), @@ -28,7 +28,7 @@ struct PrivateActionRule: OptInRule { Example("extension Foo {\n\t@IBAction public ↓func barButtonTapped(_ sender: UIButton) {}\n}"), Example("extension Foo {\n\t@IBAction internal ↓func barButtonTapped(_ sender: UIButton) {}\n}"), Example("public extension Foo {\n\t@IBAction ↓func barButtonTapped(_ sender: UIButton) {}\n}"), - Example("internal extension Foo {\n\t@IBAction ↓func barButtonTapped(_ sender: UIButton) {}\n}") + Example("internal extension Foo {\n\t@IBAction ↓func barButtonTapped(_ sender: UIButton) {}\n}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateOutletRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateOutletRule.swift index 8c1ba43808..cadc2474d2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateOutletRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateOutletRule.swift @@ -36,7 +36,7 @@ struct PrivateOutletRule: OptInRule { Example( "class Foo { @IBOutlet fileprivate(set) weak var label: UILabel? }", configuration: ["allow_private_set": true] - ) + ), ], triggeringExamples: [ Example("class Foo { @IBOutlet ↓var label: UILabel? }"), @@ -73,7 +73,7 @@ struct PrivateOutletRule: OptInRule { ellipsisButtonDidTouch?(self) } } - """, configuration: ["allow_private_set": false], excludeFromDocumentation: true) + """, configuration: ["allow_private_set": false], excludeFromDocumentation: true), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSubjectRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSubjectRuleExamples.swift index fc119d4d8c..0c2f06a1f4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSubjectRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSubjectRuleExamples.swift @@ -118,7 +118,7 @@ internal struct PrivateSubjectRuleExamples { let goodSubject = PassthroughSubject(true) } """ - ) + ), ] static let triggeringExamples: [Example] = [ @@ -272,6 +272,6 @@ internal struct PrivateSubjectRuleExamples { CurrentValueSubject(true) } """# - ) + ), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift index 1c98aacecd..a9cc469784 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift @@ -10,7 +10,7 @@ import SwiftSyntax /// /// Declare state and state objects as private to prevent setting them from a memberwise initializer, /// which can conflict with the storage management that SwiftUI provides: -@SwiftSyntaxRule +@SwiftSyntaxRule(explicitRewriter: true) struct PrivateSwiftUIStatePropertyRule: OptInRule { var configuration = SeverityConfiguration(.warning) @@ -19,179 +19,9 @@ struct PrivateSwiftUIStatePropertyRule: OptInRule { name: "Private SwiftUI State Properties", description: "SwiftUI state properties should be private", kind: .lint, - nonTriggeringExamples: [ - Example(""" - struct MyApp: App { - @State private var isPlaying: Bool = false - } - """), - Example(""" - struct MyScene: Scene { - @State private var isPlaying: Bool = false - } - """), - Example(""" - struct ContentView: View { - @State private var isPlaying: Bool = false - } - """), - Example(""" - struct ContentView: View { - @State fileprivate var isPlaying: Bool = false - } - """), - Example(""" - struct ContentView: View { - @State private var isPlaying: Bool = false - - struct InnerView: View { - @State private var showsIndicator: Bool = false - } - } - """), - Example(""" - struct MyStruct { - struct ContentView: View { - @State private var isPlaying: Bool = false - } - } - """), - Example(""" - struct MyStruct { - struct ContentView: View { - @State private var isPlaying: Bool = false - } - - @State var nonTriggeringState: Bool = false - } - """), - - Example(""" - struct ContentView: View { - var isPlaying = false - } - """), - Example(""" - struct MyApp: App { - @StateObject private var model = DataModel() - } - """), - Example(""" - struct MyScene: Scene { - @StateObject private var model = DataModel() - } - """), - Example(""" - struct ContentView: View { - @StateObject private var model = DataModel() - } - """), - Example(""" - struct MyStruct { - struct ContentView: View { - @StateObject private var dataModel = DataModel() - } - - @StateObject var nonTriggeringObject = MyModel() - } - """), - Example(""" - struct Foo { - @State var bar = false - } - """), - Example(""" - class Foo: ObservableObject { - @State var bar = Bar() - } - """), - Example(""" - extension MyObject { - struct ContentView: View { - @State private var isPlaying: Bool = false - } - } - """), - Example(""" - actor ContentView: View { - @State private var isPlaying: Bool = false - } - """) - ], - triggeringExamples: [ - Example(""" - struct MyApp: App { - @State ↓var isPlaying: Bool = false - } - """), - Example(""" - struct MyScene: Scene { - @State ↓var isPlaying: Bool = false - } - """), - Example(""" - struct ContentView: View { - @State ↓var isPlaying: Bool = false - } - """), - Example(""" - struct ContentView: View { - struct InnerView: View { - @State private var showsIndicator: Bool = false - } - - @State ↓var isPlaying: Bool = false - } - """), - Example(""" - struct MyStruct { - struct ContentView: View { - @State ↓var isPlaying: Bool = false - } - } - """), - Example(""" - struct MyStruct { - struct ContentView: View { - @State ↓var isPlaying: Bool = false - } - - @State var isPlaying: Bool = false - } - """), - Example(""" - final class ContentView: View { - @State ↓var isPlaying: Bool = false - } - """), - Example(""" - extension MyObject { - struct ContentView: View { - @State ↓var isPlaying: Bool = false - } - } - """), - Example(""" - actor ContentView: View { - @State ↓var isPlaying: Bool = false - } - """), - Example(""" - struct MyApp: App { - @StateObject ↓var model = DataModel() - } - """), - Example(""" - struct MyScene: Scene { - @StateObject ↓var model = DataModel() - } - """), - Example(""" - struct ContentView: View { - @StateObject ↓var model = DataModel() - } - """) - ] + nonTriggeringExamples: PrivateSwiftUIStatePropertyRuleExamples.nonTriggeringExamples, + triggeringExamples: PrivateSwiftUIStatePropertyRuleExamples.triggeringExamples, + corrections: PrivateSwiftUIStatePropertyRuleExamples.corrections ) } @@ -201,59 +31,156 @@ private extension PrivateSwiftUIStatePropertyRule { [ProtocolDeclSyntax.self] } - /// LIFO stack that stores type inheritance clauses for each visited node - /// The last value is the inheritance clause for the most recently visited node - /// A nil value indicates that the node does not provide any inheritance clause - private var visitedTypeInheritances = Stack() + /// LIFO stack that stores if a type conforms to SwiftUI protocols. + /// `true` indicates that SwiftUI state properties should be + /// checked in the scope of the last entered declaration. + private var swiftUITypeScopes = Stack() override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - visitedTypeInheritances.push(node.inheritanceClause) + swiftUITypeScopes.push(node.inheritanceClause.conformsToApplicableSwiftUIProtocol) return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { - visitedTypeInheritances.pop() + override func visitPost(_: ClassDeclSyntax) { + swiftUITypeScopes.pop() } override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - visitedTypeInheritances.push(node.inheritanceClause) + swiftUITypeScopes.push(node.inheritanceClause.conformsToApplicableSwiftUIProtocol) return .visitChildren } - override func visitPost(_ node: StructDeclSyntax) { - visitedTypeInheritances.pop() + override func visitPost(_: StructDeclSyntax) { + swiftUITypeScopes.pop() } override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { - visitedTypeInheritances.push(node.inheritanceClause) + swiftUITypeScopes.push(node.inheritanceClause.conformsToApplicableSwiftUIProtocol) return .visitChildren } - override func visitPost(_ node: ActorDeclSyntax) { - visitedTypeInheritances.pop() + override func visitPost(_: ActorDeclSyntax) { + swiftUITypeScopes.pop() } - override func visitPost(_ node: MemberBlockItemSyntax) { - guard - let decl = node.decl.as(VariableDeclSyntax.self), - let inheritanceClause = visitedTypeInheritances.peek() as? InheritanceClauseSyntax, - inheritanceClause.conformsToApplicableSwiftUIProtocol, - decl.attributes.hasStateAttribute, - !decl.modifiers.containsPrivateOrFileprivate() + override func visitPost(_ node: VariableDeclSyntax) { + guard node.parent?.is(MemberBlockItemSyntax.self) == true, + swiftUITypeScopes.peek() ?? false, + node.containsSwiftUIStateAccessLevelViolation else { return } - violations.append(decl.bindingSpecifier.positionAfterSkippingLeadingTrivia) + if let firstAccessLevelModifier = node.modifiers.accessLevelModifier { + violations.append(firstAccessLevelModifier.positionAfterSkippingLeadingTrivia) + } else { + violations.append(node.bindingSpecifier.positionAfterSkippingLeadingTrivia) + } + } + } + + final class Rewriter: ViolationsSyntaxRewriter { + /// LIFO stack that stores if a type conforms to SwiftUI protocols. + /// `true` indicates that SwiftUI state properties should be + /// checked in the scope of the last entered declaration. + private var swiftUITypeScopes = Stack() + + override func visit(_ node: ClassDeclSyntax) -> DeclSyntax { + swiftUITypeScopes.push(node.inheritanceClause.conformsToApplicableSwiftUIProtocol) + return super.visit(node) + } + + override func visit(_ node: StructDeclSyntax) -> DeclSyntax { + swiftUITypeScopes.push(node.inheritanceClause.conformsToApplicableSwiftUIProtocol) + return super.visit(node) } + + override func visit(_ node: ActorDeclSyntax) -> DeclSyntax { + swiftUITypeScopes.push(node.inheritanceClause.conformsToApplicableSwiftUIProtocol) + return super.visit(node) + } + + override func visitPost(_ node: Syntax) { + if node.is(ClassDeclSyntax.self) || + node.is(StructDeclSyntax.self) || + node.is(ActorDeclSyntax.self) { + swiftUITypeScopes.pop() + } + } + + override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { + guard + node.parent?.is(MemberBlockItemSyntax.self) == true, + swiftUITypeScopes.peek() ?? false, + node.containsSwiftUIStateAccessLevelViolation + else { + return DeclSyntax(node) + } + + correctionPositions.append(node.bindingSpecifier.positionAfterSkippingLeadingTrivia) + + // If there are no modifiers present on the current syntax node, + // then we should retain the binding specifier's leading trivia + // by appending it to our inserted private access level modifier + if node.modifiers.isEmpty { + // Extract the leading trivia from the binding specifier and apply it to the private modifier + let privateModifier = DeclModifierSyntax( + leadingTrivia: node.bindingSpecifier.leadingTrivia, + name: .keyword(.private), + trailingTrivia: .space + ) + + let bindingSpecifier = node.bindingSpecifier.with(\.leadingTrivia, []) + let newNode = node + .with(\.modifiers, [privateModifier]) + .with(\.bindingSpecifier, bindingSpecifier) + return DeclSyntax(newNode) + } + + // If any existing, violating access modifiers are present + // then we should extract their trivia and + // append it to the inserted private access level modifier + let existingAccessLevelModifiers = node.modifiers.filter { $0.asAccessLevelModifier != nil } + // Remove any existing access control modifiers, but preserve any of their leading and trailing trivia + // Existing trivia will be appended to the rewritten access modifier + let previousAccessModifierLeadingTrivia = existingAccessLevelModifiers + .map(\.leadingTrivia) + .reduce(Trivia(pieces: [])) { partialResult, trivia in + partialResult.merging(trivia) + } + + let previousAccessModifierTrailingTrivia = existingAccessLevelModifiers + .map(\.trailingTrivia) + .reduce(Trivia(pieces: [])) { partialResult, trivia in + partialResult.merging(trivia) + } + + let filteredModifiers = node.modifiers.filter { $0.asAccessLevelModifier == nil } + // Extract the leading trivia from the binding specifier and apply it to the private modifier + let privateModifier = DeclModifierSyntax( + leadingTrivia: previousAccessModifierLeadingTrivia, + name: .keyword(.private), + trailingTrivia: previousAccessModifierTrailingTrivia.merging(.space) + ) + + return DeclSyntax( + node.with(\.modifiers, [privateModifier] + filteredModifiers) + ) + } + } +} + +private extension VariableDeclSyntax { + var containsSwiftUIStateAccessLevelViolation: Bool { + attributes.hasStateAttribute && !modifiers.containsPrivateOrFileprivate() } } -private extension InheritanceClauseSyntax { +private extension InheritanceClauseSyntax? { static let applicableSwiftUIProtocols: Set = ["View", "App", "Scene"] var conformsToApplicableSwiftUIProtocol: Bool { - inheritedTypes.containsInheritedType(inheritedTypes: Self.applicableSwiftUIProtocols) + self?.inheritedTypes.containsInheritedType(inheritedTypes: Self.applicableSwiftUIProtocols) ?? false } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRuleExamples.swift new file mode 100644 index 0000000000..3c88ee263f --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRuleExamples.swift @@ -0,0 +1,270 @@ +internal struct PrivateSwiftUIStatePropertyRuleExamples { + static let nonTriggeringExamples: [Example] = [ + Example(""" + struct MyApp: App { + @State private var isPlaying: Bool = false + } + """), + Example(""" + struct MyScene: Scene { + @State private var isPlaying: Bool = false + } + """), + Example(""" + struct CofntentView: View { + @State private var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + @State private var isPlaying: Bool = false + + struct InnerView: View { + @State private var showsIndicator: Bool = false + } + } + """), + Example(""" + struct MyStruct { + struct ContentView: View { + @State private var isPlaying: Bool = false + } + } + """), + Example(""" + struct MyStruct { + struct ContentView: View { + @State private var isPlaying: Bool = false + } + + @State var nonTriggeringState: Bool = false + } + """), + Example(""" + struct ContentView: View { + var s: Int { + @State + var s: Int = 3 + return s + } + + var body: some View { Text("") } + } + """), + Example(""" + struct ContentView: View { + var isPlaying = false + } + """), + Example(""" + struct MyApp: App { + @StateObject private var model = DataModel() + } + """), + Example(""" + struct MyScene: Scene { + @StateObject private var model = DataModel() + } + """), + Example(""" + struct ContentView: View { + @StateObject private var model = DataModel() + } + """), + Example(""" + struct MyStruct { + struct ContentView: View { + @StateObject private var dataModel = DataModel() + } + + @StateObject var nonTriggeringObject = MyModel() + } + """), + Example(""" + struct Foo { + @State var bar = false + } + """), + Example(""" + class Foo: ObservableObject { + @State var bar = Bar() + } + """), + Example(""" + extension MyObject { + struct ContentView: View { + @State private var isPlaying: Bool = false + } + } + """), + Example(""" + actor ContentView: View { + @State private var isPlaying: Bool = false + } + """), + ] + + static let triggeringExamples: [Example] = [ + Example(""" + struct MyApp: App { + @State ↓var isPlaying: Bool = false + } + """), + Example(""" + struct MyScene: Scene { + @State ↓public var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + @State ↓var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + struct InnerView: View { + @State private var showsIndicator: Bool = false + } + + @State ↓var isPlaying: Bool = false + } + """), + Example(""" + struct MyStruct { + struct ContentView: View { + @State ↓var isPlaying: Bool = false + } + } + """), + Example(""" + struct MyStruct { + struct ContentView: View { + @State ↓var isPlaying: Bool = false + } + + @State var isPlaying: Bool = false + } + """), + Example(""" + final class ContentView: View { + @State ↓var isPlaying: Bool = false + } + """), + Example(""" + extension MyObject { + struct ContentView: View { + @State ↓var isPlaying: Bool = false + } + } + """), + Example(""" + actor ContentView: View { + @State ↓var isPlaying: Bool = false + } + """), + Example(""" + struct MyApp: App { + @StateObject ↓var model = DataModel() + } + """), + Example(""" + struct MyScene: Scene { + @StateObject ↓var model = DataModel() + } + """), + Example(""" + struct ContentView: View { + @StateObject ↓var model = DataModel() + } + """), + Example(""" + struct ContentView: View { + @State ↓private(set) var isPlaying = false + """), + Example(""" + struct ContentView: View { + @State ↓fileprivate(set) public var isPlaying = false + """), + ] + + static let corrections: [Example: Example] = [ + Example(""" + struct ContentView: View { + @State ↓var isPlaying: Bool = false + } + """): Example(""" + struct ContentView: View { + @State private var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + @State public ↓var isPlaying: Bool = false + } + """): Example(""" + struct ContentView: View { + @State private var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + @State private(set) ↓var isPlaying: Bool = false + } + """): Example(""" + struct ContentView: View { + @State private var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + @State + /// This will track if the content is currently playing + private(set) + // This is another comment about this property + public ↓var isPlaying: Bool = false + } + """): Example(""" + struct ContentView: View { + @State + /// This will track if the content is currently playing + // This is another comment about this property + private var isPlaying: Bool = false + } + """), + Example(""" + struct MyApp: App { + @State + /// This will track if the content is currently playing + ↓var isPlaying: Bool = false + } + """): Example(""" + struct MyApp: App { + @State + /// This will track if the content is currently playing + private var isPlaying: Bool = false + } + """), + Example(""" + struct MyScene: Scene { + @State /* This is a comment */ ↓var isPlaying: Bool = false + } + """): Example(""" + struct MyScene: Scene { + @State /* This is a comment */ private var isPlaying: Bool = false + } + """), + Example(""" + struct ContentView: View { + @State + /// This will track if the content is currently playing + ↓var isPlaying: Bool = false + } + """): Example(""" + struct ContentView: View { + @State + /// This will track if the content is currently playing + private var isPlaying: Bool = false + } + """), + ] +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateUnitTestRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateUnitTestRule.swift index 32264f5f90..44d0db3b74 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateUnitTestRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateUnitTestRule.swift @@ -62,7 +62,7 @@ struct PrivateUnitTestRule: Rule { private func atest() {} private static func test3() {} } - """) + """), ], triggeringExamples: [ Example(""" @@ -96,7 +96,7 @@ struct PrivateUnitTestRule: Rule { public func test3() {} private ↓func test4() {} } - """) + """), ], corrections: [ Example(""" @@ -122,7 +122,7 @@ struct PrivateUnitTestRule: Rule { @objc private func test3() {} internal func test4() {} } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedInterfaceBuilderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedInterfaceBuilderRule.swift index cd0e3af3bf..662f12c961 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedInterfaceBuilderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedInterfaceBuilderRule.swift @@ -11,11 +11,11 @@ struct ProhibitedInterfaceBuilderRule: OptInRule { kind: .lint, nonTriggeringExamples: [ wrapExample("var label: UILabel!"), - wrapExample("@objc func buttonTapped(_ sender: UIButton) {}") + wrapExample("@objc func buttonTapped(_ sender: UIButton) {}"), ], triggeringExamples: [ wrapExample("@IBOutlet ↓var label: UILabel!"), - wrapExample("@IBAction ↓func buttonTapped(_ sender: UIButton) {}") + wrapExample("@IBAction ↓func buttonTapped(_ sender: UIButton) {}"), ] ) } @@ -36,8 +36,8 @@ private extension ProhibitedInterfaceBuilderRule { } } -private func wrapExample(_ text: String, file: StaticString = #file, line: UInt = #line) -> Example { - return Example(""" +private func wrapExample(_ text: String, file: StaticString = #filePath, line: UInt = #line) -> Example { + Example(""" class ViewController: UIViewController { \(text) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedSuperRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedSuperRule.swift index f6ed403340..f19d2ba4fe 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedSuperRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ProhibitedSuperRule.swift @@ -32,7 +32,7 @@ struct ProhibitedSuperRule: OptInRule { } } } - """) + """), ], triggeringExamples: [ Example(""" @@ -67,7 +67,7 @@ struct ProhibitedSuperRule: OptInRule { } } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift index 13d5380cab..9b65c81127 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift @@ -15,7 +15,7 @@ struct QuickDiscouragedCallRule: OptInRule { func validate(file: SwiftLintFile) -> [StyleViolation] { let dict = file.structureDictionary let testClasses = dict.substructure.filter { - return $0.inheritedTypes.isNotEmpty && + $0.inheritedTypes.isNotEmpty && $0.declarationKind == .class } @@ -34,7 +34,7 @@ struct QuickDiscouragedCallRule: OptInRule { } private func validate(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [StyleViolation] { - return dictionary.traverseDepthFirst { subDict in + dictionary.traverseDepthFirst { subDict in guard let kind = subDict.expressionKind else { return nil } return validate(file: file, kind: kind, dictionary: subDict) } @@ -60,7 +60,7 @@ struct QuickDiscouragedCallRule: OptInRule { } private func violationOffsets(in substructure: [SourceKittenDictionary]) -> [ByteCount] { - return substructure.flatMap { dictionary -> [ByteCount] in + substructure.flatMap { dictionary -> [ByteCount] in let substructure = dictionary.substructure.flatMap { dict -> [SourceKittenDictionary] in if dict.expressionKind == .closure { return dict.substructure diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRuleExamples.swift index 824f9a6442..16ebe4dbdd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRuleExamples.swift @@ -172,7 +172,7 @@ internal struct QuickDiscouragedCallRuleExamples { xitBehavesLike("foo") } } - """) + """), ] static let triggeringExamples: [Example] = [ @@ -323,6 +323,6 @@ internal struct QuickDiscouragedCallRuleExamples { } } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRule.swift index c59e7a32e3..e04c831aba 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRule.swift @@ -48,7 +48,7 @@ private extension ClassDeclSyntax { private extension FunctionDeclSyntax { var isSpecFunction: Bool { - return name.tokenKind == .identifier("spec") && + name.tokenKind == .identifier("spec") && signature.parameterClause.parameters.isEmpty && modifiers.contains(keyword: .override) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRuleExamples.swift index 9a16aa1f3a..3b0e1077bc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedFocusedTestRuleExamples.swift @@ -13,7 +13,7 @@ internal struct QuickDiscouragedFocusedTestRuleExamples { } } } - """) + """), ] static let triggeringExamples = [ @@ -80,6 +80,6 @@ internal struct QuickDiscouragedFocusedTestRuleExamples { ↓fitBehavesLike("foo") } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRule.swift index d326c26d18..14c4bb8acc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRule.swift @@ -48,7 +48,7 @@ private extension ClassDeclSyntax { private extension FunctionDeclSyntax { var isSpecFunction: Bool { - return name.tokenKind == .identifier("spec") && + name.tokenKind == .identifier("spec") && signature.parameterClause.parameters.isEmpty && modifiers.contains(keyword: .override) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRuleExamples.swift index b6490f4715..eb8018cd95 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedPendingTestRuleExamples.swift @@ -13,7 +13,7 @@ internal struct QuickDiscouragedPendingTestRuleExamples { } } } - """) + """), ] static let triggeringExamples = [ @@ -87,6 +87,6 @@ internal struct QuickDiscouragedPendingTestRuleExamples { ↓xitBehavesLike("foo") } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift index 39e7662ee2..7144b5a374 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift @@ -58,7 +58,7 @@ struct RawValueForCamelCasedCodableEnumRule: OptInRule { case notAcceptable case maybeAcceptable = -1 } - """) + """), ], triggeringExamples: [ Example(""" @@ -88,7 +88,7 @@ struct RawValueForCamelCasedCodableEnumRule: OptInRule { case ↓notAcceptable case maybeAcceptable = "maybe_acceptable" } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredDeinitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredDeinitRule.swift index 529b9fd57e..5308a04d39 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredDeinitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredDeinitRule.swift @@ -31,7 +31,7 @@ struct RequiredDeinitRule: OptInRule { deinit { print("Deinit Inner") } } } - """) + """), ], triggeringExamples: [ Example("↓class Apple { }"), @@ -63,7 +63,7 @@ struct RequiredDeinitRule: OptInRule { deinit { } } } - """) + """), ] ) } @@ -83,7 +83,7 @@ private extension RequiredDeinitRule { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } - override func visitPost(_ node: DeinitializerDeclSyntax) { + override func visitPost(_: DeinitializerDeclSyntax) { hasDeinit = true } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredEnumCaseRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredEnumCaseRule.swift index 09ce28d043..e702deee4a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredEnumCaseRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/RequiredEnumCaseRule.swift @@ -104,7 +104,7 @@ struct RequiredEnumCaseRule: OptInRule { case error case notConnected(error: Error) } - """, configuration: exampleConfiguration) + """, configuration: exampleConfiguration), ], triggeringExamples: [ Example(""" @@ -128,7 +128,7 @@ struct RequiredEnumCaseRule: OptInRule { case success case error } - """, configuration: exampleConfiguration) + """, configuration: exampleConfiguration), ] ) } @@ -167,7 +167,7 @@ private extension RequiredEnumCaseRule { private extension EnumDeclSyntax { var enumCasesNames: [String] { - return memberBlock.members + memberBlock.members .flatMap { member -> [String] in guard let enumCaseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { return [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/SelfInPropertyInitializationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/SelfInPropertyInitializationRule.swift index b350f85955..74082ee95c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/SelfInPropertyInitializationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/SelfInPropertyInitializationRule.swift @@ -67,7 +67,13 @@ struct SelfInPropertyInitializationRule: Rule { func calculateA() -> String { "A" } func calculateB() -> String { "B" } } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), + Example(""" + final class NotActuallyReferencingSelf { + let keyPath: Any = \\String.self + let someType: Any = String.self + } + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example(""" @@ -87,7 +93,7 @@ struct SelfInPropertyInitializationRule: Rule { return button }() } - """) + """), ] ) } @@ -102,7 +108,7 @@ private extension SelfInPropertyInitializationRule { return } - let visitor = IdentifierUsageVisitor(identifier: .keyword(.self)) + let visitor = IdentifierUsageVisitor(viewMode: .sourceAccurate) for binding in node.bindings { guard let initializer = binding.initializer, visitor.walk(tree: initializer.value, handler: \.isTokenUsed) else { @@ -116,16 +122,12 @@ private extension SelfInPropertyInitializationRule { } private final class IdentifierUsageVisitor: SyntaxVisitor { - let identifier: TokenKind private(set) var isTokenUsed = false - init(identifier: TokenKind) { - self.identifier = identifier - super.init(viewMode: .sourceAccurate) - } - override func visitPost(_ node: DeclReferenceExprSyntax) { - if node.baseName.tokenKind == identifier, node.keyPathInParent != \MemberAccessExprSyntax.declName { + if node.baseName.tokenKind == .keyword(.self), + node.keyPathInParent != \MemberAccessExprSyntax.declName, + node.keyPathInParent != \KeyPathPropertyComponentSyntax.declName { isTokenUsed = true } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/StrongIBOutletRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/StrongIBOutletRule.swift index 3a5a298efb..8a88d4f866 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/StrongIBOutletRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/StrongIBOutletRule.swift @@ -11,12 +11,12 @@ struct StrongIBOutletRule: OptInRule { kind: .lint, nonTriggeringExamples: [ wrapExample("@IBOutlet var label: UILabel?"), - wrapExample("weak var label: UILabel?") + wrapExample("weak var label: UILabel?"), ], triggeringExamples: [ wrapExample("@IBOutlet ↓weak var label: UILabel?"), wrapExample("@IBOutlet ↓unowned var label: UILabel!"), - wrapExample("@IBOutlet ↓weak var textField: UITextField?") + wrapExample("@IBOutlet ↓weak var textField: UITextField?"), ], corrections: [ wrapExample("@IBOutlet ↓weak var label: UILabel?"): @@ -24,7 +24,7 @@ struct StrongIBOutletRule: OptInRule { wrapExample("@IBOutlet ↓unowned var label: UILabel!"): wrapExample("@IBOutlet var label: UILabel!"), wrapExample("@IBOutlet ↓weak var textField: UITextField?"): - wrapExample("@IBOutlet var textField: UITextField?") + wrapExample("@IBOutlet var textField: UITextField?"), ] ) } @@ -68,8 +68,8 @@ private extension VariableDeclSyntax { } } -private func wrapExample(_ text: String, file: StaticString = #file, line: UInt = #line) -> Example { - return Example(""" +private func wrapExample(_ text: String, file: StaticString = #filePath, line: UInt = #line) -> Example { + Example(""" class ViewController: UIViewController { \(text) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRule.swift index fc2963960c..f68829834d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRule.swift @@ -23,7 +23,7 @@ struct TestCaseAccessibilityRule: OptInRule, SubstitutionCorrectableRule { } } - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { + func substitution(for violationRange: NSRange, in _: SwiftLintFile) -> (NSRange, String)? { (violationRange, "private ") } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRuleExamples.swift index 12d2bc0d15..736c9e6031 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TestCaseAccessibilityRuleExamples.swift @@ -74,7 +74,7 @@ internal struct TestCaseAccessibilityRuleExamples { func testFoo() {} } - """) + """), ] static let triggeringExamples = [ @@ -105,7 +105,7 @@ internal struct TestCaseAccessibilityRuleExamples { final class BarTests: XCTestCase { ↓class Nested {} } - """) + """), ] static let corrections = [ @@ -142,6 +142,6 @@ internal struct TestCaseAccessibilityRuleExamples { private func helperFunction() {} } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift index 524e4bb072..4ef5f9b115 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift @@ -12,7 +12,7 @@ struct TodoRule: Rule { kind: .lint, nonTriggeringExamples: [ Example("// notaTODO:"), - Example("// notaFIXME:") + Example("// notaFIXME:"), ], triggeringExamples: [ Example("// ↓TODO:"), @@ -22,7 +22,7 @@ struct TodoRule: Rule { Example("/* ↓FIXME: */"), Example("/* ↓TODO: */"), Example("/** ↓FIXME: */"), - Example("/** ↓TODO: */") + Example("/** ↓TODO: */"), ].skipWrappingInCommentTests() ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift index 2d5d878ce4..fb353a8ac6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift @@ -14,7 +14,7 @@ struct TypesafeArrayInitRule: AnalyzerRule { enum MyError: Error {} let myResult: Result = .success("") let result: Result = myResult.map { $0 } - """), + """), Example(""" struct IntArray { let elements = [1, 2, 3] @@ -24,44 +24,50 @@ struct TypesafeArrayInitRule: AnalyzerRule { } let ints = IntArray() let intsCopy = ints.map { $0 } - """) + """), ], triggeringExamples: [ Example(""" func f(s: Seq) -> [Seq.Element] { s.↓map({ $0 }) } - """), + """), Example(""" func f(array: [Int]) -> [Int] { array.↓map { $0 } } - """), + """), Example(""" let myInts = [1, 2, 3].↓map { return $0 } - """), + """), Example(""" struct Generator: Sequence, IteratorProtocol { func next() -> Int? { nil } } let array = Generator().↓map { i in i } - """) + """), ], requiresFileOnDisk: true ) private static let parentRule = ArrayInitRule() - private static let mapTypePattern = regex(""" - \\Q \ - \\Q(Self) -> ((Self.Element) throws -> T) throws -> [T]\\E - """) + private static let mapTypePatterns = [ + regex(""" + \\Q \ + \\Q(Self) -> ((Self.Element) throws -> T) throws -> [T]\\E + """), + regex(""" + \\Q (Self) -> ((Self.Element) throws(E) -> T) throws(E) -> [T]\\E + """), + ] func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] { guard let filePath = file.path else { return [] } guard compilerArguments.isNotEmpty else { - Issue.missingCompilerArguments(path: file.path, ruleID: Self.description.identifier).print() + Issue.missingCompilerArguments(path: file.path, ruleID: Self.identifier).print() return [] } return Self.parentRule.validate(file: file) @@ -76,14 +82,16 @@ struct TypesafeArrayInitRule: AnalyzerRule { return false } return pointsToSystemMapType(pointee: request) - } + }.map { StyleViolation(ruleDescription: Self.description, location: $0.location ) } } private func pointsToSystemMapType(pointee: [String: any SourceKitRepresentable]) -> Bool { if let isSystem = pointee["key.is_system"], isSystem.isEqualTo(true), let name = pointee["key.name"], name.isEqualTo("map(_:)"), let typeName = pointee["key.typename"] as? String { - return Self.mapTypePattern.numberOfMatches(in: typeName, range: typeName.fullNSRange) == 1 + return Self.mapTypePatterns.contains { + $0.numberOfMatches(in: typeName, range: typeName.fullNSRange) == 1 + } } return false } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift index 5e2cd82b4b..a0cf64a5b0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnhandledThrowingTaskRule.swift @@ -105,7 +105,7 @@ struct UnhandledThrowingTaskRule: OptInRule { try someThrowingFunc() } } - """) + """), ], triggeringExamples: [ Example(""" @@ -181,7 +181,7 @@ struct UnhandledThrowingTaskRule: OptInRule { try await someThrowingFunction() } } - """) + """), ] ) } @@ -308,7 +308,7 @@ private final class ThrowsVisitor: SyntaxVisitor { } } - override func visitPost(_ node: ThrowStmtSyntax) { + override func visitPost(_: ThrowStmtSyntax) { doesThrow = true } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift index 09534bc962..3a00e4e22f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift @@ -41,7 +41,7 @@ private extension UnneededOverrideRule { } override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { - guard node.isUnneededOverride else { + guard configuration.affectInits, node.isUnneededOverride else { return super.visit(node) } @@ -89,8 +89,8 @@ private extension OverridableDecl { return false } - // Assume having @available changes behavior. - if attributes.contains(attributeNamed: "available") { + // Assume attributes change behavior. + guard attributes.isEmpty else { return false } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift index 25b62636de..87f0e94947 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRuleExamples.swift @@ -17,6 +17,13 @@ struct UnneededOverrideRuleExamples { } """), Example(""" + class Foo { + @objc override func bar() { + super.bar() + } + } + """), + Example(""" class Foo { override func bar() { super.bar() @@ -113,7 +120,7 @@ struct UnneededOverrideRuleExamples { super.bar(value: value) } } - """) + """), ] static let triggeringExamples = [ @@ -167,7 +174,7 @@ struct UnneededOverrideRuleExamples { super.bar(animated: animated, completion: completion) } } - """) + """), ] static let corrections = [ @@ -206,6 +213,21 @@ struct UnneededOverrideRuleExamples { // This is another function func baz() {} } - """) + """), + // Nothing happens to initializers by default. + Example(""" + class Foo { + ↓override func foo() { super.foo() } + override init(i: Int) { + super.init(i: i) + } + } + """): Example(""" + class Foo { + override init(i: Int) { + super.init(i: i) + } + } + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnownedVariableCaptureRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnownedVariableCaptureRule.swift index ab1bbf7e0a..a5af1c0b5c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnownedVariableCaptureRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnownedVariableCaptureRule.swift @@ -19,17 +19,17 @@ struct UnownedVariableCaptureRule: OptInRule { Example(""" final class First {} final class Second { - unowned var value: First - init(value: First) { - self.value = value - } + unowned var value: First + init(value: First) { + self.value = value + } } - """) + """), ], triggeringExamples: [ Example("foo { [↓unowned self] in _ }"), Example("foo { [↓unowned bar] in _ }"), - Example("foo { [bar, ↓unowned self] in _ }") + Example("foo { [bar, ↓unowned self] in _ }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift index a23b4ece77..445aba6edf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift @@ -2,7 +2,7 @@ import SwiftSyntax // TODO: [12/22/2024] Remove deprecation warning after ~2 years. private let warnDeprecatedOnceImpl: Void = { - Issue.ruleDeprecated(ruleID: UnusedCaptureListRule.description.identifier).print() + Issue.ruleDeprecated(ruleID: UnusedCaptureListRule.identifier).print() }() private func warnDeprecatedOnce() { @@ -94,7 +94,7 @@ struct UnusedCaptureListRule: SwiftSyntaxRule, OptInRule { } someInstanceFunction() } - """) + """), ], triggeringExamples: [ Example(""" @@ -134,13 +134,13 @@ struct UnusedCaptureListRule: SwiftSyntaxRule, OptInRule { Example("{ [↓foo] in _ }()"), Example(""" let closure = { [↓weak a] in - // The new `a` immediatly shadows the captured `a` which thus isn't needed. + // The new `a` immediately shadows the captured `a` which thus isn't needed. guard let a = getOptionalValue() else { return } someInstanceFunction() } - """) + """), ] ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRule.swift index 14f1f19332..fb4f701a68 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRule.swift @@ -1,4 +1,4 @@ -@preconcurrency import SwiftSyntax +import SwiftSyntax import SwiftSyntaxBuilder @SwiftSyntaxRule(explicitRewriter: true) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRuleExamples.swift index 4ef8f81fbb..cd76a55d37 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedClosureParameterRuleExamples.swift @@ -62,7 +62,7 @@ enum UnusedClosureParameterRuleExamples { """), Example(#"_ = ["a"].filter { `class` in `class`.hasPrefix("a") }"#), Example("let closure: (Int) -> Void = { `foo` in _ = foo }"), - Example("let closure: (Int) -> Void = { foo in _ = `foo` }") + Example("let closure: (Int) -> Void = { foo in _ = `foo` }"), ] static let triggering = [ @@ -97,7 +97,7 @@ enum UnusedClosureParameterRuleExamples { Example(""" let class1 = "a" _ = ["a"].filter { ↓`class` in `class1`.hasPrefix("a") } - """) + """), ] static let corrections = [ @@ -166,6 +166,6 @@ enum UnusedClosureParameterRuleExamples { let failure: Failure = { _, error in observer.sendFailed(error) } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedControlFlowLabelRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedControlFlowLabelRule.swift index 681c98f27a..1e36d5f07c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedControlFlowLabelRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedControlFlowLabelRule.swift @@ -28,7 +28,7 @@ struct UnusedControlFlowLabelRule: Rule { break loop } } while true - """) + """), ], triggeringExamples: [ Example("↓loop: while true { break }"), @@ -48,7 +48,7 @@ struct UnusedControlFlowLabelRule: Rule { break } } while true - """) + """), ], corrections: [ Example("↓loop: while true { break }"): Example("while true { break }"), @@ -80,7 +80,7 @@ struct UnusedControlFlowLabelRule: Rule { break } } while true - """) + """), ] ) } @@ -99,10 +99,8 @@ private extension UnusedControlFlowLabelRule { guard let violationPosition = node.violationPosition else { return super.visit(node) } - - let newNode = node.statement.with(\.leadingTrivia, node.leadingTrivia) correctionPositions.append(violationPosition) - return visit(newNode).as(StmtSyntax.self) ?? newNode + return visit(node.statement.with(\.leadingTrivia, node.leadingTrivia)) } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift index 4820a6b35e..33e3d23556 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift @@ -6,7 +6,7 @@ struct UnusedDeclarationRule: AnalyzerRule, CollectingRule { var referenced: Set var declared: Set - fileprivate static var empty: FileUSRs { Self(referenced: [], declared: []) } + fileprivate static var empty: Self { Self(referenced: [], declared: []) } } struct DeclaredUSR: Hashable { @@ -28,25 +28,25 @@ struct UnusedDeclarationRule: AnalyzerRule, CollectingRule { requiresFileOnDisk: true ) - func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> UnusedDeclarationRule.FileUSRs { + func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> Self.FileUSRs { guard compilerArguments.isNotEmpty else { - Issue.missingCompilerArguments(path: file.path, ruleID: Self.description.identifier).print() + Issue.missingCompilerArguments(path: file.path, ruleID: Self.identifier).print() return .empty } guard let index = file.index(compilerArguments: compilerArguments), index.value.isNotEmpty else { - Issue.indexingError(path: file.path, ruleID: Self.description.identifier).print() + Issue.indexingError(path: file.path, ruleID: Self.identifier).print() return .empty } guard let editorOpen = (try? Request.editorOpen(file: file.file).sendIfNotDisabled()) .map(SourceKittenDictionary.init) else { - Issue.fileNotReadable(path: file.path, ruleID: Self.description.identifier).print() + Issue.fileNotReadable(path: file.path, ruleID: Self.identifier).print() return .empty } return FileUSRs( - referenced: file.referencedUSRs(index: index), + referenced: file.referencedUSRs(index: index, editorOpen: editorOpen), declared: file.declaredUSRs(index: index, editorOpen: editorOpen, compilerArguments: compilerArguments, @@ -54,8 +54,9 @@ struct UnusedDeclarationRule: AnalyzerRule, CollectingRule { ) } - func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: UnusedDeclarationRule.FileUSRs], - compilerArguments: [String]) -> [StyleViolation] { + func validate(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: Self.FileUSRs], + compilerArguments _: [String]) -> [StyleViolation] { let allReferencedUSRs = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.referenced) } return violationOffsets(declaredUSRs: collectedInfo[file]?.declared ?? [], allReferencedUSRs: allReferencedUSRs) @@ -70,9 +71,9 @@ struct UnusedDeclarationRule: AnalyzerRule, CollectingRule { // Unused declarations are: // 1. all declarations // 2. minus all references - return declaredUSRs + declaredUSRs .filter { !allReferencedUSRs.contains($0.usr) } - .map { $0.nameOffset } + .map(\.nameOffset) .sorted() } } @@ -81,7 +82,7 @@ struct UnusedDeclarationRule: AnalyzerRule, CollectingRule { private extension SwiftLintFile { func index(compilerArguments: [String]) -> SourceKittenDictionary? { - return path + path .flatMap { path in try? Request.index(file: path, arguments: compilerArguments) .send() @@ -89,11 +90,16 @@ private extension SwiftLintFile { .map(SourceKittenDictionary.init) } - func referencedUSRs(index: SourceKittenDictionary) -> Set { - return Set(index.traverseEntitiesDepthFirst { entity -> String? in + func referencedUSRs(index: SourceKittenDictionary, editorOpen: SourceKittenDictionary) -> Set { + Set(index.traverseEntitiesDepthFirst { parent, entity -> String? in if let usr = entity.usr, - let kind = entity.kind, - kind.starts(with: "source.lang.swift.ref") { + let kind = entity.kind, + kind.starts(with: "source.lang.swift.ref"), + !parent.extends(reference: entity), + let line = entity.line, + let column = entity.column, + let nameOffset = stringView.byteOffset(forLine: line, bytePosition: column), + editorOpen.propertyAtOffset(nameOffset, property: \.kind) != "source.lang.swift.decl.extension" { return usr } @@ -101,18 +107,34 @@ private extension SwiftLintFile { }) } - func declaredUSRs(index: SourceKittenDictionary, editorOpen: SourceKittenDictionary, - compilerArguments: [String], configuration: UnusedDeclarationConfiguration) - -> Set { - return Set(index.traverseEntitiesDepthFirst { indexEntity in + func declaredUSRs(index: SourceKittenDictionary, + editorOpen: SourceKittenDictionary, + compilerArguments: [String], + configuration: UnusedDeclarationConfiguration) -> Set { + Set(index.traverseEntitiesDepthFirst { _, indexEntity in self.declaredUSR(indexEntity: indexEntity, editorOpen: editorOpen, compilerArguments: compilerArguments, configuration: configuration) }) } - func declaredUSR(indexEntity: SourceKittenDictionary, editorOpen: SourceKittenDictionary, - compilerArguments: [String], configuration: UnusedDeclarationConfiguration) - -> UnusedDeclarationRule.DeclaredUSR? { + func declaredUSR(indexEntity: SourceKittenDictionary, + editorOpen: SourceKittenDictionary, + compilerArguments: [String], + configuration: UnusedDeclarationConfiguration) -> UnusedDeclarationRule.DeclaredUSR? { + // Skip initializers, deinit, enum cases and subscripts since we can't reliably detect if they're used. + let declarationKindsToSkip: Set = [ + .enumelement, + .extensionProtocol, + .extension, + .extensionEnum, + .extensionClass, + .extensionStruct, + .functionConstructor, + .functionDestructor, + .functionSubscript, + .genericTypeParam, + ] + guard let stringKind = indexEntity.kind, stringKind.starts(with: "source.lang.swift.decl."), !stringKind.contains(".accessor."), @@ -133,21 +155,22 @@ private extension SwiftLintFile { return nil } - if !configuration.includePublicAndOpen, [.public, .open].contains(editorOpen.aclAtOffset(nameOffset)) { + if !configuration.includePublicAndOpen, + [.public, .open].contains(editorOpen.propertyAtOffset(nameOffset, property: \.accessibility)) { return nil } // Skip CodingKeys as they are used for Codable generation if kind == .enum, indexEntity.name == "CodingKeys", - case let allRelatedUSRs = indexEntity.traverseEntitiesDepthFirst(traverseBlock: { $0.usr }), + case let allRelatedUSRs = indexEntity.traverseEntitiesDepthFirst(traverseBlock: { $1.usr }), allRelatedUSRs.contains("s:s9CodingKeyP") { return nil } // Skip `static var allTests` members since those are used for Linux test discovery. if kind == .varStatic, indexEntity.name == "allTests" { - let allTestCandidates = indexEntity.traverseEntitiesDepthFirst { subEntity -> Bool in + let allTestCandidates = indexEntity.traverseEntitiesDepthFirst { _, subEntity -> Bool in subEntity.value["key.is_test_candidate"] as? Bool == true } @@ -187,6 +210,14 @@ private extension SwiftLintFile { } private func shouldIgnoreEntity(_ indexEntity: SourceKittenDictionary, relatedUSRsToSkip: Set) -> Bool { + let declarationAttributesToSkip: Set = [ + .ibaction, + .main, + .nsApplicationMain, + .override, + .uiApplicationMain, + ] + if indexEntity.shouldSkipIndexEntityToWorkAroundSR11985() || indexEntity.shouldSkipRelated(relatedUSRsToSkip: relatedUSRsToSkip) || indexEntity.enclosedSwiftAttributes.contains(where: declarationAttributesToSkip.contains) || @@ -219,25 +250,25 @@ private extension SwiftLintFile { private extension SourceKittenDictionary { var usr: String? { - return value["key.usr"] as? String + value["key.usr"] as? String } var annotatedDeclaration: String? { - return value["key.annotated_decl"] as? String + value["key.annotated_decl"] as? String } var isImplicit: Bool { - return value["key.is_implicit"] as? Bool == true + value["key.is_implicit"] as? Bool == true } - func aclAtOffset(_ offset: ByteCount) -> AccessControlLevel? { + func propertyAtOffset(_ offset: ByteCount, property: KeyPath) -> T? { if let nameOffset, nameOffset == offset, - let acl = accessibility { - return acl + let field = self[keyPath: property] { + return field } for child in substructure { - if let acl = child.aclAtOffset(offset) { + if let acl = child.propertyAtOffset(offset, property: property) { return acl } } @@ -245,7 +276,7 @@ private extension SourceKittenDictionary { } func shouldSkipRelated(relatedUSRsToSkip: Set) -> Bool { - return (value["key.related"] as? [[String: any SourceKitRepresentable]])? + (value["key.related"] as? [[String: any SourceKitRepresentable]])? .compactMap { SourceKittenDictionary($0).usr } .contains(where: relatedUSRsToSkip.contains) == true } @@ -268,7 +299,7 @@ private extension SourceKittenDictionary { "tableView(_:commit:forRowAt:)", "tableView(_:editingStyleForRowAt:)", "tableView(_:willDisplayHeaderView:forSection:)", - "tableView(_:willSelectRowAt:)" + "tableView(_:willSelectRowAt:)", ] return functionsToSkipForSR11985.contains(name) @@ -291,31 +322,24 @@ private extension SourceKittenDictionary { "buildFinalResult(_:)", // https://github.com/apple/swift-evolution/blob/main/proposals/0348-buildpartialblock.md "buildPartialBlock(first:)", - "buildPartialBlock(accumulated:next:)" + "buildPartialBlock(accumulated:next:)", ] return resultBuilderStaticMethods.contains(name) } -} -// Skip initializers, deinit, enum cases and subscripts since we can't reliably detect if they're used. -private let declarationKindsToSkip: Set = [ - .enumelement, - .extensionProtocol, - .extension, - .extensionEnum, - .extensionClass, - .extensionStruct, - .functionConstructor, - .functionDestructor, - .functionSubscript, - .genericTypeParam -] - -private let declarationAttributesToSkip: Set = [ - .ibaction, - .main, - .nsApplicationMain, - .override, - .uiApplicationMain -] + func extends(reference other: Self) -> Bool { + if let kind, kind.starts(with: "source.lang.swift.decl.extension") { + let extendedKind = kind.components(separatedBy: ".").last + return extendedKind != nil && extendedKind == other.referencedKind + } + return false + } + + private var referencedKind: String? { + if let kind, kind.starts(with: "source.lang.swift.ref") { + return kind.components(separatedBy: ".").last + } + return nil + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift index 937838f2e9..393aee95f6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift @@ -136,7 +136,7 @@ struct UnusedDeclarationRuleExamples { acceptComponentBuilder { "hello" } - """) + """), ] + platformSpecificNonTriggeringExamples static let triggeringExamples = [ @@ -207,8 +207,23 @@ struct UnusedDeclarationRuleExamples { } _ = ComponentBuilder() - """) - ] + platformSpecificTriggeringExamples + """), + Example(""" + protocol ↓Foo {} + extension Foo {} + """), + Example(""" + class ↓C {} + extension C {} + """), + ] + ["actor", "enum", "class", "struct"].map { + Example(""" + protocol Foo {} + \($0) ↓FooImpl {} + extension FooImpl {} + extension FooImpl: Foo {} + """, excludeFromDocumentation: true) + } + platformSpecificTriggeringExamples #if os(macOS) private static let platformSpecificNonTriggeringExamples = [ @@ -262,7 +277,7 @@ struct UnusedDeclarationRuleExamples { didSet { print("didSet") } } } - """) + """), ] private static let platformSpecificTriggeringExamples = [ @@ -304,7 +319,7 @@ struct UnusedDeclarationRuleExamples { final class ↓Bar { var ↓foo = Foo() } - """) + """), ] #else private static let platformSpecificNonTriggeringExamples = [Example]() diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift index 31d5be392b..6e1951a482 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift @@ -18,7 +18,7 @@ struct UnusedImportRule: CorrectableRule, AnalyzerRule { ) func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] { - return importUsage(in: file, compilerArguments: compilerArguments).map { importUsage in + importUsage(in: file, compilerArguments: compilerArguments).map { importUsage in StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, characterOffset: importUsage.violationRange?.location ?? 1), @@ -28,7 +28,7 @@ struct UnusedImportRule: CorrectableRule, AnalyzerRule { func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] { let importUsages = importUsage(in: file, compilerArguments: compilerArguments) - let matches = file.ruleEnabled(violatingRanges: importUsages.compactMap({ $0.violationRange }), for: self) + let matches = file.ruleEnabled(violatingRanges: importUsages.compactMap(\.violationRange), for: self) var contents = file.stringView.nsString let description = Self.description @@ -84,7 +84,7 @@ struct UnusedImportRule: CorrectableRule, AnalyzerRule { private func importUsage(in file: SwiftLintFile, compilerArguments: [String]) -> [ImportUsage] { guard compilerArguments.isNotEmpty else { - Issue.missingCompilerArguments(path: file.path, ruleID: Self.description.identifier).print() + Issue.missingCompilerArguments(path: file.path, ruleID: Self.identifier).print() return [] } @@ -113,34 +113,50 @@ private extension SwiftLintFile { unusedImports.subtract( operatorImports( arguments: compilerArguments, - processedTokenOffsets: Set(syntaxMap.tokens.map { $0.offset }) + processedTokenOffsets: Set(syntaxMap.tokens.map(\.offset)) ) ) } - let unusedImportUsages = rangedAndSortedUnusedImports(of: Array(unusedImports)) - .map { ImportUsage.unused(module: $0, range: $1) } - - guard configuration.requireExplicitImports else { - return unusedImportUsages - } - + // Find the missing imports, which should be imported, but are not. let currentModule = (compilerArguments.firstIndex(of: "-module-name")?.advanced(by: 1)) .map { compilerArguments[$0] } - let missingImports = usrFragments + var missingImports = usrFragments .subtracting(imports + [currentModule].compactMap({ $0 })) .filter { module in let modulesAllowedToImportCurrentModule = configuration.allowedTransitiveImports .filter { !unusedImports.contains($0.importedModule) } .filter { $0.transitivelyImportedModules.contains(module) } - .map { $0.importedModule } + .map(\.importedModule) return modulesAllowedToImportCurrentModule.isEmpty || imports.isDisjoint(with: modulesAllowedToImportCurrentModule) } - return unusedImportUsages + missingImports.sorted().map { .missing(module: $0) } + // Check if unused imports were used for transitive imports + var foundUmbrellaModules = Set() + var foundMissingImports = Set() + for missingImport in missingImports { + let umbrellaModules = configuration.allowedTransitiveImports + .filter { $0.transitivelyImportedModules.contains(missingImport) } + .map(\.importedModule) + if umbrellaModules.isEmpty { + continue + } + foundMissingImports.insert(missingImport) + foundUmbrellaModules.formUnion(umbrellaModules.filter(unusedImports.contains)) + } + + unusedImports.subtract(foundUmbrellaModules) + missingImports.subtract(foundMissingImports) + + let unusedImportUsages = rangedAndSortedUnusedImports(of: Array(unusedImports)) + .map { ImportUsage.unused(module: $0, range: $1) } + + return configuration.requireExplicitImports + ? unusedImportUsages + missingImports.sorted().map { .missing(module: $0) } + : unusedImportUsages } func getImportsAndUSRFragments(compilerArguments: [String]) -> (imports: Set, usrFragments: Set) { @@ -162,7 +178,7 @@ private extension SwiftLintFile { file: path!, offset: token.offset, arguments: compilerArguments ) guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()).map(SourceKittenDictionary.init) else { - Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.description.identifier).print() + Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.identifier).print() continue } if nextIsModuleImport { @@ -186,7 +202,7 @@ private extension SwiftLintFile { } func rangedAndSortedUnusedImports(of unusedImports: [String]) -> [(String, NSRange)] { - return unusedImports + unusedImports .compactMap { module in match(pattern: "^(@(?!_exported)\\w+ +)?import +\(module)\\b.*?\n").first.map { (module, $0.0) } } @@ -197,7 +213,7 @@ private extension SwiftLintFile { func operatorImports(arguments: [String], processedTokenOffsets: Set) -> Set { guard let index = (try? Request.index(file: path!, arguments: arguments).sendIfNotDisabled()) .map(SourceKittenDictionary.init) else { - Issue.indexingError(path: path, ruleID: UnusedImportRule.description.identifier).print() + Issue.indexingError(path: path, ruleID: UnusedImportRule.identifier).print() return [] } @@ -220,7 +236,7 @@ private extension SwiftLintFile { ) guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()) .map(SourceKittenDictionary.init) else { - Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.description.identifier).print() + Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.identifier).print() continue } @@ -240,7 +256,7 @@ private extension SwiftLintFile { } func offsetPerLine() -> [Int: Int64] { - return Dictionary( + Dictionary( uniqueKeysWithValues: contents.bridge() .components(separatedBy: "\n") .map { Int64($0.bridge().lengthOfBytes(using: .utf8)) } @@ -259,7 +275,7 @@ private extension SwiftLintFile { guard let kind else { return false } return [ "source.lang.swift.ref.function.operator", - "source.lang.swift.ref.function.method.static" + "source.lang.swift.ref.function.method.static", ].contains { kind.hasPrefix($0) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRuleExamples.swift index c8ac95135c..fdd33285e1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRuleExamples.swift @@ -24,7 +24,34 @@ struct UnusedImportRuleExamples { import Foundation let 👨‍👩‍👧‍👦 = #selector(NSArray.contains(_:)) 👨‍👩‍👧‍👦 == 👨‍👩‍👧‍👦 - """) + """), + Example(""" + import Foundation + enum E { + static let min: CGFloat = 44 + } + """, configuration: [ + "allowed_transitive_imports": [ + [ + "module": "Foundation", + "allowed_transitive_imports": ["CoreFoundation"], + ] as [String: any Sendable], + ], + ]), + Example(""" + import SwiftUI + + final class EditMode: ObservableObject { + @Published var isEditing = false + } + """, configuration: [ + "allowed_transitive_imports": [ + [ + "module": "SwiftUI", + "allowed_transitive_imports": ["Foundation"], + ] as [String: any Sendable], + ], + ], excludeFromDocumentation: true), ] static let triggeringExamples = [ @@ -62,7 +89,7 @@ struct UnusedImportRuleExamples { ↓import Swift ↓import SwiftShims func foo(error: Swift.Error) {} - """) + """), ] static let corrections = [ @@ -152,30 +179,35 @@ struct UnusedImportRuleExamples { class A {} """), Example(""" - ↓↓import Foundation + import Foundation typealias Foo = CFArray + dispatchMain() """, configuration: [ "require_explicit_imports": true, "allowed_transitive_imports": [ [ "module": "Foundation", - "allowed_transitive_imports": ["CoreFoundation"] - ] as [String: any Sendable] - ] + "allowed_transitive_imports": ["CoreFoundation", "Dispatch"], + ] as [String: any Sendable], + ], ] as [String: any Sendable], testMultiByteOffsets: false, testOnLinux: false): Example(""" - import CoreFoundation + import Foundation typealias Foo = CFArray + dispatchMain() """), Example(""" - ↓↓import Foundation + ↓↓↓import Foundation typealias Foo = CFData + dispatchMain() """, configuration: [ "require_explicit_imports": true ], testMultiByteOffsets: false, testOnLinux: false): Example(""" import CoreFoundation + import Dispatch typealias Foo = CFData + dispatchMain() """), Example(""" import Foundation @@ -187,9 +219,9 @@ struct UnusedImportRuleExamples { "allowed_transitive_imports": [ [ "module": "Foundation", - "allowed_transitive_imports": ["CoreFoundation"] - ] as [String: any Sendable] - ] + "allowed_transitive_imports": ["CoreFoundation"], + ] as [String: any Sendable], + ], ] as [String: any Sendable]): Example(""" import Foundation @@ -229,6 +261,6 @@ struct UnusedImportRuleExamples { """): Example(""" func foo(error: Swift.Error) {} - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedParameterRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedParameterRule.swift new file mode 100644 index 0000000000..70cfe125b3 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedParameterRule.swift @@ -0,0 +1,202 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule +struct UnusedParameterRule: SwiftSyntaxCorrectableRule, OptInRule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "unused_parameter", + name: "Unused Parameter", + description: """ + Other than unused local variable declarations, unused function/initializer/subscript parameters are not \ + marked by the Swift compiler. Since unused parameters are code smells, they should either be removed \ + or replaced/shadowed by a wildcard '_' to indicate that they are being deliberately disregarded. + """, + kind: .lint, + nonTriggeringExamples: [ + Example(""" + func f(a: Int) { + _ = a + } + """), + Example(""" + func f(case: Int) { + _ = `case` + } + """), + Example(""" + func f(a _: Int) {} + """), + Example(""" + func f(_: Int) {} + """), + Example(""" + func f(a: Int, b c: String) { + func g() { + _ = a + _ = c + } + } + """), + Example(""" + func f(a: Int, c: Int) -> Int { + struct S { + let b = 1 + func f(a: Int, b: Int = 2) -> Int { a + b } + } + return a + c + } + """), + Example(""" + func f(a: Int?) { + if let a {} + } + """), + Example(""" + func f(a: Int) { + let a = a + return a + } + """), + Example(""" + func f(`operator`: Int) -> Int { `operator` } + """), + ], + triggeringExamples: [ + Example(""" + func f(↓a: Int) {} + """), + Example(""" + func f(↓a: Int, b ↓c: String) {} + """), + Example(""" + func f(↓a: Int, b ↓c: String) { + func g(a: Int, ↓b: Double) { + _ = a + } + } + """), + Example(""" + struct S { + let a: Int + + init(a: Int, ↓b: Int) { + func f(↓a: Int, b: Int) -> Int { b } + self.a = f(a: a, b: 0) + } + } + """), + Example(""" + struct S { + subscript(a: Int, ↓b: Int) { + func f(↓a: Int, b: Int) -> Int { b } + return f(a: a, b: 0) + } + } + """), + Example(""" + func f(↓a: Int, ↓b: Int, c: Int) -> Int { + struct S { + let b = 1 + func f(a: Int, ↓c: Int = 2) -> Int { a + b } + } + return S().f(a: c) + } + """), + Example(""" + func f(↓a: Int, c: String) { + let a = 1 + return a + c + } + """), + ], + corrections: [ + Example(""" + func f(a: Int) {} + """): Example(""" + func f(a _: Int) {} + """), + Example(""" + func f(a b: Int) {} + """): Example(""" + func f(a _: Int) {} + """), + Example(""" + func f(_ a: Int) {} + """): Example(""" + func f(_: Int) {} + """), + ] + ) +} + +// MARK: Visitor + +private extension UnusedParameterRule { + final class Visitor: DeclaredIdentifiersTrackingVisitor { + private var referencedDeclarations = Set() + + override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] } + + // MARK: Violation checking + + override func visitPost(_ node: CodeBlockItemListSyntax) { + let declarations = scope.peek() ?? [] + for declaration in declarations.reversed() where !referencedDeclarations.contains(declaration) { + guard case let .parameter(name) = declaration, + let previousToken = name.previousToken(viewMode: .sourceAccurate) else { + continue + } + let startPosReplacement = + if previousToken.tokenKind == .wildcard { + (previousToken.positionAfterSkippingLeadingTrivia, "_") + } else if case .identifier = previousToken.tokenKind { + (name.positionAfterSkippingLeadingTrivia, "_") + } else { + (name.positionAfterSkippingLeadingTrivia, name.text + " _") + } + violations.append(.init( + position: name.positionAfterSkippingLeadingTrivia, + reason: "Parameter '\(name.text)' is unused; consider removing or replacing it with '_'", + severity: configuration.severity, + correction: .init( + start: startPosReplacement.0, + end: name.endPositionBeforeTrailingTrivia, + replacement: startPosReplacement.1 + ) + )) + } + super.visitPost(node) + } + + // MARK: Reference collection + + override func visitPost(_ node: DeclReferenceExprSyntax) { + if node.keyPathInParent != \MemberAccessExprSyntax.declName { + addReference(node.baseName.text) + } + } + + override func visitPost(_ node: OptionalBindingConditionSyntax) { + if node.initializer == nil, let id = node.pattern.as(IdentifierPatternSyntax.self)?.identifier.text { + addReference(id) + } + } + + // MARK: Private methods + + private func addReference(_ id: String) { + for declarations in scope.reversed() { + if declarations.onlyElement == .lookupBoundary { + return + } + for declaration in declarations.reversed() where declaration.declares(id: id) { + if referencedDeclarations.insert(declaration).inserted { + return + } + } + } + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift index 6624acca97..a3073f47b5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift @@ -61,7 +61,7 @@ struct UnusedSetterValueRule: Rule { } set {} } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example(""" @@ -124,7 +124,7 @@ struct UnusedSetterValueRule: Rule { Persister.shared.aValue = aValue } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ValidIBInspectableRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ValidIBInspectableRule.swift index 7ded48d717..922d4034af 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ValidIBInspectableRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ValidIBInspectableRule.swift @@ -68,7 +68,7 @@ struct ValidIBInspectableRule: Rule { } } } - """) + """), ], triggeringExamples: [ Example(""" @@ -105,7 +105,7 @@ struct ValidIBInspectableRule: Rule { class Foo { @IBInspectable private ↓var x: Optional } - """) + """), ] ) @@ -122,7 +122,7 @@ struct ValidIBInspectableRule: Rule { "UIColor", "NSColor", "UIImage", - "NSImage" + "NSImage", ] let types = [ @@ -135,7 +135,7 @@ struct ValidIBInspectableRule: Rule { "CGSize", "NSSize", "CGRect", - "NSRect" + "NSRect", ] let intTypes: [String] = ["", "8", "16", "32", "64"].flatMap { size in @@ -186,7 +186,7 @@ private extension VariableDeclSyntax { } return bindings.allSatisfy { binding in - guard let accessorBlock = binding.accessorBlock?.as(AccessorBlockSyntax.self) else { + guard let accessorBlock = binding.accessorBlock else { return true } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift index b55e70d007..42bf2f3540 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift @@ -50,7 +50,7 @@ struct WeakDelegateRule: OptInRule { } } """, excludeFromDocumentation: true), - Example("private var appDelegate: String?", excludeFromDocumentation: true) + Example("private var appDelegate: String?", excludeFromDocumentation: true), ], triggeringExamples: [ Example("class Foo {\n ↓var delegate: SomeProtocol?\n}"), @@ -62,7 +62,7 @@ struct WeakDelegateRule: OptInRule { print("Updated delegate") } } - """) + """), ] ) } @@ -123,7 +123,7 @@ private extension VariableDeclSyntax { let ignoredAttributes: Set = [ "UIApplicationDelegateAdaptor", "NSApplicationDelegateAdaptor", - "WKExtensionDelegateAdaptor" + "WKExtensionDelegateAdaptor", ] return attributes.contains { attr in diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift index 55f692da79..afff97830c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift @@ -21,7 +21,7 @@ struct YodaConditionRule: OptInRule { Example("if foo == nil {}"), Example("if flags & 1 == 1 {}"), Example("if true {}", excludeFromDocumentation: true), - Example("if true == false || b, 2 != 3, {}", excludeFromDocumentation: true) + Example("if true == false || b, 2 != 3, {}", excludeFromDocumentation: true), ], triggeringExamples: [ Example("if ↓42 == foo {}"), @@ -32,7 +32,7 @@ struct YodaConditionRule: OptInRule { Example("while ↓1 < foo { }"), Example("if ↓nil == foo {}"), Example("while ↓1 > i + 5 {}"), - Example("if ↓200 <= i && i <= 299 || ↓600 <= i {}") + Example("if ↓200 <= i && i <= 299 || ↓600 <= i {}"), ]) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRuleExamples.swift index d5e7cda8ba..db248733f9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRuleExamples.swift @@ -13,7 +13,7 @@ internal struct ClosureBodyLengthRuleExamples { labeledArgumentClosure(codeLinesCount: 29), multiLabeledArgumentClosures(codeLinesCount: 29), labeledAndTrailingClosures(codeLinesCount: 29), - lazyInitialization(codeLinesCount: 28) + lazyInitialization(codeLinesCount: 28), ] static let triggeringExamples: [Example] = [ @@ -23,23 +23,23 @@ internal struct ClosureBodyLengthRuleExamples { labeledArgumentClosure("↓", codeLinesCount: 31), multiLabeledArgumentClosures("↓", codeLinesCount: 31), labeledAndTrailingClosures("↓", codeLinesCount: 31), - lazyInitialization("↓", codeLinesCount: 29) + lazyInitialization("↓", codeLinesCount: 29), ] } // MARK: - Private -private func singleLineClosure(file: StaticString = #file, line: UInt = #line) -> Example { - return Example("foo.bar { $0 }", file: file, line: line) +private func singleLineClosure(file: StaticString = #filePath, line: UInt = #line) -> Example { + Example("foo.bar { $0 }", file: file, line: line) } private func trailingClosure(_ violationSymbol: String = "", codeLinesCount: Int, commentLinesCount: Int, emptyLinesCount: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line) -> Example { - return Example(""" + Example(""" foo.bar \(violationSymbol){ toto in \((0.. Example { - return Example(""" + Example(""" foo.bar(\(violationSymbol){ toto in \((0.. Example { - return Example(""" + Example(""" foo.bar(label: \(violationSymbol){ toto in \((0.. Example { - return Example(""" + Example(""" foo.bar(label: \(violationSymbol){ toto in \((0.. Example { - return Example(""" + Example(""" foo.bar(label: \(violationSymbol){ toto in \((0.. Example { - return Example(""" + Example(""" let foo: Bar = \(violationSymbol){ toto in \tlet bar = Bar() \((0.. SyntaxVisitorContinueKind { + override func visit(_: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { .skipChildren } - override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { .skipChildren } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/EnumCaseAssociatedValuesLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/EnumCaseAssociatedValuesLengthRule.swift index 0cc37a4980..5804d2a377 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/EnumCaseAssociatedValuesLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/EnumCaseAssociatedValuesLengthRule.swift @@ -20,7 +20,7 @@ struct EnumCaseAssociatedValuesLengthRule: OptInRule { enum Barcode { case upc(Int, Int, Int, Int) } - """) + """), ], triggeringExamples: [ Example(""" @@ -33,7 +33,7 @@ struct EnumCaseAssociatedValuesLengthRule: OptInRule { enum Barcode { case ↓upc(Int, Int, Int, Int, Int, Int) } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift index 4604c467d4..6679fec0bf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift @@ -14,7 +14,7 @@ struct FileLengthRule: Rule { triggeringExamples: [ Example(repeatElement("print(\"swiftlint\")\n", count: 401).joined()), Example((repeatElement("print(\"swiftlint\")\n", count: 400) + ["//\n"]).joined()), - Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()) + Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()), ].skipWrappingInCommentTests() ) @@ -22,7 +22,7 @@ struct FileLengthRule: Rule { func lineCountWithoutComments() -> Int { let commentKinds = SyntaxKind.commentKinds return file.syntaxKindsByLines.filter { kinds in - return !Set(kinds).isSubset(of: commentKinds) + !Set(kinds).isSubset(of: commentKinds) }.count } @@ -39,10 +39,14 @@ struct FileLengthRule: Rule { let reason = "File should contain \(configuration.severityConfiguration.warning) lines or less" + (configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") + ": currently contains \(lineCount)" - return [StyleViolation(ruleDescription: Self.description, - severity: parameter.severity, - location: Location(file: file.path, line: file.lines.count), - reason: reason)] + return [ + StyleViolation( + ruleDescription: Self.description, + severity: parameter.severity, + location: Location(file: file.path, line: file.lines.count), + reason: reason + ), + ] } return [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionParameterCountRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionParameterCountRule.swift index 6990fe89fb..27895f1c0c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionParameterCountRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionParameterCountRule.swift @@ -22,7 +22,7 @@ struct FunctionParameterCountRule: Rule { func f(a: [Int], b: Int, c: Int, d: Int, f: Int) -> [Int] { let s = a.flatMap { $0 as? [String: Int] } ?? []}} """), - Example("override func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {}") + Example("override func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {}"), ], triggeringExamples: [ Example("↓func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {}"), @@ -32,7 +32,7 @@ struct FunctionParameterCountRule: Rule { struct Foo { init(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} ↓func bar(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {}} - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift index e801fb531d..ec604610ee 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift @@ -27,7 +27,7 @@ struct LargeTupleRuleExamples { Example("func foo(bar: (Int, String, Float) async -> Void)"), Example("func foo(bar: (Int, String, Float) async throws -> Void)"), Example("func getDictionaryAndInt() async -> (Dictionary, Int)?"), - Example("func getGenericTypeAndInt() async -> (Type, Int)?") + Example("func getGenericTypeAndInt() async -> (Type, Int)?"), ] static let triggeringExamples: [Example] = [ @@ -51,6 +51,6 @@ struct LargeTupleRuleExamples { Example("func foo() async throws -> ↓(Int, Int, Int)"), Example("func foo() async throws -> ↓(Int, Int, Int) {}"), Example("func foo() async throws -> ↓(Int, ↓(String, String, String), Int) {}"), - Example("func getDictionaryAndInt() async -> (Dictionary, Int)?") + Example("func getDictionaryAndInt() async -> (Dictionary, Int)?"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift index d016b35915..161378b01a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift @@ -16,17 +16,17 @@ struct LineLengthRule: Rule { nonTriggeringExamples: [ Example(String(repeating: "/", count: 120) + ""), Example(String(repeating: "#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)", count: 120) + ""), - Example(String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 120) + "") + Example(String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 120) + ""), ], triggeringExamples: [ Example(String(repeating: "/", count: 121) + ""), Example(String(repeating: "#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)", count: 121) + ""), - Example(String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 121) + "") + Example(String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 121) + ""), ].skipWrappingInCommentTests().skipWrappingInStringTests() ) func validate(file: SwiftLintFile) -> [StyleViolation] { - let minValue = configuration.params.map({ $0.value }).min() ?? .max + let minValue = configuration.params.map(\.value).min() ?? .max let swiftDeclarationKindsByLine = Lazy(file.swiftDeclarationKindsByLine() ?? []) let syntaxKindsByLine = Lazy(file.syntaxKindsByLine() ?? []) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift index 2e9b95662e..7eb1af8163 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift @@ -50,7 +50,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: ActorDeclSyntax) { + override func visitPost(_: ActorDeclSyntax) { levels.pop() } @@ -59,7 +59,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { + override func visitPost(_: ClassDeclSyntax) { levels.pop() } @@ -68,7 +68,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: EnumDeclSyntax) { + override func visitPost(_: EnumDeclSyntax) { levels.pop() } @@ -77,7 +77,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: ExtensionDeclSyntax) { + override func visitPost(_: ExtensionDeclSyntax) { levels.pop() } @@ -86,7 +86,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: FunctionDeclSyntax) { + override func visitPost(_: FunctionDeclSyntax) { levels.pop() } @@ -95,7 +95,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: ProtocolDeclSyntax) { + override func visitPost(_: ProtocolDeclSyntax) { levels.pop() } @@ -104,7 +104,7 @@ private extension NestingRule { return .visitChildren } - override func visitPost(_ node: StructDeclSyntax) { + override func visitPost(_: StructDeclSyntax) { levels.pop() } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift index 71622e264a..d94c196068 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift @@ -17,7 +17,7 @@ internal struct NestingRuleExamples { \(type) Example_0 { \(type) Example_1 {} } - """), + """), /* all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) @@ -30,7 +30,7 @@ internal struct NestingRuleExamples { } return 5 } - """), + """), // didSet is not present in file structure although there is such a swift declaration kind .init(""" @@ -41,14 +41,14 @@ internal struct NestingRuleExamples { } } } - """), + """), // extensions are counted as a type level .init(""" extension Example_0 { \(type) Example_1 {} } - """) + """), ] } @@ -60,7 +60,7 @@ internal struct NestingRuleExamples { func f_2() {} } } - """), + """), /* all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) @@ -75,7 +75,7 @@ internal struct NestingRuleExamples { } return 5 } - """), + """), // didSet is not present in file structure although there is such a swift declaration kind .init(""" @@ -88,7 +88,7 @@ internal struct NestingRuleExamples { } } } - """), + """), // extensions are counted as a type level .init(""" @@ -99,17 +99,17 @@ internal struct NestingRuleExamples { } } } - """) + """), ] private static let nonTriggeringProtocolExamples = detectingTypes.flatMap { type in [ Example(""" - \(type) Example_0 { - protocol Example_1 {} - } - """), + \(type) Example_0 { + protocol Example_1 {} + } + """), Example(""" var example: Int { \(type) Example_0 { @@ -117,7 +117,7 @@ internal struct NestingRuleExamples { } return 5 } - """), + """), Example(""" var example: Int = 5 { didSet { @@ -126,12 +126,12 @@ internal struct NestingRuleExamples { } } } - """), + """), Example(""" extension Example_0 { protocol Example_1 {} } - """) + """), ] } @@ -152,7 +152,7 @@ internal struct NestingRuleExamples { } } } - """), + """), // closure var example .init(""" @@ -166,7 +166,7 @@ internal struct NestingRuleExamples { } } } - """), + """), // function closure parameter example .init(""" @@ -180,7 +180,7 @@ internal struct NestingRuleExamples { } } }) - """) + """), ] } @@ -199,7 +199,7 @@ internal struct NestingRuleExamples { protocol P {} } } - """), + """), // default maximum nesting level for both type and function within closures and statements .init(""" @@ -225,7 +225,7 @@ internal struct NestingRuleExamples { } } } - """) + """), ] } } @@ -247,7 +247,7 @@ extension NestingRuleExamples { ↓\(type) Example_2 {} } } - """), + """), /* all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) @@ -262,7 +262,7 @@ extension NestingRuleExamples { } return 5 } - """), + """), // didSet is not present in file structure although there is such a swift declaration kind .init(""" @@ -275,7 +275,7 @@ extension NestingRuleExamples { } } } - """), + """), // extensions are counted as a type level, violation of default maximum type nesting level .init(""" @@ -284,7 +284,7 @@ extension NestingRuleExamples { ↓\(type) Example_2 {} } } - """) + """), ] } @@ -298,7 +298,7 @@ extension NestingRuleExamples { } } } - """), + """), /* all variableKinds of SwiftDeclarationKind (except .varParameter which is a function parameter) @@ -315,7 +315,7 @@ extension NestingRuleExamples { } return 5 } - """), + """), // didSet is not present in file structure although there is such a swift declaration kind .init(""" @@ -330,7 +330,7 @@ extension NestingRuleExamples { } } } - """), + """), // extensions are counted as a type level, violation of default maximum function nesting level .init(""" @@ -343,7 +343,7 @@ extension NestingRuleExamples { } } } - """) + """), ] private static let triggeringClosureAndStatementExamples = @@ -367,7 +367,7 @@ extension NestingRuleExamples { } } } - """), + """), // closure var example .init(""" @@ -385,7 +385,7 @@ extension NestingRuleExamples { } } } - """), + """), // function closure parameter example .init(""" @@ -401,7 +401,7 @@ extension NestingRuleExamples { } } }) - """) + """), ] } @@ -409,12 +409,12 @@ extension NestingRuleExamples { detectingTypes.flatMap { type in [ Example(""" - \(type) Example_0 { - \(type) Example_1 { - ↓protocol Example_2 {} + \(type) Example_0 { + \(type) Example_1 { + ↓protocol Example_2 {} + } } - } - """), + """), Example(""" var example: Int { \(type) Example_0 { @@ -424,7 +424,7 @@ extension NestingRuleExamples { } return 5 } - """), + """), Example(""" var example: Int = 5 { didSet { @@ -435,14 +435,14 @@ extension NestingRuleExamples { } } } - """), + """), Example(""" extension Example_0 { \(type) Example_1 { ↓protocol Example_2 {} } } - """) + """), ] } @@ -464,7 +464,7 @@ extension NestingRuleExamples { } } } - """), + """), // violation of default maximum nesting level for both type and function within closures and statements .init(""" @@ -496,7 +496,7 @@ extension NestingRuleExamples { } } } - """) + """), ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index 1852f791e3..0c3da3c68b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -4,9 +4,9 @@ private func wrapExample( _ template: String, _ count: Int, _ add: String = "", - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line) -> Example { - return Example("\(prefix)\(type) Abc {\n" + + Example("\(prefix)\(type) Abc {\n" + repeatElement(template, count: count).joined() + "\(add)}\n", file: file, line: line) } @@ -23,7 +23,7 @@ struct TypeBodyLengthRule: SwiftSyntaxRule { wrapExample(type, "let abc = 0\n", 249), wrapExample(type, "\n", 251), wrapExample(type, "// this is a comment\n", 251), - wrapExample(type, "let abc = 0\n", 249, "\n/* this is\na multiline comment\n*/") + wrapExample(type, "let abc = 0\n", 249, "\n/* this is\na multiline comment\n*/"), ] }), triggeringExamples: ["class", "struct", "enum", "actor"].map({ type in diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterCountRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterCountRule.swift index ba1ddb4ca9..ba5eae2559 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterCountRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterCountRule.swift @@ -10,21 +10,21 @@ struct ContainsOverFilterCountRule: OptInRule { description: "Prefer `contains` over comparing `filter(where:).count` to 0", kind: .performance, nonTriggeringExamples: [">", "==", "!="].flatMap { operation in - return [ + [ Example("let result = myList.filter(where: { $0 % 2 == 0 }).count \(operation) 1"), Example("let result = myList.filter { $0 % 2 == 0 }.count \(operation) 1"), - Example("let result = myList.filter(where: { $0 % 2 == 0 }).count \(operation) 01") + Example("let result = myList.filter(where: { $0 % 2 == 0 }).count \(operation) 01"), ] } + [ Example("let result = myList.contains(where: { $0 % 2 == 0 })"), Example("let result = !myList.contains(where: { $0 % 2 == 0 })"), - Example("let result = myList.contains(10)") + Example("let result = myList.contains(10)"), ], triggeringExamples: [">", "==", "!="].flatMap { operation in - return [ + [ Example("let result = ↓myList.filter(where: { $0 % 2 == 0 }).count \(operation) 0"), Example("let result = ↓myList.filter { $0 % 2 == 0 }.count \(operation) 0"), - Example("let result = ↓myList.filter(where: someFunction).count \(operation) 0") + Example("let result = ↓myList.filter(where: someFunction).count \(operation) 0"), ] } ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterIsEmptyRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterIsEmptyRule.swift index 2d08935c26..d2546c70cf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterIsEmptyRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFilterIsEmptyRule.swift @@ -10,20 +10,20 @@ struct ContainsOverFilterIsEmptyRule: OptInRule { description: "Prefer `contains` over using `filter(where:).isEmpty`", kind: .performance, nonTriggeringExamples: [">", "==", "!="].flatMap { operation in - return [ + [ Example("let result = myList.filter(where: { $0 % 2 == 0 }).count \(operation) 1"), - Example("let result = myList.filter { $0 % 2 == 0 }.count \(operation) 1") + Example("let result = myList.filter { $0 % 2 == 0 }.count \(operation) 1"), ] } + [ Example("let result = myList.contains(where: { $0 % 2 == 0 })"), Example("let result = !myList.contains(where: { $0 % 2 == 0 })"), - Example("let result = myList.contains(10)") + Example("let result = myList.contains(10)"), ], triggeringExamples: [ Example("let result = ↓myList.filter(where: { $0 % 2 == 0 }).isEmpty"), Example("let result = !↓myList.filter(where: { $0 % 2 == 0 }).isEmpty"), Example("let result = ↓myList.filter { $0 % 2 == 0 }.isEmpty"), - Example("let result = ↓myList.filter(where: someFunction).isEmpty") + Example("let result = ↓myList.filter(where: someFunction).isEmpty"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFirstNotNilRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFirstNotNilRule.swift index 42469c5c9f..fbe945218e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFirstNotNilRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverFirstNotNilRule.swift @@ -10,20 +10,20 @@ struct ContainsOverFirstNotNilRule: OptInRule { description: "Prefer `contains` over `first(where:) != nil` and `firstIndex(where:) != nil`.", kind: .performance, nonTriggeringExamples: ["first", "firstIndex"].flatMap { method in - return [ + [ Example("let \(method) = myList.\(method)(where: { $0 % 2 == 0 })"), - Example("let \(method) = myList.\(method) { $0 % 2 == 0 }") + Example("let \(method) = myList.\(method) { $0 % 2 == 0 }"), ] }, triggeringExamples: ["first", "firstIndex"].flatMap { method in - return ["!=", "=="].flatMap { comparison in - return [ + ["!=", "=="].flatMap { comparison in + [ Example("↓myList.\(method) { $0 % 2 == 0 } \(comparison) nil"), Example("↓myList.\(method)(where: { $0 % 2 == 0 }) \(comparison) nil"), Example("↓myList.map { $0 + 1 }.\(method)(where: { $0 % 2 == 0 }) \(comparison) nil"), Example("↓myList.\(method)(where: someFunction) \(comparison) nil"), Example("↓myList.map { $0 + 1 }.\(method) { $0 % 2 == 0 } \(comparison) nil"), - Example("(↓myList.\(method) { $0 % 2 == 0 }) \(comparison) nil") + Example("(↓myList.\(method) { $0 % 2 == 0 }) \(comparison) nil"), ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverRangeNilComparisonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverRangeNilComparisonRule.swift index 51577058a5..a0f5857480 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverRangeNilComparisonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ContainsOverRangeNilComparisonRule.swift @@ -6,17 +6,17 @@ struct ContainsOverRangeNilComparisonRule: OptInRule { static let description = RuleDescription( identifier: "contains_over_range_nil_comparison", - name: "Contains over Range Comparision to Nil", + name: "Contains over Range Comparison to Nil", description: "Prefer `contains` over `range(of:) != nil` and `range(of:) == nil`", kind: .performance, nonTriggeringExamples: [ Example("let range = myString.range(of: \"Test\")"), Example("myString.contains(\"Test\")"), Example("!myString.contains(\"Test\")"), - Example("resourceString.range(of: rule.regex, options: .regularExpression) != nil") + Example("resourceString.range(of: rule.regex, options: .regularExpression) != nil"), ], triggeringExamples: ["!=", "=="].flatMap { comparison in - return [ + [ Example("↓myString.range(of: \"Test\") \(comparison) nil") ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCollectionLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCollectionLiteralRule.swift index 1b739a34dc..2d74682cd3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCollectionLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCollectionLiteralRule.swift @@ -13,7 +13,7 @@ struct EmptyCollectionLiteralRule: OptInRule { Example("myArray = []"), Example("myArray.isEmpty"), Example("!myArray.isEmpty"), - Example("myDict = [:]") + Example("myDict = [:]"), ], triggeringExamples: [ Example("myArray↓ == []"), @@ -23,7 +23,7 @@ struct EmptyCollectionLiteralRule: OptInRule { Example("myDict↓ != [:]"), Example("myDict↓ == [: ]"), Example("myDict↓ == [ :]"), - Example("myDict↓ == [ : ]") + Example("myDict↓ == [ : ]"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift index c5293d75cf..c1ffce14f7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift @@ -19,7 +19,7 @@ struct EmptyCountRule: OptInRule { Example("[Int]().count == 0b01"), Example("[Int]().count == 0o07"), Example("discount == 0"), - Example("order.discount == 0") + Example("order.discount == 0"), ], triggeringExamples: [ Example("[Int]().↓count == 0"), @@ -31,7 +31,7 @@ struct EmptyCountRule: OptInRule { Example("[Int]().↓count == 0x00_00"), Example("[Int]().↓count == 0b00"), Example("[Int]().↓count == 0o00"), - Example("↓count == 0") + Example("↓count == 0"), ], corrections: [ Example("[].↓count == 0"): @@ -61,7 +61,7 @@ struct EmptyCountRule: OptInRule { Example("↓count == 0 && [Int]().↓count == 0o00"): Example("isEmpty && [Int]().isEmpty"), Example("[Int]().count != 3 && [Int]().↓count != 0 || ↓count == 0 && [Int]().count > 2"): - Example("[Int]().count != 3 && ![Int]().isEmpty || isEmpty && [Int]().count > 2") + Example("[Int]().count != 3 && ![Int]().isEmpty || isEmpty && [Int]().count > 2"), ] ) } @@ -88,9 +88,9 @@ private extension EmptyCountRule { if let (count, position) = node.countNodeAndPosition(onlyAfterDot: configuration.onlyAfterDot) { let newNode = if let count = count.as(MemberAccessExprSyntax.self) { - count.with(\.declName.baseName, "isEmpty").trimmed.as(ExprSyntax.self) + ExprSyntax(count.with(\.declName.baseName, "isEmpty").trimmed) } else { - count.as(DeclReferenceExprSyntax.self)?.with(\.baseName, "isEmpty").trimmed.as(ExprSyntax.self) + ExprSyntax(count.as(DeclReferenceExprSyntax.self)?.with(\.baseName, "isEmpty").trimmed) } guard let newNode else { return super.visit(node) } correctionPositions.append(position) diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyStringRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyStringRule.swift index faba6f737b..ad199a36a4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyStringRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyStringRule.swift @@ -12,14 +12,14 @@ struct EmptyStringRule: OptInRule { nonTriggeringExamples: [ Example("myString.isEmpty"), Example("!myString.isEmpty"), - Example("\"\"\"\nfoo==\n\"\"\"") + Example("\"\"\"\nfoo==\n\"\"\""), ], triggeringExamples: [ Example(#"myString↓ == """#), Example(#"myString↓ != """#), Example(#"myString↓=="""#), Example(##"myString↓ == #""#"##), - Example(###"myString↓ == ##""##"###) + Example(###"myString↓ == ##""##"###), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/FinalTestCaseRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/FinalTestCaseRule.swift index f71f8bd8c2..5afde4bc10 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/FinalTestCaseRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/FinalTestCaseRule.swift @@ -5,7 +5,7 @@ import SwiftSyntax struct FinalTestCaseRule: OptInRule { var configuration = FinalTestCaseConfiguration() - static var description = RuleDescription( + static let description = RuleDescription( identifier: "final_test_case", name: "Final Test Case", description: "Test cases should be final", @@ -15,18 +15,18 @@ struct FinalTestCaseRule: OptInRule { Example("open class Test: XCTestCase {}"), Example("public final class Test: QuickSpec {}"), Example("class Test: MyTestCase {}"), - Example("struct Test: MyTestCase {}", configuration: ["test_parent_classes": "MyTestCase"]) + Example("struct Test: MyTestCase {}", configuration: ["test_parent_classes": "MyTestCase"]), ], triggeringExamples: [ Example("class ↓Test: XCTestCase {}"), Example("public class ↓Test: QuickSpec {}"), - Example("class ↓Test: MyTestCase {}", configuration: ["test_parent_classes": "MyTestCase"]) + Example("class ↓Test: MyTestCase {}", configuration: ["test_parent_classes": "MyTestCase"]), ], corrections: [ Example("class ↓Test: XCTestCase {}"): Example("final class Test: XCTestCase {}"), Example("internal class ↓Test: XCTestCase {}"): - Example("internal final class Test: XCTestCase {}") + Example("internal final class Test: XCTestCase {}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift index 474e3d8181..4576af9f98 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift @@ -16,7 +16,7 @@ struct FirstWhereRule: OptInRule { Example("(myList.filter { $0 == 1 }.suffix(2)).first"), Example(#"collection.filter("stringCol = '3'").first"#), Example(#"realm?.objects(User.self).filter(NSPredicate(format: "email ==[c] %@", email)).first"#), - Example(#"if let pause = timeTracker.pauses.filter("beginDate < %@", beginDate).first { print(pause) }"#) + Example(#"if let pause = timeTracker.pauses.filter("beginDate < %@", beginDate).first { print(pause) }"#), ], triggeringExamples: [ Example("↓myList.filter { $0 % 2 == 0 }.first"), @@ -27,7 +27,7 @@ struct FirstWhereRule: OptInRule { Example("↓myList.filter({ $0 % 2 == 0 })\n.first"), Example("(↓myList.filter { $0 == 1 }).first"), Example(#"↓myListOfDict.filter { dict in dict["1"] }.first"#), - Example(#"↓myListOfDict.filter { $0["someString"] }.first"#) + Example(#"↓myListOfDict.filter { $0["someString"] }.first"#), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/FlatMapOverMapReduceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/FlatMapOverMapReduceRule.swift index b3e6bfb96b..650990e7cb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/FlatMapOverMapReduceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/FlatMapOverMapReduceRule.swift @@ -11,7 +11,7 @@ struct FlatMapOverMapReduceRule: OptInRule { kind: .performance, nonTriggeringExamples: [ Example("let foo = bar.map { $0.count }.reduce(0, +)"), - Example("let foo = bar.flatMap { $0.array }") + Example("let foo = bar.flatMap { $0.array }"), ], triggeringExamples: [ Example("let foo = ↓bar.map { $0.array }.reduce([], +)") diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift index 7c0c7c0b03..577bc5d34e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift @@ -14,7 +14,7 @@ struct LastWhereRule: OptInRule { Example("myList.last(where: { $0 % 2 == 0 })"), Example("match(pattern: pattern).filter { $0.last == .identifier }"), Example("(myList.filter { $0 == 1 }.suffix(2)).last"), - Example(#"collection.filter("stringCol = '3'").last"#) + Example(#"collection.filter("stringCol = '3'").last"#), ], triggeringExamples: [ Example("↓myList.filter { $0 % 2 == 0 }.last"), @@ -23,7 +23,7 @@ struct LastWhereRule: OptInRule { Example("↓myList.map { $0 + 1 }.filter({ $0 % 2 == 0 }).last?.something()"), Example("↓myList.filter(someFunction).last"), Example("↓myList.filter({ $0 % 2 == 0 })\n.last"), - Example("(↓myList.filter { $0 == 1 }).last") + Example("(↓myList.filter { $0 == 1 }).last"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceBooleanRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceBooleanRule.swift index 4df46b59c2..9298eeb441 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceBooleanRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceBooleanRule.swift @@ -12,7 +12,7 @@ struct ReduceBooleanRule: Rule { nonTriggeringExamples: [ Example("nums.reduce(0) { $0.0 + $0.1 }"), Example("nums.reduce(0.0) { $0.0 + $0.1 }"), - Example("nums.reduce(initial: true) { $0.0 && $0.1 == 3 }") + Example("nums.reduce(initial: true) { $0.0 && $0.1 == 3 }"), ], triggeringExamples: [ Example("let allNines = nums.↓reduce(true) { $0.0 && $0.1 == 9 }"), @@ -23,7 +23,7 @@ struct ReduceBooleanRule: Rule { Example("let anyNines = nums.↓reduce(false, { $0.0 || $0.1 == 9 })"), Example("let allValid = validators.↓reduce(true, { $0 && $1(input) })"), Example("let anyValid = validators.↓reduce(false, { $0 || $1(input) })"), - Example("nums.reduce(into: true) { (r: inout Bool, s) in r = r && (s == 3) }") + Example("nums.reduce(into: true) { (r: inout Bool, s) in r = r && (s == 3) }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift index 14c450b895..d683a2d29e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift @@ -48,7 +48,7 @@ struct ReduceIntoRule: OptInRule { result.handleValue(value) return result } - """) + """), ], triggeringExamples: [ Example(""" @@ -103,7 +103,7 @@ struct ReduceIntoRule: OptInRule { } } } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/SortedFirstLastRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/SortedFirstLastRule.swift index 31e57e23aa..ebe76cb73a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/SortedFirstLastRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/SortedFirstLastRule.swift @@ -26,7 +26,7 @@ struct SortedFirstLastRule: OptInRule { Example("myList.sorted().first(where: someFunction)"), Example("myList.sorted().last(where: someFunction)"), Example("myList.sorted().first { $0 == key }"), - Example("myList.sorted().last { $0 == key }") + Example("myList.sorted().last { $0 == key }"), ], triggeringExamples: [ Example("↓myList.sorted().first"), @@ -41,7 +41,7 @@ struct SortedFirstLastRule: OptInRule { Example("↓myList.map { $0 + 1 }.sorted().last"), Example("↓myList.sorted(by: someFunction).last"), Example("↓myList.map { $0 + 1 }.sorted { $0.description < $1.description }.last"), - Example("↓myList.map { $0 + 1 }.sorted { $0.first < $1.first }.last") + Example("↓myList.map { $0 + 1 }.sorted { $0.first < $1.first }.last"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/AttributesConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/AttributesConfiguration.swift index e9a6fe06b9..da8917e26e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/AttributesConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/AttributesConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct AttributesConfiguration: SeverityBasedRuleConfiguration { typealias Parent = AttributesRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/BlanketDisableCommandConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/BlanketDisableCommandConfiguration.swift index b7bc5e1d31..c960606aa2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/BlanketDisableCommandConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/BlanketDisableCommandConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct BlanketDisableCommandConfiguration: SeverityBasedRuleConfiguration { typealias Parent = BlanketDisableCommandRule @@ -12,7 +12,7 @@ struct BlanketDisableCommandConfiguration: SeverityBasedRuleConfiguration { "file_length", "file_name", "file_name_no_space", - "single_test_class" + "single_test_class", ] @ConfigurationElement(key: "always_blanket_disable") private(set) var alwaysBlanketDisableRuleIdentifiers: Set = [] diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift index 0838689139..0c2ba8eadd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct CollectionAlignmentConfiguration: SeverityBasedRuleConfiguration { typealias Parent = CollectionAlignmentRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ColonConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ColonConfiguration.swift index ee0379a0a7..078ecded56 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ColonConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ColonConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ColonConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ColonRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ComputedAccessorsOrderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ComputedAccessorsOrderConfiguration.swift index 01a3a2e3eb..ce06b9dfa4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ComputedAccessorsOrderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ComputedAccessorsOrderConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ComputedAccessorsOrderConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ComputedAccessorsOrderRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum Order: String { case getSet = "get_set" case setGet = "set_get" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ConditionalReturnsOnNewlineConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ConditionalReturnsOnNewlineConfiguration.swift index 2a44156d46..29f948bef0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ConditionalReturnsOnNewlineConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ConditionalReturnsOnNewlineConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ConditionalReturnsOnNewlineConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ConditionalReturnsOnNewlineRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CyclomaticComplexityConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CyclomaticComplexityConfiguration.swift index c7fc2d380a..0ebcd63fe7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CyclomaticComplexityConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CyclomaticComplexityConfiguration.swift @@ -1,16 +1,16 @@ import SourceKittenFramework import SwiftLintCore -@AutoApply +@AutoConfigParser struct CyclomaticComplexityConfiguration: RuleConfiguration { typealias Parent = CyclomaticComplexityRule - @ConfigurationElement + @ConfigurationElement(inline: true) private(set) var length = SeverityLevelsConfiguration(warning: 10, error: 20) @ConfigurationElement(key: "ignores_case_statements") private(set) var ignoresCaseStatements = false var params: [RuleParameter] { - return length.params + length.params } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift index ca8b32a157..6c7cd87ee1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift @@ -16,18 +16,18 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { rawValue + "_deployment_target" } - var appExtensionCounterpart: Self? { + var appExtensionCounterpart: WritableKeyPath? { switch self { - case .iOS: return Self.iOSApplicationExtension - case .macOS: return Self.macOSApplicationExtension - case .watchOS: return Self.watchOSApplicationExtension - case .tvOS: return Self.tvOSApplicationExtension - default: return nil + case .iOS: \DeploymentTargetConfiguration.iOSAppExtensionDeploymentTarget + case .macOS: \DeploymentTargetConfiguration.macOSAppExtensionDeploymentTarget + case .watchOS: \DeploymentTargetConfiguration.watchOSAppExtensionDeploymentTarget + case .tvOS: \DeploymentTargetConfiguration.tvOSAppExtensionDeploymentTarget + default: nil } } } - class Version: Equatable, Comparable { + struct Version: Equatable, Comparable { let platform: Platform var major: Int var minor: Int @@ -47,22 +47,19 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { self.patch = patch } - convenience init(platform: Platform, rawValue: String) throws { - let (major, minor, patch) = try Self.parseVersion(string: rawValue) + init(platform: Platform, value: Any) throws { + let (major, minor, patch) = try Self.parseVersion(string: String(describing: value)) self.init(platform: platform, major: major, minor: minor, patch: patch) } - fileprivate func update(using value: Any) throws { - let (major, minor, patch) = try Self.parseVersion(string: String(describing: value)) - self.major = major - self.minor = minor - self.patch = patch + var configurationKey: String { + platform.configurationKey } private static func parseVersion(string: String) throws -> (Int, Int, Int) { func parseNumber(_ string: String) throws -> Int { guard let number = Int(string) else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } return number } @@ -70,7 +67,7 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { let parts = string.components(separatedBy: ".") switch parts.count { case 0: - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) case 1: return (try parseNumber(parts[0]), 0, 0) case 2: @@ -80,11 +77,11 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { } } - static func == (lhs: Version, rhs: Version) -> Bool { + static func == (lhs: Self, rhs: Self) -> Bool { lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch } - static func < (lhs: Version, rhs: Version) -> Bool { + static func < (lhs: Self, rhs: Self) -> Bool { if lhs.major != rhs.major { return lhs.major < rhs.major } @@ -107,17 +104,8 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { private(set) var severityConfiguration = SeverityConfiguration(.warning) - private let targets: [String: Version] - var parameterDescription: RuleConfigurationDescription? { - severityConfiguration - for (platform, target) in targets.sorted(by: { $0.key < $1.key }) { - platform => .symbol(target.stringValue) - } - } - - init() { - self.targets = Dictionary(uniqueKeysWithValues: [ + let targets = Dictionary(uniqueKeysWithValues: [ iOSDeploymentTarget, iOSAppExtensionDeploymentTarget, macOSDeploymentTarget, @@ -125,28 +113,55 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { watchOSDeploymentTarget, watchOSAppExtensionDeploymentTarget, tvOSDeploymentTarget, - tvOSAppExtensionDeploymentTarget + tvOSAppExtensionDeploymentTarget, ].map { ($0.platform.configurationKey, $0) }) + severityConfiguration + for (platform, target) in targets.sorted(by: { $0.key < $1.key }) { + platform => .symbol(target.stringValue) + } } + // swiftlint:disable:next cyclomatic_complexity mutating func apply(configuration: Any) throws { guard let configuration = configuration as? [String: Any] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } for (key, value) in configuration { if key == "severity", let value = value as? String { try severityConfiguration.apply(configuration: value) continue } - guard let target = targets[key] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) - } - try target.update(using: value) - if let extensionConfigurationKey = target.platform.appExtensionCounterpart?.configurationKey, - configuration[extensionConfigurationKey] == nil, - let child = targets[extensionConfigurationKey] { - try child.update(using: value) + switch key { + case iOSDeploymentTarget.platform.configurationKey: + try apply(value: value, to: \.iOSDeploymentTarget, from: configuration) + case iOSAppExtensionDeploymentTarget.platform.configurationKey: + iOSAppExtensionDeploymentTarget = try Version(platform: .iOSApplicationExtension, value: value) + case macOSDeploymentTarget.platform.configurationKey: + try apply(value: value, to: \.macOSDeploymentTarget, from: configuration) + case macOSAppExtensionDeploymentTarget.platform.configurationKey: + macOSAppExtensionDeploymentTarget = try Version(platform: .macOSApplicationExtension, value: value) + case watchOSDeploymentTarget.platform.configurationKey: + try apply(value: value, to: \.watchOSDeploymentTarget, from: configuration) + case watchOSAppExtensionDeploymentTarget.platform.configurationKey: + watchOSAppExtensionDeploymentTarget = try Version(platform: .watchOSApplicationExtension, value: value) + case tvOSDeploymentTarget.platform.configurationKey: + try apply(value: value, to: \.tvOSDeploymentTarget, from: configuration) + case tvOSAppExtensionDeploymentTarget.platform.configurationKey: + tvOSAppExtensionDeploymentTarget = try Version(platform: .tvOSApplicationExtension, value: value) + default: throw Issue.invalidConfiguration(ruleID: Parent.identifier) } } } + + private mutating func apply(value: Any, + to target: WritableKeyPath, + from configuration: [String: Any]) throws { + let platform = self[keyPath: target].platform + self[keyPath: target] = try Version(platform: platform, value: value) + if let counterpart = platform.appExtensionCounterpart, + case let counterPlatform = self[keyPath: counterpart].platform, + configuration[counterPlatform.configurationKey] == nil { + self[keyPath: counterpart] = try Version(platform: counterPlatform, value: value) + } + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DiscouragedDirectInitConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DiscouragedDirectInitConfiguration.swift index 22641758c6..72753bf382 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DiscouragedDirectInitConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DiscouragedDirectInitConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct DiscouragedDirectInitConfiguration: SeverityBasedRuleConfiguration { typealias Parent = DiscouragedDirectInitRule @@ -14,6 +14,6 @@ struct DiscouragedDirectInitConfiguration: SeverityBasedRuleConfiguration { private(set) var discouragedInits: Set = [ "Bundle", "NSError", - "UIDevice" + "UIDevice", ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/EmptyCountConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/EmptyCountConfiguration.swift index c0302c9cee..8216adce8e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/EmptyCountConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/EmptyCountConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct EmptyCountConfiguration: SeverityBasedRuleConfiguration { typealias Parent = EmptyCountRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExpiringTodoConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExpiringTodoConfiguration.swift index 4667ff09ee..29f1fb7da0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExpiringTodoConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExpiringTodoConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ExpiringTodoConfiguration: RuleConfiguration { typealias Parent = ExpiringTodoRule typealias Severity = SeverityConfiguration diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitInitConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitInitConfiguration.swift index 12797b725e..6fd03935db 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitInitConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitInitConfiguration.swift @@ -1,4 +1,4 @@ -@AutoApply +@AutoConfigParser struct ExplicitInitConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ExplicitInitRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift index b57dd54f74..89d4b8a2ed 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ExplicitTypeInterfaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ExplicitTypeInterfaceRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum VariableKind: String, CaseIterable { case instance case local diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift index 48706f9722..2090c23ba6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift @@ -28,7 +28,7 @@ struct FileHeaderConfiguration: SeverityBasedRuleConfiguration { mutating func apply(configuration: Any) throws { guard let configuration = configuration as? [String: String] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } // Cache the created regexes if possible. @@ -65,8 +65,10 @@ struct FileHeaderConfiguration: SeverityBasedRuleConfiguration { } } - private func makeRegex(for file: SwiftLintFile, using pattern: String, - options: NSRegularExpression.Options, escapeFileName: Bool) -> NSRegularExpression? { + private func makeRegex(for file: SwiftLintFile, + using pattern: String, + options: NSRegularExpression.Options, + escapeFileName: Bool) -> NSRegularExpression? { // Recompile the regex for this file... let replacedPattern = file.path.map { path in let fileName = path.bridge().lastPathComponent @@ -85,13 +87,11 @@ struct FileHeaderConfiguration: SeverityBasedRuleConfiguration { } private func regexFromString(for file: SwiftLintFile, using pattern: String) -> NSRegularExpression? { - return makeRegex(for: file, using: pattern, options: Self.stringRegexOptions, - escapeFileName: false) + makeRegex(for: file, using: pattern, options: Self.stringRegexOptions, escapeFileName: false) } private func regexFromPattern(for file: SwiftLintFile, using pattern: String) -> NSRegularExpression? { - return makeRegex(for: file, using: pattern, options: Self.patternRegexOptions, - escapeFileName: true) + makeRegex(for: file, using: pattern, options: Self.patternRegexOptions, escapeFileName: true) } func forbiddenRegex(for file: SwiftLintFile) -> NSRegularExpression? { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileLengthConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileLengthConfiguration.swift index f32ea9cd78..e5a1675079 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileLengthConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileLengthConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct FileLengthConfiguration: RuleConfiguration { typealias Parent = FileLengthRule - @ConfigurationElement + @ConfigurationElement(inline: true) private(set) var severityConfiguration = SeverityLevelsConfiguration(warning: 400, error: 1000) @ConfigurationElement(key: "ignore_comment_only_lines") private(set) var ignoreCommentOnlyLines = false diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift index 9caeff15f9..7aa8be61b5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct FileNameConfiguration: SeverityBasedRuleConfiguration { typealias Parent = FileNameRule @@ -14,4 +14,6 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration { private(set) var suffixPattern = "\\+.*" @ConfigurationElement(key: "nested_type_separator") private(set) var nestedTypeSeparator = "." + @ConfigurationElement(key: "require_fully_qualified_names") + private(set) var requireFullyQualifiedNames = false } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameNoSpaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameNoSpaceConfiguration.swift index 8482977ece..e9f8128390 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameNoSpaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameNoSpaceConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct FileNameNoSpaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = FileNameNoSpaceRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift index f9d6c34323..4ec1bc066b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct FileTypesOrderConfiguration: SeverityBasedRuleConfiguration { typealias Parent = FileTypesOrderRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum FileType: String { case supportingType = "supporting_type" case mainType = "main_type" @@ -21,6 +21,6 @@ struct FileTypesOrderConfiguration: SeverityBasedRuleConfiguration { [.mainType], [.extension], [.previewProvider], - [.libraryContentProvider] + [.libraryContentProvider], ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ForWhereConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ForWhereConfiguration.swift index 56f13072e4..3a29712110 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ForWhereConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ForWhereConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ForWhereConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ForWhereRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionDefaultParameterAtEndConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionDefaultParameterAtEndConfiguration.swift new file mode 100644 index 0000000000..71d4d33bbd --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionDefaultParameterAtEndConfiguration.swift @@ -0,0 +1,13 @@ +import SwiftLintCore + +@AutoConfigParser +struct FunctionDefaultParameterAtEndConfiguration: SeverityBasedRuleConfiguration { + // swiftlint:disable:previous type_name + + typealias Parent = FunctionDefaultParameterAtEndRule + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "ignore_first_isolation_inheritance_parameter") + private(set) var ignoreFirstIsolationInheritanceParameter = true +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionParameterCountConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionParameterCountConfiguration.swift index aedea8a3ad..f72993a7a1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionParameterCountConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FunctionParameterCountConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct FunctionParameterCountConfiguration: RuleConfiguration { typealias Parent = FunctionParameterCountRule - @ConfigurationElement + @ConfigurationElement(inline: true) private(set) var severityConfiguration = SeverityLevelsConfiguration(warning: 5, error: 8) @ConfigurationElement(key: "ignores_default_parameters") private(set) var ignoresDefaultParameters = true diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IdentifierNameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IdentifierNameConfiguration.swift index f5550b8085..7cdb33754a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IdentifierNameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IdentifierNameConfiguration.swift @@ -1,12 +1,12 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct IdentifierNameConfiguration: RuleConfiguration { typealias Parent = IdentifierNameRule private static let defaultOperators = ["/", "=", "-", "+", "!", "*", "|", "^", "~", "?", ".", "%", "<", ">", "&"] - @ConfigurationElement + @ConfigurationElement(inline: true) private(set) var nameConfiguration = NameConfiguration(minLengthWarning: 3, minLengthError: 2, maxLengthWarning: 40, diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift index 8c1c2af319..d4bbe24d71 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ImplicitReturnConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ImplicitReturnRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum ReturnKind: String, CaseIterable, Comparable { case closure case function @@ -29,6 +29,6 @@ struct ImplicitReturnConfiguration: SeverityBasedRuleConfiguration { } func isKindIncluded(_ kind: ReturnKind) -> Bool { - return self.includedKinds.contains(kind) + includedKinds.contains(kind) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift index 1f4ec57f41..d60c7a8cff 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ImplicitlyUnwrappedOptionalConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ImplicitlyUnwrappedOptionalRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum ImplicitlyUnwrappedOptionalModeConfiguration: String { // swiftlint:disable:this type_name case all = "all" case allExceptIBOutlets = "all_except_iboutlets" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/InclusiveLanguageConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/InclusiveLanguageConfiguration.swift index 03d2e472bb..8139a3cd38 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/InclusiveLanguageConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/InclusiveLanguageConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct InclusiveLanguageConfiguration: SeverityBasedRuleConfiguration { typealias Parent = InclusiveLanguageRule @@ -8,7 +8,7 @@ struct InclusiveLanguageConfiguration: SeverityBasedRuleConfiguration { "whitelist", "blacklist", "master", - "slave" + "slave", ] private static let defaultAllowedTerms: Set = [ diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IndentationWidthConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IndentationWidthConfiguration.swift index 8f78f761b4..8e79864f5b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IndentationWidthConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/IndentationWidthConfiguration.swift @@ -1,14 +1,21 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct IndentationWidthConfiguration: SeverityBasedRuleConfiguration { typealias Parent = IndentationWidthRule + private static let defaultIndentationWidth = 4 + @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration.warning @ConfigurationElement( key: "indentation_width", - postprocessor: { if $0 < 1 { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } } + postprocessor: { + if $0 < 1 { + Issue.invalidConfiguration(ruleID: Parent.identifier).print() + $0 = Self.defaultIndentationWidth + } + } ) private(set) var indentationWidth = 4 @ConfigurationElement(key: "include_comments") diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/LineLengthConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/LineLengthConfiguration.swift index e8354a08d8..0f83c6ee24 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/LineLengthConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/LineLengthConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct LineLengthConfiguration: RuleConfiguration { typealias Parent = LineLengthRule - @ConfigurationElement + @ConfigurationElement(inline: true) private(set) var length = SeverityLevelsConfiguration(warning: 120, error: 200) @ConfigurationElement(key: "ignores_urls") private(set) var ignoresURLs = false @@ -18,6 +18,6 @@ struct LineLengthConfiguration: RuleConfiguration { private(set) var excludedLinesPatterns: Set = [] var params: [RuleParameter] { - return length.params + length.params } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MissingDocsConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MissingDocsConfiguration.swift index a497952ce5..f1466c1503 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MissingDocsConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MissingDocsConfiguration.swift @@ -5,7 +5,7 @@ struct MissingDocsConfiguration: RuleConfiguration { private(set) var parameters = [ RuleParameter(severity: .warning, value: .open), - RuleParameter(severity: .warning, value: .public) + RuleParameter(severity: .warning, value: .public), ] @ConfigurationElement(key: "excludes_extensions") @@ -14,6 +14,8 @@ struct MissingDocsConfiguration: RuleConfiguration { private(set) var excludesInheritedTypes = true @ConfigurationElement(key: "excludes_trivial_init") private(set) var excludesTrivialInit = false + @ConfigurationElement(key: "evaluate_effective_access_control_level") + private(set) var evaluateEffectiveAccessControlLevel = false var parameterDescription: RuleConfigurationDescription? { let parametersDescription = parameters.group { $0.severity } @@ -26,11 +28,12 @@ struct MissingDocsConfiguration: RuleConfiguration { $excludesExtensions.key => .flag(excludesExtensions) $excludesInheritedTypes.key => .flag(excludesInheritedTypes) $excludesTrivialInit.key => .flag(excludesTrivialInit) + $evaluateEffectiveAccessControlLevel.key => .flag(evaluateEffectiveAccessControlLevel) } mutating func apply(configuration: Any) throws { guard let dict = configuration as? [String: Any] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } if let shouldExcludeExtensions = dict[$excludesExtensions.key] as? Bool { @@ -45,6 +48,10 @@ struct MissingDocsConfiguration: RuleConfiguration { self.excludesTrivialInit = excludesTrivialInit } + if let evaluateEffectiveAccessControlLevel = dict[$evaluateEffectiveAccessControlLevel.key] as? Bool { + self.evaluateEffectiveAccessControlLevel = evaluateEffectiveAccessControlLevel + } + if let parameters = try parameters(from: dict) { self.parameters = parameters } @@ -62,7 +69,7 @@ struct MissingDocsConfiguration: RuleConfiguration { let rules: [RuleParameter] = try array .map { val -> RuleParameter in guard let acl = AccessControlLevel(description: val) else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } return RuleParameter(severity: severity, value: acl) } @@ -75,8 +82,8 @@ struct MissingDocsConfiguration: RuleConfiguration { } } - guard parameters.count == parameters.map({ $0.value }).unique.count else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + guard parameters.count == parameters.map(\.value).unique.count else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } return parameters.isNotEmpty ? parameters : nil diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ModifierOrderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ModifierOrderConfiguration.swift index 05ef0cba4e..d2ce6210c9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ModifierOrderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ModifierOrderConfiguration.swift @@ -1,7 +1,7 @@ import SourceKittenFramework import SwiftLintCore -@AutoApply +@AutoConfigParser struct ModifierOrderConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ModifierOrderRule @@ -19,7 +19,7 @@ struct ModifierOrderConfiguration: SeverityBasedRuleConfiguration { .required, .convenience, .typeMethods, - .owned + .owned, ] } @@ -28,7 +28,7 @@ extension SwiftDeclarationAttributeKind.ModifierGroup: AcceptableByConfiguration if let value = value as? String, let newSelf = Self(rawValue: value), newSelf != .atPrefixed { self = newSelf } else { - throw Issue.unknownConfiguration(ruleID: ruleID) + throw Issue.invalidConfiguration(ruleID: ruleID) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineArgumentsConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineArgumentsConfiguration.swift index 674e5fb039..e0939e6daf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineArgumentsConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineArgumentsConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct MultilineArgumentsConfiguration: SeverityBasedRuleConfiguration { typealias Parent = MultilineArgumentsRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum FirstArgumentLocation: String { case anyLine = "any_line" case sameLine = "same_line" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineParametersConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineParametersConfiguration.swift index 963afd7b6e..22c3cd470c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineParametersConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/MultilineParametersConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct MultilineParametersConfiguration: SeverityBasedRuleConfiguration { typealias Parent = MultilineParametersRule @@ -8,4 +8,29 @@ struct MultilineParametersConfiguration: SeverityBasedRuleConfiguration { private(set) var severityConfiguration = SeverityConfiguration(.warning) @ConfigurationElement(key: "allows_single_line") private(set) var allowsSingleLine = true + @ConfigurationElement(key: "max_number_of_single_line_parameters") + private(set) var maxNumberOfSingleLineParameters: Int? + + func validate() throws { + guard let maxNumberOfSingleLineParameters else { + return + } + guard maxNumberOfSingleLineParameters >= 1 else { + Issue.inconsistentConfiguration( + ruleID: Parent.identifier, + message: "Option '\($maxNumberOfSingleLineParameters.key)' should be >= 1." + ).print() + return + } + + if maxNumberOfSingleLineParameters > 1, !allowsSingleLine { + Issue.inconsistentConfiguration( + ruleID: Parent.identifier, + message: """ + Option '\($maxNumberOfSingleLineParameters.key)' has no effect when \ + '\($allowsSingleLine.key)' is false. + """ + ).print() + } + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NameConfiguration.swift index dd952e3992..6c9ee3e4b4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NameConfiguration.swift @@ -1,7 +1,7 @@ import Foundation import SwiftLintCore -struct NameConfiguration: RuleConfiguration { +struct NameConfiguration: RuleConfiguration, InlinableOptionType { typealias Severity = SeverityConfiguration typealias SeverityLevels = SeverityLevelsConfiguration typealias StartWithLowercaseConfiguration = ChildOptionSeverityConfiguration @@ -20,11 +20,11 @@ struct NameConfiguration: RuleConfiguration { private(set) var validatesStartWithLowercase = StartWithLowercaseConfiguration.error var minLengthThreshold: Int { - return max(minLength.warning, minLength.error ?? minLength.warning) + max(minLength.warning, minLength.error ?? minLength.warning) } var maxLengthThreshold: Int { - return min(maxLength.warning, maxLength.error ?? maxLength.warning) + min(maxLength.warning, maxLength.error ?? maxLength.warning) } var allowedSymbolsAndAlphanumerics: CharacterSet { @@ -51,7 +51,7 @@ struct NameConfiguration: RuleConfiguration { mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } if let minLengthConfiguration = configurationDict[$minLength.key] { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift index 9fe2c155cd..750b1793ef 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct NestingConfiguration: RuleConfiguration { typealias Parent = NestingRule typealias Severity = SeverityLevelsConfiguration diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NoEmptyBlockConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NoEmptyBlockConfiguration.swift new file mode 100644 index 0000000000..a367a45575 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NoEmptyBlockConfiguration.swift @@ -0,0 +1,26 @@ +import SwiftLintCore + +@AutoConfigParser +struct NoEmptyBlockConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = NoEmptyBlockRule + + @AcceptableByConfigurationElement + enum CodeBlockType: String, CaseIterable { + case functionBodies = "function_bodies" + case initializerBodies = "initializer_bodies" + case statementBlocks = "statement_blocks" + case closureBlocks = "closure_blocks" + + static let all = Set(allCases) + } + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + + @ConfigurationElement(key: "disabled_block_types") + private(set) var disabledBlockTypes: [CodeBlockType] = [] + + var enabledBlockTypes: Set { + CodeBlockType.all.subtracting(disabledBlockTypes) + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOverridableClassDeclarationConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOverridableClassDeclarationConfiguration.swift index 5022944724..c302c8dfa1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOverridableClassDeclarationConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOverridableClassDeclarationConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply // swiftlint:disable:next type_name +@AutoConfigParser // swiftlint:disable:next type_name struct NonOverridableClassDeclarationConfiguration: SeverityBasedRuleConfiguration { typealias Parent = NonOverridableClassDeclarationRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum FinalClassModifier: String { case finalClass = "final class" case `static` = "static" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NumberSeparatorConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NumberSeparatorConfiguration.swift index 6268222e0a..aad4aad6f9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NumberSeparatorConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NumberSeparatorConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct NumberSeparatorConfiguration: SeverityBasedRuleConfiguration { typealias Parent = NumberSeparatorRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ObjectLiteralConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ObjectLiteralConfiguration.swift index fd691da869..f18c01ab8c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ObjectLiteralConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ObjectLiteralConfiguration.swift @@ -2,7 +2,7 @@ import SwiftLintCore typealias DiscouragedObjectLiteralConfiguration = ObjectLiteralConfiguration -@AutoApply +@AutoConfigParser struct ObjectLiteralConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OpeningBraceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OpeningBraceConfiguration.swift index 85f40e1da0..a136f1d300 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OpeningBraceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OpeningBraceConfiguration.swift @@ -1,11 +1,23 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct OpeningBraceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = OpeningBraceRule @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) - @ConfigurationElement(key: "allow_multiline_func") + @ConfigurationElement(key: "ignore_multiline_type_headers") + private(set) var ignoreMultilineTypeHeaders = false + @ConfigurationElement(key: "ignore_multiline_statement_conditions") + private(set) var ignoreMultilineStatementConditions = false + @ConfigurationElement(key: "ignore_multiline_function_signatures") + private(set) var ignoreMultilineFunctionSignatures = false + // TODO: [08/23/2026] Remove deprecation warning after ~2 years. + @ConfigurationElement(key: "allow_multiline_func", deprecationNotice: .suggestAlternative( + ruleID: Parent.identifier, name: "ignore_multiline_function_signatures")) private(set) var allowMultilineFunc = false + + var shouldIgnoreMultilineFunctionSignatures: Bool { + ignoreMultilineFunctionSignatures || allowMultilineFunc + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OperatorUsageWhitespaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OperatorUsageWhitespaceConfiguration.swift index 09c8bcd489..1bd83318f6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OperatorUsageWhitespaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OperatorUsageWhitespaceConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct OperatorUsageWhitespaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = OperatorUsageWhitespaceRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift index ca2a9aeb32..1c4a3f12bc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct OverriddenSuperCallConfiguration: SeverityBasedRuleConfiguration { typealias Parent = OverriddenSuperCallRule @@ -33,7 +33,7 @@ struct OverriddenSuperCallConfiguration: SeverityBasedRuleConfiguration { "viewWillAppear(_:)", "viewWillDisappear(_:)", // XCTestCase - "invokeTest()" + "invokeTest()", ] @ConfigurationElement(key: "severity") diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PreferKeyPathConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PreferKeyPathConfiguration.swift new file mode 100644 index 0000000000..1df784c41e --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PreferKeyPathConfiguration.swift @@ -0,0 +1,11 @@ +import SwiftLintCore + +@AutoConfigParser +struct PreferKeyPathConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = PreferKeyPathRule + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "restrict_to_standard_functions") + private(set) var restrictToStandardFunctions = true +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrefixedTopLevelConstantConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrefixedTopLevelConstantConfiguration.swift index f6057ceb93..591b8cc13a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrefixedTopLevelConstantConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrefixedTopLevelConstantConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct PrefixedTopLevelConstantConfiguration: SeverityBasedRuleConfiguration { typealias Parent = PrefixedTopLevelConstantRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOutletConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOutletConfiguration.swift index e31075e071..ae3fdf5702 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOutletConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOutletConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct PrivateOutletConfiguration: SeverityBasedRuleConfiguration { typealias Parent = PrivateOutletRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOverFilePrivateConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOverFilePrivateConfiguration.swift index ed9766230f..69fc9eb042 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOverFilePrivateConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateOverFilePrivateConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct PrivateOverFilePrivateConfiguration: SeverityBasedRuleConfiguration { typealias Parent = PrivateOverFilePrivateRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift index 9a67e2bd6c..65b14cdcfe 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift @@ -14,7 +14,7 @@ struct PrivateUnitTestConfiguration: SeverityBasedRuleConfiguration { mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } if let extraTestParentClasses = configurationDict[$testParentClasses.key] as? [String] { self.testParentClasses.formUnion(extraTestParentClasses) diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift index 914e3a5dfc..cd06da7cf1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ProhibitedSuperConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ProhibitedSuperRule @@ -19,7 +19,7 @@ struct ProhibitedSuperConfiguration: SeverityBasedRuleConfiguration { // NSView "updateLayer()", // UIViewController - "loadView()" + "loadView()", ] var resolvedMethodNames: [String] { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantTypeAnnotationConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantTypeAnnotationConfiguration.swift new file mode 100644 index 0000000000..7bb54dacfa --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantTypeAnnotationConfiguration.swift @@ -0,0 +1,15 @@ +import SwiftLintCore + +@AutoConfigParser +struct RedundantTypeAnnotationConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = RedundantTypeAnnotationRule + + @ConfigurationElement(key: "severity") + var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "ignore_attributes") + var ignoreAttributes = Set(["IBInspectable"]) + @ConfigurationElement(key: "ignore_properties") + private(set) var ignoreProperties = false + @ConfigurationElement(key: "consider_default_literal_types_redundant") + private(set) var considerDefaultLiteralTypesRedundant = false +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantVoidReturnConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantVoidReturnConfiguration.swift index c81e36723a..14dbe5aba1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantVoidReturnConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantVoidReturnConfiguration.swift @@ -1,4 +1,4 @@ -@AutoApply +@AutoConfigParser struct RedundantVoidReturnConfiguration: SeverityBasedRuleConfiguration { typealias Parent = RedundantVoidReturnRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RequiredEnumCaseConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RequiredEnumCaseConfiguration.swift index 7a3184a68a..2402ed84d3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RequiredEnumCaseConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RequiredEnumCaseConfiguration.swift @@ -32,7 +32,7 @@ struct RequiredEnumCaseConfiguration: RuleConfiguration { mutating func apply(configuration: Any) throws { guard let config = configuration as? [String: [String: String]] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } register(protocols: config) diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SelfBindingConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SelfBindingConfiguration.swift index 005a838f3c..9557225db9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SelfBindingConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SelfBindingConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct SelfBindingConfiguration: SeverityBasedRuleConfiguration { typealias Parent = SelfBindingRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ShorthandArgumentConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ShorthandArgumentConfiguration.swift index e5313e64d9..07907bb75a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ShorthandArgumentConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ShorthandArgumentConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct ShorthandArgumentConfiguration: SeverityBasedRuleConfiguration { typealias Parent = ShorthandArgumentRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SortedImportsConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SortedImportsConfiguration.swift index 8aaab0abc8..6d08c57d33 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SortedImportsConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SortedImportsConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct SortedImportsConfiguration: SeverityBasedRuleConfiguration { typealias Parent = SortedImportsRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum SortedImportsGroupingConfiguration: String { /// Sorts import lines based on any import attributes (e.g. `@testable`, `@_exported`, etc.), followed by a case /// insensitive comparison of the imported module name. diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/StatementPositionConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/StatementPositionConfiguration.swift index a0361e64d7..1844fdfa8c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/StatementPositionConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/StatementPositionConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct StatementPositionConfiguration: SeverityBasedRuleConfiguration { typealias Parent = StatementPositionRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum StatementModeConfiguration: String { case `default` = "default" case uncuddledElse = "uncuddled_else" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SwitchCaseAlignmentConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SwitchCaseAlignmentConfiguration.swift index 7f0a94920e..1828a7508e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SwitchCaseAlignmentConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/SwitchCaseAlignmentConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct SwitchCaseAlignmentConfiguration: SeverityBasedRuleConfiguration { typealias Parent = SwitchCaseAlignmentRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift index 05296ceb18..51fbb0fb7d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct TestCaseAccessibilityConfiguration: SeverityBasedRuleConfiguration { typealias Parent = TestCaseAccessibilityRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TodoConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TodoConfiguration.swift index 437c57a2cb..7fd34fda29 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TodoConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TodoConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct TodoConfiguration: SeverityBasedRuleConfiguration { typealias Parent = TodoRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum TodoKeyword: String, CaseIterable { case todo = "TODO" case fixme = "FIXME" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingClosureConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingClosureConfiguration.swift index e4b4862921..58f88681fd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingClosureConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingClosureConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct TrailingClosureConfiguration: SeverityBasedRuleConfiguration { typealias Parent = TrailingClosureRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingCommaConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingCommaConfiguration.swift index f386f1967d..7a07760192 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingCommaConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingCommaConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct TrailingCommaConfiguration: SeverityBasedRuleConfiguration { typealias Parent = TrailingCommaRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingWhitespaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingWhitespaceConfiguration.swift index d8577e64ae..0eb7577b40 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingWhitespaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TrailingWhitespaceConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct TrailingWhitespaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = TrailingWhitespaceRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeContentsOrderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeContentsOrderConfiguration.swift index 811622d0f7..01f1427f6e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeContentsOrderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeContentsOrderConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@MakeAcceptableByConfigurationElement +@AcceptableByConfigurationElement enum TypeContent: String { case `case` = "case" case typeAlias = "type_alias" @@ -19,7 +19,7 @@ enum TypeContent: String { case deinitializer = "deinitializer" } -@AutoApply +@AutoConfigParser struct TypeContentsOrderConfiguration: SeverityBasedRuleConfiguration { typealias Parent = TypeContentsOrderRule @@ -40,6 +40,6 @@ struct TypeContentsOrderConfiguration: SeverityBasedRuleConfiguration { [.ibAction], [.otherMethod], [.subscript], - [.deinitializer] + [.deinitializer], ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeNameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeNameConfiguration.swift index 8d56cbf5e5..d9d51cb664 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeNameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeNameConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct TypeNameConfiguration: RuleConfiguration { typealias Parent = TypeNameRule - @ConfigurationElement + @ConfigurationElement(inline: true) private(set) var nameConfiguration = NameConfiguration(minLengthWarning: 3, minLengthError: 0, maxLengthWarning: 40, diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnitTestConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnitTestConfiguration.swift index 43be8e9889..96f5d0de6d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnitTestConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnitTestConfiguration.swift @@ -6,7 +6,7 @@ typealias FinalTestCaseConfiguration = UnitTestConfiguration typealias NoMagicNumbersConfiguration = UnitTestConfiguration typealias SingleTestClassConfiguration = UnitTestConfiguration -@AutoApply +@AutoConfigParser struct UnitTestConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnneededOverrideRuleConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnneededOverrideRuleConfiguration.swift index 14d4968152..cdbf8441fa 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnneededOverrideRuleConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnneededOverrideRuleConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct UnneededOverrideRuleConfiguration: SeverityBasedRuleConfiguration { typealias Parent = UnneededOverrideRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedDeclarationConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedDeclarationConfiguration.swift index 58877e82a5..e3deec3ac3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedDeclarationConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedDeclarationConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct UnusedDeclarationConfiguration: SeverityBasedRuleConfiguration { typealias Parent = UnusedDeclarationRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift index 57aa628523..aed18138ff 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift @@ -8,13 +8,13 @@ struct TransitiveModuleConfiguration: Equatable, AcceptableByConfi /// The set of modules that can be transitively imported by `importedModule`. let transitivelyImportedModules: [String] - init(fromAny configuration: Any, context ruleID: String) throws { + init(fromAny configuration: Any, context _: String) throws { guard let configurationDict = configuration as? [String: Any], Set(configurationDict.keys) == ["module", "allowed_transitive_imports"], let importedModule = configurationDict["module"] as? String, let transitivelyImportedModules = configurationDict["allowed_transitive_imports"] as? [String] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } self.importedModule = importedModule self.transitivelyImportedModules = transitivelyImportedModules @@ -27,7 +27,7 @@ struct TransitiveModuleConfiguration: Equatable, AcceptableByConfi } } -@AutoApply +@AutoConfigParser struct UnusedImportConfiguration: SeverityBasedRuleConfiguration { typealias Parent = UnusedImportRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedOptionalBindingConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedOptionalBindingConfiguration.swift index fa094ad346..083d7d27a7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedOptionalBindingConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedOptionalBindingConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct UnusedOptionalBindingConfiguration: SeverityBasedRuleConfiguration { typealias Parent = UnusedOptionalBindingRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceClosingBracesConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceClosingBracesConfiguration.swift index 700b06e04b..cb62a95da0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceClosingBracesConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceClosingBracesConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply // swiftlint:disable:next type_name +@AutoConfigParser // swiftlint:disable:next type_name struct VerticalWhitespaceClosingBracesConfiguration: SeverityBasedRuleConfiguration { typealias Parent = VerticalWhitespaceClosingBracesRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift index 2249594ee8..2bc989d73f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift @@ -1,6 +1,6 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct VerticalWhitespaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = VerticalWhitespaceRule diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/XCTSpecificMatcherConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/XCTSpecificMatcherConfiguration.swift index 249c35ef4e..298fb42c2f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/XCTSpecificMatcherConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/XCTSpecificMatcherConfiguration.swift @@ -1,10 +1,10 @@ import SwiftLintCore -@AutoApply +@AutoConfigParser struct XCTSpecificMatcherConfiguration: SeverityBasedRuleConfiguration { typealias Parent = XCTSpecificMatcherRule - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum Matcher: String, CaseIterable { case oneArgumentAsserts = "one-argument-asserts" case twoArgumentAsserts = "two-argument-asserts" diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/AttributeNameSpacingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/AttributeNameSpacingRule.swift new file mode 100644 index 0000000000..50ca9d4b92 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Style/AttributeNameSpacingRule.swift @@ -0,0 +1,158 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule +struct AttributeNameSpacingRule: SwiftSyntaxCorrectableRule { + var configuration = SeverityConfiguration(.error) + + static let description = RuleDescription( + identifier: "attribute_name_spacing", + name: "Attribute Name Spacing", + description: """ + This rule prevents trailing spaces after attribute names, ensuring compatibility \ + with Swift 6 where a space between an attribute name and the opening parenthesis \ + results in a compilation error (e.g. `@MyPropertyWrapper ()`, `private (set)`). + """, + kind: .style, + nonTriggeringExamples: [ + Example("private(set) var foo: Bool = false"), + Example("fileprivate(set) var foo: Bool = false"), + Example("@MainActor class Foo {}"), + Example("func funcWithEscapingClosure(_ x: @escaping () -> Int) {}"), + Example("@available(*, deprecated)"), + Example("@MyPropertyWrapper(param: 2) "), + Example("nonisolated(unsafe) var _value: X?"), + Example("@testable import SwiftLintCore"), + Example("func func_type_attribute_with_space(x: @convention(c) () -> Int) {}"), + Example(""" + @propertyWrapper + struct MyPropertyWrapper { + var wrappedValue: Int = 1 + + init(param: Int) {} + } + """), + Example(""" + let closure2 = { @MainActor + (a: Int, b: Int) in + } + """), + ], + triggeringExamples: [ + Example("private ↓(set) var foo: Bool = false"), + Example("fileprivate ↓(set) var foo: Bool = false"), + Example("public ↓(set) var foo: Bool = false"), + Example(" public ↓(set) var foo: Bool = false"), + Example("@ ↓MainActor class Foo {}"), + Example("func funcWithEscapingClosure(_ x: @ ↓escaping () -> Int) {}"), + Example("func funcWithEscapingClosure(_ x: @escaping↓() -> Int) {}"), + Example("@available ↓(*, deprecated)"), + Example("@MyPropertyWrapper ↓(param: 2) "), + Example("nonisolated ↓(unsafe) var _value: X?"), + Example("@MyProperty ↓() class Foo {}"), + Example(""" + let closure1 = { @MainActor ↓(a, b) in + } + """), + ], + corrections: [ + Example("private↓ (set) var foo: Bool = false"): Example("private(set) var foo: Bool = false"), + Example("fileprivate↓ (set) var foo: Bool = false"): Example("fileprivate(set) var foo: Bool = false"), + Example("internal↓ (set) var foo: Bool = false"): Example("internal(set) var foo: Bool = false"), + Example("public↓ (set) var foo: Bool = false"): Example("public(set) var foo: Bool = false"), + Example("public↓ (set) var foo: Bool = false"): Example("public(set) var foo: Bool = false"), + Example("@↓ MainActor"): Example("@MainActor"), + Example("func test(_ x: @↓ escaping () -> Int) {}"): Example("func test(_ x: @escaping () -> Int) {}"), + Example("func test(_ x: @escaping↓() -> Int) {}"): Example("func test(_ x: @escaping () -> Int) {}"), + Example("@available↓ (*, deprecated)"): Example("@available(*, deprecated)"), + Example("@MyPropertyWrapper↓ (param: 2) "): Example("@MyPropertyWrapper(param: 2) "), + Example("nonisolated↓ (unsafe) var _value: X?"): Example("nonisolated(unsafe) var _value: X?"), + Example("@MyProperty↓ ()"): Example("@MyProperty()"), + Example(""" + let closure1 = { @MainActor↓ (a, b) in + } + """): Example(""" + let closure1 = { @MainActor(a, b) in + } + """), + ] + ) +} + +private extension AttributeNameSpacingRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: DeclModifierSyntax) { + guard node.detail != nil, node.name.trailingTrivia.isNotEmpty else { + return + } + + addViolation( + startPosition: node.name.endPositionBeforeTrailingTrivia, + endPosition: node.name.endPosition, + replacement: "", + reason: "There must not be any space between access control modifier and scope" + ) + } + + override func visitPost(_ node: AttributeSyntax) { + // Check for trailing trivia after the '@' sign. Handles cases like `@ MainActor` / `@ escaping`. + if node.atSign.trailingTrivia.isNotEmpty { + addViolation( + startPosition: node.atSign.endPositionBeforeTrailingTrivia, + endPosition: node.atSign.endPosition, + replacement: "", + reason: "Attributes must not have trivia between `@` and the identifier" + ) + } + + let hasTrailingTrivia = node.attributeName.trailingTrivia.isNotEmpty + + // Handles cases like `@MyPropertyWrapper (param: 2)`. + if node.arguments != nil, hasTrailingTrivia { + addViolation( + startPosition: node.attributeName.endPositionBeforeTrailingTrivia, + endPosition: node.attributeName.endPosition, + replacement: "", + reason: "Attribute declarations with arguments must not have trailing trivia" + ) + } + + if !hasTrailingTrivia, node.isEscaping { + // Handles cases where escaping has the wrong spacing: `@escaping()` + addViolation( + startPosition: node.attributeName.endPositionBeforeTrailingTrivia, + endPosition: node.attributeName.endPosition, + replacement: " ", + reason: "`@escaping` must have a trailing space before the associated type" + ) + } + } + + private func addViolation( + startPosition: AbsolutePosition, + endPosition: AbsolutePosition, + replacement: String, + reason: String + ) { + let correction = ReasonedRuleViolation.ViolationCorrection( + start: startPosition, + end: endPosition, + replacement: replacement + ) + + let violation = ReasonedRuleViolation( + position: endPosition, + reason: reason, + severity: configuration.severity, + correction: correction + ) + violations.append(violation) + } + } +} + +private extension AttributeSyntax { + var isEscaping: Bool { + attributeNameText == "escaping" + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRuleExamples.swift index e537cebca3..470b75c146 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRuleExamples.swift @@ -95,7 +95,7 @@ internal struct AttributesRuleExamples { @Environment(\.colorScheme) var second: ColorScheme @Persisted(primaryKey: true) var id: Int } - """#, configuration: ["attributes_with_arguments_always_on_line_above": false], excludeFromDocumentation: true) + """#, configuration: ["attributes_with_arguments_always_on_line_above": false], excludeFromDocumentation: true), ] static let triggeringExamples = [ @@ -139,6 +139,6 @@ internal struct AttributesRuleExamples { ) var entities: FetchedResults } - """#, excludeFromDocumentation: true) + """#, excludeFromDocumentation: true), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosingBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosingBraceRule.swift index 2e876912ec..0d60cd9e91 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosingBraceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosingBraceRule.swift @@ -11,11 +11,11 @@ struct ClosingBraceRule: Rule { kind: .style, nonTriggeringExamples: [ Example("[].map({ })"), - Example("[].map(\n { }\n)") + Example("[].map(\n { }\n)"), ], triggeringExamples: [ Example("[].map({ ↓} )"), - Example("[].map({ ↓}\t)") + Example("[].map({ ↓}\t)"), ], corrections: [ Example("[].map({ ↓} )"): Example("[].map({ })") diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift index 018848ef83..4e760bbc0f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift @@ -17,8 +17,8 @@ struct ClosureEndIndentationRule: Rule, OptInRule { fileprivate static let notWhitespace = regex("[^\\s]") func validate(file: SwiftLintFile) -> [StyleViolation] { - return violations(in: file).map { violation in - return styleViolation(for: violation, in: file) + violations(in: file).map { violation in + styleViolation(for: violation, in: file) } } @@ -62,8 +62,7 @@ extension ClosureEndIndentationRule: CorrectableRule { } var corrections = correctedLocations.map { - return Correction(ruleDescription: Self.description, - location: Location(file: file, characterOffset: $0)) + Correction(ruleDescription: Self.description, location: Location(file: file, characterOffset: $0)) } file.write(correctedContents) @@ -114,13 +113,14 @@ extension ClosureEndIndentationRule { } fileprivate func violations(in file: SwiftLintFile) -> [Violation] { - return file.structureDictionary.traverseDepthFirst { subDict in + file.structureDictionary.traverseDepthFirst { subDict in guard let kind = subDict.expressionKind else { return nil } return violations(in: file, of: kind, dictionary: subDict) } } - private func violations(in file: SwiftLintFile, of kind: SwiftExpressionKind, + private func violations(in file: SwiftLintFile, + of kind: SwiftExpressionKind, dictionary: SourceKittenDictionary) -> [Violation] { guard kind == .call else { return [] @@ -204,7 +204,7 @@ extension ClosureEndIndentationRule { } return closureArguments.compactMap { dictionary in - return validateClosureArgument(in: file, dictionary: dictionary) + validateClosureArgument(in: file, dictionary: dictionary) } } @@ -270,7 +270,8 @@ extension ClosureEndIndentationRule { } private func isSingleLineClosure(dictionary: SourceKittenDictionary, - endPosition: ByteCount, file: SwiftLintFile) -> Bool { + endPosition: ByteCount, + file: SwiftLintFile) -> Bool { let contents = file.stringView guard let start = dictionary.bodyOffset, @@ -283,7 +284,8 @@ extension ClosureEndIndentationRule { } private func containsSingleLineClosure(dictionary: SourceKittenDictionary, - endPosition: ByteCount, file: SwiftLintFile) -> Bool { + endPosition: ByteCount, + file: SwiftLintFile) -> Bool { let contents = file.stringView guard let closure = trailingClosure(dictionary: dictionary, file: file), @@ -311,7 +313,7 @@ extension ClosureEndIndentationRule { private func filterClosureArguments(_ arguments: [SourceKittenDictionary], file: SwiftLintFile) -> [SourceKittenDictionary] { - return arguments.filter { argument in + arguments.filter { argument in guard let bodyByteRange = argument.bodyByteRange, let range = file.stringView.byteRangeToNSRange(bodyByteRange), let match = regex("\\s*\\{").firstMatch(in: file.contents, options: [], range: range)?.range, diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift index bf1db05f7a..45c83d6d34 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift @@ -33,7 +33,7 @@ internal struct ClosureEndIndentationRuleExamples { " anotherClosure: { y in\n" + " print(y)\n" + " })"), - Example("(-variable).foo()") + Example("(-variable).foo()"), ] static let triggeringExamples = [ @@ -52,7 +52,7 @@ internal struct ClosureEndIndentationRuleExamples { "↓},\n" + " anotherClosure: { y in\n" + " print(y)\n" + - "↓})") + "↓})"), ] static let corrections = [ @@ -168,6 +168,6 @@ internal struct ClosureEndIndentationRuleExamples { " print(x)\n" + " }, anotherClosure: { y in\n" + " print(y)\n" + - " })") + " })"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureParameterPositionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureParameterPositionRule.swift index 71ad4d59eb..4acb0078b5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureParameterPositionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureParameterPositionRule.swift @@ -29,7 +29,7 @@ struct ClosureParameterPositionRule: Rule { let mediaView: UIView = { [weak self] index in return UIView() }(index) - """) + """), ], triggeringExamples: [ Example(""" @@ -91,7 +91,7 @@ struct ClosureParameterPositionRule: Rule { [weak ↓self] in self?.bar() } - """) + """), ] ) } @@ -119,7 +119,7 @@ private extension ClosureParameterPositionRule { return } let localViolations = positionsToCheck.dropLast().filter { position in - return locationConverter.location(for: position).line != startLine + locationConverter.location(for: position).line != startLine } violations.append(contentsOf: localViolations) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift index 26322c54f7..55da2a8d0f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift @@ -21,7 +21,7 @@ struct ClosureSpacingRule: OptInRule { Example(""" let test1 = func1(arg: { /* do nothing */ }) let test2 = func1 { /* do nothing */ } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example("[].filter↓{ $0.contains(location) }"), @@ -31,7 +31,7 @@ struct ClosureSpacingRule: OptInRule { Example("filter ↓{ sorted ↓{ $0 < $1}}"), Example(""" var tapped: (UITapGestureRecognizer) -> Void = ↓{ _ in /* no-op */ } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], corrections: [ Example("[].filter(↓{$0.contains(location) })"): @@ -41,7 +41,7 @@ struct ClosureSpacingRule: OptInRule { Example("filter ↓{sorted ↓{ $0 < $1}}"): Example("filter { sorted { $0 < $1 } }"), Example("(↓{each in return result.contains(where: ↓{e in return 0})}).count"): - Example("({ each in return result.contains(where: { e in return 0 }) }).count") + Example("({ each in return result.contains(where: { e in return 0 }) }).count"), ] ) } @@ -189,7 +189,7 @@ private extension TokenSyntax { .postfixQuestionMark, .rightParen, .rightSquare, - .semicolon + .semicolon, ] if case .newlines = trailingTrivia.first { return true diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift index 3d34029eb2..916f9bd2c7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift @@ -93,7 +93,7 @@ extension CollectionAlignmentRule { } private var alignColonsTriggeringExamples: [Example] { - return [ + [ Example(""" doThings(arg: [ "foo": 1, @@ -117,12 +117,12 @@ extension CollectionAlignmentRule { "b" ↓:2, "c" : 3 ] - """) + """), ] } private var alignColonsNonTriggeringExamples: [Example] { - return [ + [ Example(""" doThings(arg: [ "foo": 1, @@ -150,12 +150,12 @@ extension CollectionAlignmentRule { Example(""" NSAttributedString(string: "…", attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .regular), .foregroundColor: UIColor(white: 0, alpha: 0.2)]) - """) + """), ] } private var alignLeftTriggeringExamples: [Example] { - return [ + [ Example(""" doThings(arg: [ "foo": 1, @@ -179,12 +179,12 @@ extension CollectionAlignmentRule { "lunch": "sandwich", ↓"dinner": "burger" ] - """) + """), ] } private var alignLeftNonTriggeringExamples: [Example] { - return [ + [ Example(""" doThings(arg: [ "foo": 1, @@ -212,12 +212,12 @@ extension CollectionAlignmentRule { Example(""" NSAttributedString(string: "…", attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .regular), .foregroundColor: UIColor(white: 0, alpha: 0.2)]) - """) + """), ] } private var sharedTriggeringExamples: [Example] { - return [ + [ Example(""" let coordinates = [ CLLocationCoordinate2D(latitude: 0, longitude: 33), @@ -231,12 +231,12 @@ extension CollectionAlignmentRule { ↓4, 6 ] - """) + """), ] } private var sharedNonTriggeringExamples: [Example] { - return [ + [ Example(""" let coordinates = [ CLLocationCoordinate2D(latitude: 0, longitude: 33), @@ -263,7 +263,7 @@ extension CollectionAlignmentRule { let abc = [ "foo": "bar", "fizz": "buzz" ] - """) + """), ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift index 3d382b6e2e..aaf84fe061 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift @@ -84,7 +84,7 @@ struct ColonRule: SubstitutionCorrectableRule, SourceKitFreeRule { } } - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { + func substitution(for violationRange: NSRange, in _: SwiftLintFile) -> (NSRange, String)? { (violationRange, ": ") } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRuleExamples.swift index 73da73825e..a963c54181 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRuleExamples.swift @@ -74,7 +74,7 @@ internal struct ColonRuleExamples { case 0x2029 /* PARAGRAPH SEPARATOR */: return true default: return false } - """) + """), ] static let triggeringExamples = [ @@ -135,7 +135,7 @@ internal struct ColonRuleExamples { case .bar↓ : return baz } """), - Example("private var action↓:(() -> Void)?") + Example("private var action↓:(() -> Void)?"), ] static let corrections = [ @@ -219,6 +219,6 @@ internal struct ColonRuleExamples { case .bar: return baz } """), - Example("private var action↓:(() -> Void)?"): Example("private var action: (() -> Void)?") + Example("private var action↓:(() -> Void)?"): Example("private var action: (() -> Void)?"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift index a304035d78..30af47edc1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift @@ -22,7 +22,7 @@ struct CommaInheritanceRule: OptInRule, SubstitutionCorrectableRule, protocol G { associatedtype Model: Codable, Equatable } - """) + """), ], triggeringExamples: [ Example("struct A: Codable↓ & Equatable {}"), @@ -36,7 +36,7 @@ struct CommaInheritanceRule: OptInRule, SubstitutionCorrectableRule, protocol G { associatedtype Model: Codable↓ & Equatable } - """) + """), ], corrections: [ Example("struct A: Codable↓ & Equatable {}"): Example("struct A: Codable, Equatable {}"), @@ -54,14 +54,14 @@ struct CommaInheritanceRule: OptInRule, SubstitutionCorrectableRule, protocol G { associatedtype Model: Codable, Equatable } - """) + """), ] ) // MARK: - Rule func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(in: file).map { + violationRanges(in: file).map { StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, characterOffset: $0.location)) @@ -70,8 +70,8 @@ struct CommaInheritanceRule: OptInRule, SubstitutionCorrectableRule, // MARK: - SubstitutionCorrectableRule - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { - return (violationRange, ", ") + func substitution(for violationRange: NSRange, in _: SwiftLintFile) -> (NSRange, String)? { + (violationRange, ", ") } func violationRanges(in file: SwiftLintFile) -> [NSRange] { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift index edb3054648..18f7425c59 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift @@ -26,7 +26,7 @@ struct CommaRule: CorrectableRule, SourceKitFreeRule { /// appends after an existing message (" (use beNil() to match nils)") case appends(ExpectationMessage, /* Appended Message */ String) } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example("func abc(a: String↓ ,b: String) { }"), @@ -48,7 +48,7 @@ struct CommaRule: CorrectableRule, SourceKitFreeRule { another: parameter, doIt: true, alignment: .center) """), - Example(#"Logger.logError("Hat is too large"↓, info: [])"#) + Example(#"Logger.logError("Hat is too large"↓, info: [])"#), ], corrections: [ Example("func abc(a: String↓,b: String) {}"): Example("func abc(a: String, b: String) {}"), @@ -84,12 +84,12 @@ struct CommaRule: CorrectableRule, SourceKitFreeRule { alignment: .center) """), Example(#"Logger.logError("Hat is too large"↓, info: [])"#): - Example(#"Logger.logError("Hat is too large", info: [])"#) + Example(#"Logger.logError("Hat is too large", info: [])"#), ] ) func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(in: file).map { + violationRanges(in: file).map { StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, byteOffset: $0.0.location)) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ComputedAccessorsOrderRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ComputedAccessorsOrderRuleExamples.swift index e0f7bf5e16..d2f0be9aa5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ComputedAccessorsOrderRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ComputedAccessorsOrderRuleExamples.swift @@ -1,6 +1,6 @@ struct ComputedAccessorsOrderRuleExamples { static var nonTriggeringExamples: [Example] { - return [ + [ Example(""" class Foo { var foo: Int { @@ -163,12 +163,12 @@ struct ComputedAccessorsOrderRuleExamples { protocol Foo { subscript(i: Int) -> Int { set get } } - """) + """), ] } static var triggeringExamples: [Example] { - return [ + [ Example(""" class Foo { var foo: Int { @@ -230,7 +230,7 @@ struct ComputedAccessorsOrderRuleExamples { } } } - """) + """), ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ConditionalReturnsOnNewlineRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ConditionalReturnsOnNewlineRule.swift index f1dc20bcc5..7c95293376 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ConditionalReturnsOnNewlineRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ConditionalReturnsOnNewlineRule.swift @@ -19,7 +19,7 @@ struct ConditionalReturnsOnNewlineRule: OptInRule { Example(""" guard something else { return } - """) + """), ], triggeringExamples: [ Example("↓guard true else { return }"), @@ -29,7 +29,7 @@ struct ConditionalReturnsOnNewlineRule: OptInRule { Example("↓if true { return \"YES\" } else { return \"NO\" }"), Example(""" ↓guard condition else { XCTFail(); return } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift new file mode 100644 index 0000000000..ba4ceb083b --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift @@ -0,0 +1,113 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule +struct ContrastedOpeningBraceRule: OptInRule, SwiftSyntaxCorrectableRule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "contrasted_opening_brace", + name: "Contrasted Opening Brace", + description: """ + The correct positioning of braces that introduce a block of code or member list is highly controversial. \ + No matter which style is preferred, consistency is key. Apart from different tastes, \ + the positioning of braces can also have a significant impact on the readability of the code, \ + especially for visually impaired developers. This rule ensures that braces are on a separate line \ + after the declaration to contrast the code block from the rest of the declaration. Comments between the \ + declaration and the opening brace are respected. Check out the `opening_brace` rule for a different style. + """, + kind: .style, + nonTriggeringExamples: ContrastedOpeningBraceRuleExamples.nonTriggeringExamples, + triggeringExamples: ContrastedOpeningBraceRuleExamples.triggeringExamples, + corrections: ContrastedOpeningBraceRuleExamples.corrections + ) +} + +private extension ContrastedOpeningBraceRule { + final class Visitor: CodeBlockVisitor { + override func collectViolations(for bracedItem: (some BracedSyntax)?) { + if let bracedItem, let correction = violationCorrection(bracedItem) { + violations.append( + ReasonedRuleViolation( + position: bracedItem.openingPosition, + reason: "Opening brace should be on a separate line", + correction: correction + ) + ) + } + } + + private func violationCorrection(_ node: some BracedSyntax) -> ReasonedRuleViolation.ViolationCorrection? { + let leftBrace = node.leftBrace + guard let previousToken = leftBrace.previousToken(viewMode: .sourceAccurate) else { + return nil + } + let openingPosition = node.openingPosition + let triviaBetween = previousToken.trailingTrivia + leftBrace.leadingTrivia + let previousLocation = previousToken.endLocation(converter: locationConverter) + let leftBraceLocation = leftBrace.startLocation(converter: locationConverter) + let parentStartColumn = node + .indentationDecidingParent? + .startLocation(converter: locationConverter) + .column ?? 1 + if previousLocation.line + 1 == leftBraceLocation.line, leftBraceLocation.column == parentStartColumn { + return nil + } + let comment = triviaBetween.description.trimmingTrailingCharacters(in: .whitespacesAndNewlines) + return .init( + start: previousToken.endPositionBeforeTrailingTrivia + SourceLength(of: comment), + end: openingPosition, + replacement: "\n" + String(repeating: " ", count: parentStartColumn - 1) + ) + } + } +} + +private extension BracedSyntax { + var openingPosition: AbsolutePosition { + leftBrace.positionAfterSkippingLeadingTrivia + } + + var indentationDecidingParent: (any SyntaxProtocol)? { + if let catchClause = parent?.as(CatchClauseSyntax.self) { + return catchClause.parent?.as(CatchClauseListSyntax.self)?.parent?.as(DoStmtSyntax.self) + } + if let ifExpr = parent?.as(IfExprSyntax.self) { + return ifExpr.indentationDecidingParent + } + if let binding = parent?.as(PatternBindingSyntax.self) { + return binding.parent?.as(PatternBindingListSyntax.self)?.parent?.as(VariableDeclSyntax.self) + } + if let closure = `as`(ClosureExprSyntax.self), + closure.keyPathInParent == \FunctionCallExprSyntax.trailingClosure { + return closure.leftBrace.previousIndentationDecidingToken + } + if let closureLabel = parent?.as(MultipleTrailingClosureElementSyntax.self)?.label { + return closureLabel.previousIndentationDecidingToken + } + return parent + } +} + +private extension TokenSyntax { + var previousIndentationDecidingToken: TokenSyntax { + var indentationDecidingToken = self + repeat { + if let previousToken = indentationDecidingToken.previousToken(viewMode: .sourceAccurate) { + indentationDecidingToken = previousToken + } else { + break + } + } while !indentationDecidingToken.leadingTrivia.containsNewlines() + return indentationDecidingToken + } +} + +private extension IfExprSyntax { + var indentationDecidingParent: any SyntaxProtocol { + if keyPathInParent == \IfExprSyntax.elseBody, let parent = parent?.as(IfExprSyntax.self) { + return parent.indentationDecidingParent + } + return self + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRuleExamples.swift new file mode 100644 index 0000000000..5caf1fda74 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRuleExamples.swift @@ -0,0 +1,482 @@ +// swiftlint:disable file_length + +// swiftlint:disable:next type_body_length +struct ContrastedOpeningBraceRuleExamples { + static let nonTriggeringExamples = [ + Example(""" + func abc() + { + } + """), + Example(""" + [].map() + { + $0 + } + """), + Example(""" + [].map( + { + } + ) + """), + Example(""" + if let a = b + { + } + """), + Example(""" + while a == b + { + } + """), + Example(""" + guard let a = b else + { + } + """), + Example(""" + struct Rule + { + } + """), + Example(""" + struct Parent + { + struct Child + { + let foo: Int + } + } + """), + Example(""" + func f(rect: CGRect) + { + { + let centre = CGPoint(x: rect.midX, y: rect.midY) + print(centre) + }() + } + """), + Example(""" + func f(rect: CGRect) -> () -> Void + { + { + let centre = CGPoint(x: rect.midX, y: rect.midY) + print(centre) + } + } + """), + Example(""" + func f() -> () -> Void + { + {} + } + """), + Example(""" + @MyProperty class Rule: + NSObject + { + var a: String + { + return "" + } + } + """), + Example(""" + self.foo( + ( + "String parameter", + { "Do something here" } + ) + ) + """), + Example(##"let pattern = #/(\{(?\w+)\})/#"##), + Example(""" + if c + {} + else + {} + """), + Example(""" + if c /* comment */ + { + return + } + """), + Example(""" + if c1 + { + return + } else if c2 + { + return + } else if c3 + { + return + } + """), + Example(""" + let a = f.map + { a in + a + } + """), + ] + + static let triggeringExamples = [ + Example(""" + func abc()↓{ + } + """), + Example(""" + func abc() { } + """), + Example(""" + func abc(a: A, + b: B) {} + """), + Example(""" + [].map { $0 } + """), + Example(""" + struct OldContentView: View ↓{ + @State private var showOptions = false + + var body: some View ↓{ + Button(action: { + self.showOptions.toggle() + })↓{ + Image(systemName: "gear") + } label: ↓{ + Image(systemName: "gear") + } + } + } + """), + Example(""" + class Rule + { + var a: String↓{ + return "" + } + } + """), + Example(""" + @MyProperty class Rule + { + var a: String + { + willSet↓{ + + } + didSet ↓{ + + } + } + } + """), + Example(""" + precedencegroup Group ↓{ + assignment: true + } + """), + Example(""" + class TestFile + { + func problemFunction() ↓{ + #if DEBUG + #endif + } + + func openingBraceViolation() + { + print("Brackets") + } + } + """, excludeFromDocumentation: true), + Example(""" + if + "test".isEmpty ↓{ + // code here + } + """), + Example(""" + if c ↓{} + else /* comment */ ↓{} + """), + Example(""" + if c + ↓{ + // code here + } + """), + Example(""" + if c1 ↓{ + return + } else if c2↓{ + return + } else if c3 + ↓{ + return + } + """), + Example(""" + func f() + { + return a.map + ↓{ $0 } + } + """), + Example(""" + a ↓{ + $0 + } b: ↓{ + $1 + } + """), + ] + + static let corrections = [ + Example(""" + struct Rule{} + """): Example(""" + struct Rule + {} + """), + Example(""" + struct Parent { + struct Child { + let foo: Int + } + } + """): Example(""" + struct Parent + { + struct Child + { + let foo: Int + } + } + """), + Example(""" + [].map(){ $0 } + """): Example(""" + [].map() + { $0 } + """), + Example(""" + if a == b{ } + """): Example(""" + if a == b + { } + """), + Example(""" + @MyProperty actor MyActor { + + } + """): Example(""" + @MyProperty actor MyActor + { + + } + """), + Example(""" + actor MyActor where T: U { + + } + """): Example(""" + actor MyActor where T: U + { + + } + """), + Example(""" + do { + + } catch { + + } + """): Example(""" + do + { + + } catch + { + + } + """), + Example(""" + do { + + } catch MyError.unknown { + + } + """): Example(""" + do + { + + } catch MyError.unknown + { + + } + """), + Example(""" + defer { + + } + """): Example(""" + defer + { + + } + """), + Example(""" + for a in b where a == c { + + } + """): Example(""" + for a in b where a == c + { + + } + """), + Example(""" + if varDecl.parent?.is(CodeBlockItemSyntax.self) == true // Local variable declaration + || varDecl.bindings.onlyElement?.accessor != nil // Computed property + || !node.type.is(SimpleTypeIdentifierSyntax.self) { // Complex or collection type + return .visitChildren + } + """): Example(""" + if varDecl.parent?.is(CodeBlockItemSyntax.self) == true // Local variable declaration + || varDecl.bindings.onlyElement?.accessor != nil // Computed property + || !node.type.is(SimpleTypeIdentifierSyntax.self) + { // Complex or collection type + return .visitChildren + } + """), + Example(""" + @MyProperty class Rule + { + var a: String { + didSet { + + } + } + } + """): Example(""" + @MyProperty class Rule + { + var a: String + { + didSet + { + + } + } + } + """), + Example(""" + precedencegroup Group{ + assignment: true + } + """): Example(""" + precedencegroup Group + { + assignment: true + } + """), + Example(""" + if c /* comment */ { + return + } + """): Example(""" + if c /* comment */ + { + return + } + """), + Example(""" + func foo() { + if q1, q2 { + do1() + } else if q3, q4 { + do2() + } + } + """): Example(""" + func foo() + { + if q1, q2 + { + do1() + } else if q3, q4 + { + do2() + } + } + """), + Example(""" + if + "test".isEmpty + // swiftlint:disable:next contrasted_opening_brace + { + // code here + } + """): Example(""" + if + "test".isEmpty + // swiftlint:disable:next contrasted_opening_brace + { + // code here + } + """), + Example(""" + private func f() + // comment + { + let a = 1 + } + """): Example(""" + private func f() + // comment + { + let a = 1 + } + """), + Example(""" + while true /* endless loop */ { + // nothing + } + """): Example(""" + while true /* endless loop */ + { + // nothing + } + """), + Example(""" + a.b { $0 } + .c { $1 } + """): Example(""" + a.b + { $0 } + .c + { $1 } + """), + Example(""" + a { + $0 + } b: { + $1 + } + """): Example(""" + a + { + $0 + } b: + { + $1 + } + """), + ] +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift index 67fc9565f2..1ba85155d5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift @@ -1,4 +1,3 @@ -@_spi(SyntaxTransformVisitor) import SwiftSyntax @SwiftSyntaxRule(explicitRewriter: true) @@ -29,7 +28,9 @@ struct ControlStatementRule: Rule { Example("if max(a, b) < c {}"), Example("switch (lhs, rhs) {}"), Example("if (f() { g() {} }) {}"), - Example("if (a + f() {} == 1) {}") + Example("if (a + f() {} == 1) {}"), + Example("if ({ true }()) {}"), + Example("if ({if i < 1 { true } else { false }}()) {}", excludeFromDocumentation: true), ], triggeringExamples: [ Example("↓if (condition) {}"), @@ -44,7 +45,7 @@ struct ControlStatementRule: Rule { Example("do { ; } ↓while (condition) {}"), Example("↓switch (foo) {}"), Example("do {} ↓catch(let error as NSError) {}"), - Example("↓if (max(a, b) < c) {}") + Example("↓if (max(a, b) < c) {}"), ], corrections: [ Example("↓if (condition) {}"): Example("if condition {}"), @@ -68,7 +69,7 @@ struct ControlStatementRule: Rule { """): Example(""" if a, b == 1 {} - """) + """), ] ) } @@ -166,27 +167,23 @@ private extension ControlStatementRule { } } -private class TrailingClosureFinder: SyntaxTransformVisitor { - func visitAny(_ node: Syntax) -> Bool { - false - } - - func visit(_ node: FunctionCallExprSyntax) -> Bool { - node.trailingClosure != nil - } - - func visit(_ node: SequenceExprSyntax) -> Bool { - node.elements.contains(where: visit) - } -} - private extension ExprSyntax { var unwrapped: ExprSyntax? { if let expr = self.as(TupleExprSyntax.self)?.elements.onlyElement?.expression { - return TrailingClosureFinder().visit(expr) ? nil : expr + return containsTrailingClosure(Syntax(expr)) ? nil : expr } return nil } + + private func containsTrailingClosure(_ node: Syntax) -> Bool { + switch node.as(SyntaxEnum.self) { + case .functionCallExpr(let node): + node.trailingClosure != nil || node.calledExpression.is(ClosureExprSyntax.self) + case .sequenceExpr(let node): + node.elements.contains { containsTrailingClosure(Syntax($0)) } + default: false + } + } } private extension ConditionElementListSyntax { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/DirectReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/DirectReturnRule.swift index 4221be8564..591b7e1d6a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/DirectReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/DirectReturnRule.swift @@ -16,7 +16,7 @@ struct DirectReturnRule: OptInRule { let a = 1 return b } - """), + """), Example(""" struct S { var a: Int { @@ -25,14 +25,14 @@ struct DirectReturnRule: OptInRule { return b } } - """), + """), Example(""" func f() -> Int { let b = 2 f() return b } - """), + """), Example(""" func f() -> Int { { i in @@ -40,7 +40,7 @@ struct DirectReturnRule: OptInRule { return i }(1) } - """) + """), ], triggeringExamples: [ Example(""" @@ -48,7 +48,7 @@ struct DirectReturnRule: OptInRule { let ↓b = 2 return b } - """), + """), Example(""" struct S { var a: Int { @@ -57,13 +57,13 @@ struct DirectReturnRule: OptInRule { return b } } - """), + """), Example(""" func f() -> Bool { let a = 1, ↓b = true return b } - """), + """), Example(""" func f() -> Int { { _ in @@ -71,7 +71,7 @@ struct DirectReturnRule: OptInRule { return b }(1) } - """), + """), Example(""" func f(i: Int) -> Int { if i > 1 { @@ -82,7 +82,7 @@ struct DirectReturnRule: OptInRule { return b } } - """) + """), ], corrections: [ Example(""" @@ -90,11 +90,11 @@ struct DirectReturnRule: OptInRule { let b = 2 return b } - """): Example(""" - func f() -> Int { - return 2 - } - """), + """): Example(""" + func f() -> Int { + return 2 + } + """), Example(""" struct S { var a: Int { @@ -106,28 +106,28 @@ struct DirectReturnRule: OptInRule { } func f() -> Int { 1 } } - """): Example(""" - struct S { - var a: Int { - // comment - return 2 > 1 - ? f() - : 1_000 + """): Example(""" + struct S { + var a: Int { + // comment + return 2 > 1 + ? f() + : 1_000 + } + func f() -> Int { 1 } } - func f() -> Int { 1 } - } - """), + """), Example(""" func f() -> Bool { let a = 1, b = true return b } - """): Example(""" - func f() -> Bool { - let a = 1 - return true - } - """), + """): Example(""" + func f() -> Bool { + let a = 1 + return true + } + """), Example(""" func f() -> Int { { _ in @@ -137,45 +137,45 @@ struct DirectReturnRule: OptInRule { return b }(1) } - """): Example(""" - func f() -> Int { - { _ in - // A comment - // Another comment - return 2 - }(1) - } - """), + """): Example(""" + func f() -> Int { + { _ in + // A comment + // Another comment + return 2 + }(1) + } + """), Example(""" func f() -> UIView { let view = instantiateView() as! UIView // swiftlint:disable:this force_cast return view } - """): Example(""" - func f() -> UIView { - return instantiateView() as! UIView // swiftlint:disable:this force_cast - } - """), + """): Example(""" + func f() -> UIView { + return instantiateView() as! UIView // swiftlint:disable:this force_cast + } + """), Example(""" func f() -> UIView { let view = instantiateView() as! UIView // swiftlint:disable:this force_cast return view // return the view } - """): Example(""" - func f() -> UIView { - return instantiateView() as! UIView // swiftlint:disable:this force_cast // return the view - } - """), + """): Example(""" + func f() -> UIView { + return instantiateView() as! UIView // swiftlint:disable:this force_cast // return the view + } + """), Example(""" func f() -> Bool { let b : Bool = true return b } - """): Example(""" - func f() -> Bool { - return true as Bool - } - """) + """): Example(""" + func f() -> Bool { + return true as Bool + } + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/EmptyEnumArgumentsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/EmptyEnumArgumentsRule.swift index 63c22fa8f6..fc58da0b89 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/EmptyEnumArgumentsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/EmptyEnumArgumentsRule.swift @@ -3,8 +3,9 @@ import SwiftSyntax private func wrapInSwitch( variable: String = "foo", _ str: String, - file: StaticString = #file, line: UInt = #line) -> Example { - return Example( + file: StaticString = #filePath, + line: UInt = #line) -> Example { + Example( """ switch \(variable) { \(str): break @@ -12,11 +13,11 @@ private func wrapInSwitch( """, file: file, line: line) } -private func wrapInFunc(_ str: String, file: StaticString = #file, line: UInt = #line) -> Example { - return Example(""" +private func wrapInFunc(_ str: String, file: StaticString = #filePath, line: UInt = #line) -> Example { + Example(""" func example(foo: Foo) { switch foo { - case \(str): + \(str): break } } @@ -49,17 +50,17 @@ struct EmptyEnumArgumentsRule: Rule { Example("guard foo == .bar() else { return }"), Example(""" if case .appStore = self.appInstaller, !UIDevice.isSimulator() { - viewController.present(self, animated: false) + viewController.present(self, animated: false) } else { - UIApplication.shared.open(self.appInstaller.url) + UIApplication.shared.open(self.appInstaller.url) } """), Example(""" let updatedUserNotificationSettings = deepLink.filter { nav in - guard case .settings(.notifications(_, nil)) = nav else { return false } - return true + guard case .settings(.notifications(_, nil)) = nav else { return false } + return true } - """) + """), ], triggeringExamples: [ wrapInSwitch("case .bar↓(_)"), @@ -75,17 +76,17 @@ struct EmptyEnumArgumentsRule: Rule { Example("guard case .bar↓() = foo else {\n}"), Example(""" if case .appStore↓(_) = self.appInstaller, !UIDevice.isSimulator() { - viewController.present(self, animated: false) + viewController.present(self, animated: false) } else { - UIApplication.shared.open(self.appInstaller.url) + UIApplication.shared.open(self.appInstaller.url) } """), Example(""" let updatedUserNotificationSettings = deepLink.filter { nav in - guard case .settings(.notifications↓(_, _)) = nav else { return false } - return true + guard case .settings(.notifications↓(_, _)) = nav else { return false } + return true } - """) + """), ], corrections: [ wrapInSwitch("case .bar↓(_)"): wrapInSwitch("case .bar"), @@ -99,16 +100,16 @@ struct EmptyEnumArgumentsRule: Rule { Example("guard case .bar↓(_) = foo else {"): Example("guard case .bar = foo else {"), Example(""" let updatedUserNotificationSettings = deepLink.filter { nav in - guard case .settings(.notifications↓(_, _)) = nav else { return false } - return true + guard case .settings(.notifications↓(_, _)) = nav else { return false } + return true } """): Example(""" let updatedUserNotificationSettings = deepLink.filter { nav in - guard case .settings(.notifications) = nav else { return false } - return true + guard case .settings(.notifications) = nav else { return false } + return true } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParametersRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParametersRule.swift index 984705e158..806a7d13df 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParametersRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParametersRule.swift @@ -15,20 +15,20 @@ struct EmptyParametersRule: Rule { Example("func foo(completion: () throws -> Void)"), Example("let foo: (ConfigurationTests) -> Void throws -> Void)"), Example("let foo: (ConfigurationTests) -> Void throws -> Void)"), - Example("let foo: (ConfigurationTests) ->Void throws -> Void)") + Example("let foo: (ConfigurationTests) ->Void throws -> Void)"), ], triggeringExamples: [ Example("let abc: ↓(Void) -> Void = {}"), Example("func foo(completion: ↓(Void) -> Void)"), Example("func foo(completion: ↓(Void) throws -> Void)"), - Example("let foo: ↓(Void) -> () throws -> Void)") + Example("let foo: ↓(Void) -> () throws -> Void)"), ], corrections: [ Example("let abc: ↓(Void) -> Void = {}"): Example("let abc: () -> Void = {}"), Example("func foo(completion: ↓(Void) -> Void)"): Example("func foo(completion: () -> Void)"), Example("func foo(completion: ↓(Void) throws -> Void)"): Example("func foo(completion: () throws -> Void)"), - Example("let foo: ↓(Void) -> () throws -> Void)"): Example("let foo: () -> () throws -> Void)") + Example("let foo: ↓(Void) -> () throws -> Void)"): Example("let foo: () -> () throws -> Void)"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParenthesesWithTrailingClosureRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParenthesesWithTrailingClosureRule.swift index c9edafb09a..78a7668484 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParenthesesWithTrailingClosureRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/EmptyParenthesesWithTrailingClosureRule.swift @@ -22,14 +22,14 @@ struct EmptyParenthesesWithTrailingClosureRule: Rule { }, completion: { _ in () }) - """) + """), ], triggeringExamples: [ Example("[1, 2].map↓() { $0 + 1 }"), Example("[1, 2].map↓( ) { $0 + 1 }"), Example("[1, 2].map↓() { number in\n number + 1 \n}"), Example("[1, 2].map↓( ) { number in\n number + 1 \n}"), - Example("func foo() -> [Int] {\n return [1, 2].map↓() { $0 + 1 }\n}") + Example("func foo() -> [Int] {\n return [1, 2].map↓() { $0 + 1 }\n}"), ], corrections: [ Example("[1, 2].map↓() { $0 + 1 }"): Example("[1, 2].map { $0 + 1 }"), @@ -41,7 +41,7 @@ struct EmptyParenthesesWithTrailingClosureRule: Rule { Example("func foo() -> [Int] {\n return [1, 2].map↓() { $0 + 1 }\n}"): Example("func foo() -> [Int] {\n return [1, 2].map { $0 + 1 }\n}"), Example("class C {\n#if true\nfunc f() {\n[1, 2].map↓() { $0 + 1 }\n}\n#endif\n}"): - Example("class C {\n#if true\nfunc f() {\n[1, 2].map { $0 + 1 }\n}\n#endif\n}") + Example("class C {\n#if true\nfunc f() {\n[1, 2].map { $0 + 1 }\n}\n#endif\n}"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift index 89df900515..7d86574842 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift @@ -16,7 +16,7 @@ struct ExplicitSelfRule: CorrectableRule, AnalyzerRule { ) func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] { - return violationRanges(in: file, compilerArguments: compilerArguments).map { + violationRanges(in: file, compilerArguments: compilerArguments).map { StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, characterOffset: $0.location)) @@ -42,7 +42,7 @@ struct ExplicitSelfRule: CorrectableRule, AnalyzerRule { private func violationRanges(in file: SwiftLintFile, compilerArguments: [String]) -> [NSRange] { guard compilerArguments.isNotEmpty else { - Issue.missingCompilerArguments(path: file.path, ruleID: Self.description.identifier).print() + Issue.missingCompilerArguments(path: file.path, ruleID: Self.identifier).print() return [] } @@ -69,7 +69,7 @@ struct ExplicitSelfRule: CorrectableRule, AnalyzerRule { return cursorsMissingExplicitSelf.compactMap { cursorInfo in guard let byteOffset = (cursorInfo["swiftlint.offset"] as? Int64).flatMap(ByteCount.init) else { - Issue.genericWarning("Cannot convert offsets in '\(Self.description.identifier)' rule.").print() + Issue.genericWarning("Cannot convert offsets in '\(Self.identifier)' rule.").print() return nil } @@ -80,13 +80,13 @@ struct ExplicitSelfRule: CorrectableRule, AnalyzerRule { private let kindsToFind: Set = [ "source.lang.swift.ref.function.method.instance", - "source.lang.swift.ref.var.instance" + "source.lang.swift.ref.var.instance", ] private extension SwiftLintFile { func allCursorInfo(compilerArguments: [String], atByteOffsets byteOffsets: [ByteCount]) throws -> [[String: any SourceKitRepresentable]] { - return try byteOffsets.compactMap { offset in + try byteOffsets.compactMap { offset in if isExplicitAccess(at: offset) { return nil } let cursorInfoRequest = Request.cursorInfoWithoutSymbolGraph( file: self.path!, offset: offset, arguments: compilerArguments diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRuleExamples.swift index 39626a427b..8dad43365f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRuleExamples.swift @@ -34,7 +34,7 @@ struct ExplicitSelfRuleExamples { func f1() { A(p1: 10).$p1 } - """) + """), ] static let triggeringExamples = [ @@ -80,7 +80,7 @@ struct ExplicitSelfRuleExamples { func f1() { A(p1: 10).$p1 } - """) + """), ] static let corrections = [ @@ -168,6 +168,6 @@ struct ExplicitSelfRuleExamples { func f1() { A(p1: 10).$p1 } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift index a92b6cae38..866fa84269 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift @@ -14,7 +14,7 @@ struct FileHeaderRule: OptInRule { nonTriggeringExamples: [ Example("let foo = \"Copyright\""), Example("let foo = 2 // Copyright"), - Example("let foo = 2\n // Copyright") + Example("let foo = 2\n // Copyright"), ], triggeringExamples: [ Example("// ↓Copyright"), @@ -27,7 +27,7 @@ struct FileHeaderRule: OptInRule { // Created by Marcelo Fabri on 27/11/16. // ↓Copyright © 2016 Realm. All rights reserved. // - """) + """), ].skipWrappingInCommentTests() ) @@ -95,15 +95,15 @@ struct FileHeaderRule: OptInRule { } private func makeViolation(at location: Location) -> StyleViolation { - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: location, - reason: "Header comments should be consistent with project patterns") + StyleViolation(ruleDescription: Self.description, + severity: configuration.severityConfiguration.severity, + location: location, + reason: "Header comments should be consistent with project patterns") } } private extension SyntaxKind { var isFileHeaderKind: Bool { - return self == .comment || self == .commentURL + self == .comment || self == .commentURL } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift index 202dc76a34..0b39623f2a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRule.swift @@ -74,7 +74,7 @@ struct FileTypesOrderRule: OptInRule { let fileTypeOffset = orderedFileTypeOffsets[index] let fileType = fileTypeOffset.fileType.rawValue - let expected = expectedTypes.map { $0.rawValue }.joined(separator: ",") + let expected = expectedTypes.map(\.rawValue).joined(separator: ",") let article = ["a", "e", "i", "o", "u"].contains(fileType.substring(from: 0, length: 1)) ? "An" : "A" let styleViolation = StyleViolation( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRuleExamples.swift index 65ddc8ecb1..2d9c3d3691 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileTypesOrderRuleExamples.swift @@ -108,7 +108,7 @@ internal struct FileTypesOrderRuleExamples { return 1 } } - """ + """, ] static let nonTriggeringExamples = [ @@ -138,7 +138,7 @@ internal struct FileTypesOrderRuleExamples { LibraryItem(ContentView()) } } - """) + """), ] static let triggeringExamples = [ @@ -222,6 +222,6 @@ internal struct FileTypesOrderRuleExamples { Text("Hello, World!") } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRuleExamples.swift index 3a1779bb56..0e6cba45f9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRuleExamples.swift @@ -29,7 +29,7 @@ internal struct IdentifierNameRuleExamples { static var Bar = 0 } """), - Example("func √ (arg: Double) -> Double { arg }", configuration: ["additional_operators": "√"]) + Example("func √ (arg: Double) -> Double { arg }", configuration: ["additional_operators": "√"]), ] static let triggeringExamples = [ @@ -95,6 +95,6 @@ internal struct IdentifierNameRuleExamples { } } """), - Example("func ↓√ (arg: Double) -> Double { arg }") + Example("func ↓√ (arg: Double) -> Double { arg }"), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitGetterRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitGetterRuleExamples.swift index c2683aa487..433ee125e3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitGetterRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitGetterRuleExamples.swift @@ -202,7 +202,7 @@ struct ImplicitGetterRuleExamples { } } } - """) + """), ] static let triggeringExamples: [Example] = [ @@ -261,6 +261,6 @@ struct ImplicitGetterRuleExamples { } } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift index 236280e18a..f58d9db366 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift @@ -66,9 +66,9 @@ private extension ImplicitReturnRule { return } let returnKeyword = returnStmt.returnKeyword - violations.append(returnKeyword.positionAfterSkippingLeadingTrivia) - violationCorrections.append( - ViolationCorrection( + violations.append( + at: returnKeyword.positionAfterSkippingLeadingTrivia, + correction: .init( start: returnKeyword.positionAfterSkippingLeadingTrivia, end: returnKeyword.endPositionBeforeTrailingTrivia .advanced(by: returnStmt.expression == nil ? 0 : 1), diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples.swift index eaf6b8cd71..df92fb92da 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples.swift @@ -8,7 +8,7 @@ struct ImplicitReturnRuleExamples { [1, 2].first(where: { true }) - """) + """), ] static let triggeringExamples = [ @@ -27,7 +27,7 @@ struct ImplicitReturnRuleExamples { [1, 2].first(where: { ↓return true }) - """) + """), ] static let corrections = [ @@ -60,7 +60,7 @@ struct ImplicitReturnRuleExamples { [1, 2].first(where: { true }) - """) + """), ] } @@ -102,7 +102,7 @@ struct ImplicitReturnRuleExamples { return g() func g() -> Int { 4 } } - """) + """), ] static let triggeringExamples = [ @@ -118,7 +118,7 @@ struct ImplicitReturnRuleExamples { """), Example(""" func f() { ↓return } - """) + """), ] static let corrections = [ @@ -156,7 +156,7 @@ struct ImplicitReturnRuleExamples { \("") // Another comment } - """) + """), ] } @@ -178,7 +178,7 @@ struct ImplicitReturnRuleExamples { 0 } } - """) + """), ] static let triggeringExamples = [ @@ -198,7 +198,7 @@ struct ImplicitReturnRuleExamples { ↓return 0 } } - """) + """), ] static let corrections = [ @@ -219,7 +219,7 @@ struct ImplicitReturnRuleExamples { } } } - """) + """), ] } @@ -244,7 +244,7 @@ struct ImplicitReturnRuleExamples { return nil } } - """) + """), ] static let triggeringExamples = [ @@ -261,7 +261,7 @@ struct ImplicitReturnRuleExamples { ↓return nil } } - """) + """), ] static let corrections = [ @@ -290,7 +290,7 @@ struct ImplicitReturnRuleExamples { nil } } - """) + """), ] } @@ -303,7 +303,7 @@ struct ImplicitReturnRuleExamples { return res } } - """) + """), ] static let triggeringExamples = [ @@ -313,7 +313,7 @@ struct ImplicitReturnRuleExamples { ↓return i } } - """) + """), ] static let corrections = [ @@ -329,7 +329,25 @@ struct ImplicitReturnRuleExamples { i } } - """) + """), + ] + } + + struct MixedExamples { + static let corrections = [ + Example(""" + func foo() -> Int { + ↓return [1, 2].first(where: { + ↓return true + }) + } + """): Example(""" + func foo() -> Int { + [1, 2].first(where: { + true + }) + } + """), ] } @@ -353,7 +371,8 @@ struct ImplicitReturnRuleExamples { FunctionExamples.corrections, GetterExamples.corrections, InitializerExamples.corrections, - SubscriptExamples.corrections + SubscriptExamples.corrections, + MixedExamples.corrections, ] .reduce(into: [:]) { result, element in result.merge(element) { _, _ in diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRule.swift index e817d48d4b..28dc3a7fc8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRule.swift @@ -101,7 +101,7 @@ private extension InclusiveLanguageRule { } } - override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRuleExamples.swift index 9ebc7f0221..39b9eb3b9b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/InclusiveLanguageRuleExamples.swift @@ -15,7 +15,7 @@ internal struct InclusiveLanguageRuleExamples { case mastercard } """), - Example("func chargeMasterCard(_ card: Card) {}") + Example("func chargeMasterCard(_ card: Card) {}"), ] static let triggeringExamples: [Example] = [ @@ -37,7 +37,7 @@ internal struct InclusiveLanguageRuleExamples { final class FooBar { func register<↓Master, ↓Slave>(one: Master, two: Slave) {} } - """) + """), ] // MARK: - Non-default config @@ -54,8 +54,8 @@ internal struct InclusiveLanguageRuleExamples { private func doThisThing() {} """, configuration: [ "override_terms": ["abc123"], - "additional_terms": ["xyz789"] - ]) + "additional_terms": ["xyz789"], + ]), ] static let triggeringExamplesWithConfig: [Example] = [ @@ -75,6 +75,6 @@ internal struct InclusiveLanguageRuleExamples { private var ↓fooBar = "abc" """, configuration: [ "additional_terms": ["FoObAr"] - ]) + ]), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index d79e11b85b..888c9eb01c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -29,13 +29,13 @@ struct IndentationWidthRule: OptInRule { Example("firstLine\n secondLine"), Example("firstLine\n\tsecondLine\n\t\tthirdLine\n\n\t\tfourthLine"), Example("firstLine\n\tsecondLine\n\t\tthirdLine\n\t//test\n\t\tfourthLine"), - Example("firstLine\n secondLine\n thirdLine\nfourthLine") + Example("firstLine\n secondLine\n thirdLine\nfourthLine"), ], triggeringExamples: [ Example("↓ firstLine", testMultiByteOffsets: false, testDisableCommand: false), Example("firstLine\n secondLine"), Example("firstLine\n\tsecondLine\n\n↓\t\t\tfourthLine"), - Example("firstLine\n secondLine\n thirdLine\n↓ fourthLine") + Example("firstLine\n secondLine\n thirdLine\n↓ fourthLine"), ].skipWrappingInCommentTests() ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift index 59ccd8b946..329fad6178 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift @@ -14,7 +14,7 @@ struct LeadingWhitespaceRule: CorrectableRule, SourceKitFreeRule { ], triggeringExamples: [ Example("\n//"), - Example(" //") + Example(" //"), ].skipMultiByteOffsetTests().skipDisableCommandTests(), corrections: [ Example("\n //", testMultiByteOffsets: false): Example("//") @@ -27,9 +27,13 @@ struct LeadingWhitespaceRule: CorrectableRule, SourceKitFreeRule { return [] } - return [StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file.path, line: 1))] + return [ + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file.path, line: 1) + ), + ] } func correct(file: SwiftLintFile) -> [Correction] { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LetVarWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LetVarWhitespaceRule.swift index 43f3f36408..018ad886e1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LetVarWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LetVarWhitespaceRule.swift @@ -16,77 +16,77 @@ struct LetVarWhitespaceRule: OptInRule { var x = 1 var y = 2 - """), + """), Example(""" let a = 5 var x = 1 - """), + """), Example(""" var a = 0 - """), + """), Example(""" let a = 1 + 2 let b = 5 - """), + """), Example(""" var x: Int { return 0 } - """), + """), Example(""" var x: Int { let a = 0 return a } - """), + """), Example(""" #if os(macOS) let a = 0 func f() {} #endif - """), + """), Example(""" #warning("TODO: remove it") let a = 0 #warning("TODO: remove it") let b = 0 - """), + """), Example(""" #error("TODO: remove it") let a = 0 - """), + """), Example(""" @available(swift 4) let a = 0 - """), + """), Example(""" @objc var s: String = "" - """), + """), Example(""" @objc func a() {} - """), + """), Example(""" var x = 0 lazy var y = 0 - """), + """), Example(""" @available(OSX, introduced: 10.6) @available(*, deprecated) var x = 0 - """), + """), Example(""" // swiftlint:disable superfluous_disable_command // swiftlint:disable force_cast let x = bar as! Bar - """), + """), Example(""" @available(swift 4) @UserDefault("param", defaultValue: true) @@ -94,14 +94,14 @@ struct LetVarWhitespaceRule: OptInRule { @Attribute func f() {} - """), + """), // Don't trigger on local variable declarations. Example(""" var x: Int { let a = 0 return a } - """), + """), Example(""" static var test: String { /* Comment block */ let s = "!" @@ -109,7 +109,7 @@ struct LetVarWhitespaceRule: OptInRule { } func f() {} - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(#""" @Flag(name: "name", help: "help") var fix = false @@ -120,32 +120,32 @@ struct LetVarWhitespaceRule: OptInRule { var format = false @Flag(help: "help") var useAlternativeExcluding = false - """#, excludeFromDocumentation: true) + """#, excludeFromDocumentation: true), ].map(Self.wrapIntoClass) + [ Example(""" a = 2 - """), + """), Example(""" a = 2 var b = 3 - """), + """), Example(""" #warning("message") let a = 2 - """), + """), Example(""" #if os(macOS) let a = 2 #endif - """), + """), // Don't trigger in closure bodies. Example(""" f { let a = 1 return a } - """), + """), Example(""" func f() { #if os(macOS) @@ -155,27 +155,27 @@ struct LetVarWhitespaceRule: OptInRule { return 1 #endif } - """) + """), ], triggeringExamples: [ Example(""" let a ↓func x() {} - """), + """), Example(""" var x = 0 ↓@objc func f() {} - """), + """), Example(""" var x = 0 ↓@objc func f() {} - """), + """), Example(""" @objc func f() { } ↓var x = 0 - """), + """), Example(""" func f() {} ↓@Wapper @@ -183,18 +183,18 @@ struct LetVarWhitespaceRule: OptInRule { @Wapper var isEnabled = true ↓func g() {} - """), + """), Example(""" #if os(macOS) let a = 0 ↓func f() {} #endif - """) + """), ].map(Self.wrapIntoClass) + [ Example(""" let a = 2 ↓b = 1 - """), + """), Example(""" #if os(macOS) let a = 0 @@ -203,7 +203,7 @@ struct LetVarWhitespaceRule: OptInRule { func f() {} ↓let a = 1 #endif - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] ) @@ -215,12 +215,12 @@ struct LetVarWhitespaceRule: OptInRule { private extension LetVarWhitespaceRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: MemberBlockItemListSyntax) { - collectViolations(from: node, using: { $0.decl }) + collectViolations(from: node, using: { $0.decl }) // swiftlint:disable:this prefer_key_path } override func visitPost(_ node: CodeBlockItemListSyntax) { if node.isInValidContext { - collectViolations(from: node, using: { $0.unwrap }) + collectViolations(from: node, using: \.unwrap) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift index 245a2cb5ea..29193dba7b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift @@ -48,7 +48,7 @@ struct LiteralExpressionEndIndentationRule: Rule, OptInRule { key: 0, key2: 20 ] - """) + """), ], triggeringExamples: [ Example(""" @@ -67,7 +67,7 @@ struct LiteralExpressionEndIndentationRule: Rule, OptInRule { let x = [ key: value ↓] - """) + """), ], corrections: [ Example(""" @@ -117,13 +117,13 @@ struct LiteralExpressionEndIndentationRule: Rule, OptInRule { 3, 4 ] - """) + """), ] ) func validate(file: SwiftLintFile) -> [StyleViolation] { - return violations(in: file).map { violation in - return styleViolation(for: violation, in: file) + violations(in: file).map { violation in + styleViolation(for: violation, in: file) } } @@ -169,8 +169,7 @@ extension LiteralExpressionEndIndentationRule: CorrectableRule { } var corrections = correctedLocations.map { - return Correction(ruleDescription: Self.description, - location: Location(file: file, characterOffset: $0)) + Correction(ruleDescription: Self.description, location: Location(file: file, characterOffset: $0)) } file.write(correctedContents) @@ -214,14 +213,15 @@ extension LiteralExpressionEndIndentationRule { } fileprivate func violations(in file: SwiftLintFile) -> [Violation] { - return file.structureDictionary.traverseDepthFirst { subDict in + file.structureDictionary.traverseDepthFirst { subDict in guard let kind = subDict.expressionKind else { return nil } guard let violation = violation(in: file, of: kind, dictionary: subDict) else { return nil } return [violation] } } - private func violation(in file: SwiftLintFile, of kind: SwiftExpressionKind, + private func violation(in file: SwiftLintFile, + of kind: SwiftExpressionKind, dictionary: SourceKittenDictionary) -> Violation? { guard kind == .dictionary || kind == .array else { return nil diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRule.swift index 409e5f3760..9e4909706e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRule.swift @@ -14,7 +14,7 @@ struct ModifierOrderRule: ASTRule, OptInRule, CorrectableRule { ) func validate(file: SwiftLintFile, - kind: SwiftDeclarationKind, + kind _: SwiftDeclarationKind, dictionary: SourceKittenDictionary) -> [StyleViolation] { guard let offset = dictionary.offset else { return [] @@ -32,14 +32,14 @@ struct ModifierOrderRule: ASTRule, OptInRule, CorrectableRule { severity: configuration.severityConfiguration.severity, location: Location(file: file, byteOffset: offset), reason: reason - ) + ), ] } return [] } func correct(file: SwiftLintFile) -> [Correction] { - return file.structureDictionary.traverseDepthFirst { subDict in + file.structureDictionary.traverseDepthFirst { subDict in guard subDict.declarationKind != nil else { return nil } return correct(file: file, dictionary: subDict) } @@ -85,7 +85,7 @@ struct ModifierOrderRule: ASTRule, OptInRule, CorrectableRule { file: file, byteOffset: offset ) - ) + ), ] } return corrections @@ -119,7 +119,7 @@ struct ModifierOrderRule: ASTRule, OptInRule, CorrectableRule { let prioritizedModifiers = self.prioritizedModifiers(violatableModifiers: violatableModifiers) let sortedByPriorityModifiers = prioritizedModifiers .sorted { $0.priority < $1.priority } - .map { $0.modifier } + .map(\.modifier) return zip(sortedByPriorityModifiers, violatableModifiers).filter { $0 != $1 } } @@ -171,7 +171,7 @@ private extension SourceKittenDictionary { private extension String { func lastComponentAfter(_ character: String) -> String { - return components(separatedBy: character).last ?? "" + components(separatedBy: character).last ?? "" } } @@ -180,5 +180,5 @@ private struct ModifierDescription: Equatable { let group: SwiftDeclarationAttributeKind.ModifierGroup let offset: ByteCount let length: ByteCount - var range: ByteRange { return ByteRange(location: offset, length: length) } + var range: ByteRange { ByteRange(location: offset, length: length) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRuleExamples.swift index 2e382185f9..fc6ee5d416 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRuleExamples.swift @@ -110,7 +110,7 @@ internal struct ModifierOrderRuleExamples { class Foo { internal lazy var bar: String = "foo" } - """) + """), ] static let triggeringExamples = [ @@ -223,6 +223,6 @@ internal struct ModifierOrderRuleExamples { class Foo { lazy internal var bar: String = "foo" } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsBracketsRule.swift index 3575ab8e3a..35cd7feee8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsBracketsRule.swift @@ -99,7 +99,7 @@ struct MultilineArgumentsBracketsRule: OptInRule { SomeType( a: 1 ) {} onError: {} - """) + """), ], triggeringExamples: [ Example(""" @@ -153,7 +153,7 @@ struct MultilineArgumentsBracketsRule: OptInRule { title: "MacBook", subtitle: "M1", action: { [weak self] in print("action tapped") }↓)) - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRule.swift index 724a46a2ba..a5e50b31a7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRule.swift @@ -70,7 +70,7 @@ private extension MultilineArgumentsRule { private func removeViolationsBeforeFirstClosure(arguments: [Argument], violations: [Argument]) -> [Argument] { - guard let firstClosure = arguments.first(where: { $0.isClosure }), + guard let firstClosure = arguments.first(where: \.isClosure), let firstArgument = arguments.first else { return violations } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift index d1501d08b9..da7c1a3f0e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift @@ -68,7 +68,7 @@ internal struct MultilineArgumentsRuleExamples { } completion: { _ in print("b") } - """) + """), ] static let triggeringExamples = [ @@ -89,6 +89,6 @@ internal struct MultilineArgumentsRuleExamples { 0, ↓param1: 1, param2: true, ↓param3: [3] ) - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift index a12b0e8fd1..2d048013ba 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift @@ -60,7 +60,7 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { Example(""" self.happeningNewsletterOn = self.updateCurrentUser .map { $0.newsletters.happening }.skipNil().skipRepeats() - """) + """), ], triggeringExamples: [ Example(""" @@ -88,17 +88,17 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { a.b { // ““ }↓.e() - """) + """), ] ) func validate(file: SwiftLintFile, kind: SwiftExpressionKind, dictionary: SourceKittenDictionary) -> [StyleViolation] { - return violatingOffsets(file: file, kind: kind, dictionary: dictionary).map { offset in - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, characterOffset: offset)) + violatingOffsets(file: file, kind: kind, dictionary: dictionary).map { offset in + StyleViolation(ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, characterOffset: offset)) } } @@ -116,7 +116,7 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { return (dotLine: line, dotOffset: offset, range: range) } - let uniqueLines = calls.map { $0.dotLine }.unique + let uniqueLines = calls.map(\.dotLine).unique if uniqueLines.count == 1 { return [] } @@ -127,7 +127,7 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { !callHasLeadingNewline(file: file, callRange: line.range) } - return noLeadingNewlineViolations.map { $0.dotOffset } + return noLeadingNewlineViolations.map(\.dotOffset) } private static let whitespaceDotRegex = regex("\\s*\\.") @@ -214,7 +214,7 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { private extension SourceKittenDictionary { var subcalls: [SourceKittenDictionary] { - return substructure.compactMap { dictionary -> SourceKittenDictionary? in + substructure.compactMap { dictionary -> SourceKittenDictionary? in guard dictionary.expressionKind == .call else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift index 9be4eab865..38264908d5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift @@ -46,7 +46,7 @@ struct MultilineLiteralBracketsRule: OptInRule { 5, 6, 7, 8, 9 ] - """) + """), ], triggeringExamples: [ Example(""" @@ -92,13 +92,13 @@ struct MultilineLiteralBracketsRule: OptInRule { 4, 5, 6, 7, 8, 9↓] - """), + """), Example(""" _ = [↓1, 2, 3, 4, 5, 6, 7, 8, 9 ] - """), + """), Example(""" class Hogwarts { let houseCup = [ @@ -107,7 +107,7 @@ struct MultilineLiteralBracketsRule: OptInRule { $0.isValid }.sum()↓] } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift index ee19dca382..5021c338c6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift @@ -53,7 +53,7 @@ struct MultilineParametersBracketsRule: OptInRule { func foo(a: [Int] = [ 1 ]) - """) + """), ], triggeringExamples: [ Example(""" @@ -86,12 +86,12 @@ struct MultilineParametersBracketsRule: OptInRule { func foo(↓param1: T, param2: String, param3: String ) -> T - """) + """), ] ) func validate(file: SwiftLintFile) -> [StyleViolation] { - return violations(in: file.structureDictionary, file: file) + violations(in: file.structureDictionary, file: file) } private func violations(in substructure: SourceKittenDictionary, file: SwiftLintFile) -> [StyleViolation] { @@ -112,7 +112,7 @@ struct MultilineParametersBracketsRule: OptInRule { let parameters = substructure.substructure.filter { $0.declarationKind == .varParameter } let parameterBodies = parameters.compactMap { $0.content(in: file) } let parametersNewlineCount = parameterBodies.map { body in - return body.countOccurrences(of: "\n") + body.countOccurrences(of: "\n") }.reduce(0, +) let declarationNewlineCount = functionName.countOccurrences(of: "\n") let isMultiline = declarationNewlineCount > parametersNewlineCount diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift index 281e97a504..72f25152c0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift @@ -43,6 +43,12 @@ private extension MultilineParametersRule { numberOfParameters += 1 } + if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters, + configuration.allowsSingleLine, + numberOfParameters > maxNumberOfSingleLineParameters { + return true + } + guard linesWithParameters.count > (configuration.allowsSingleLine ? 1 : 0), numberOfParameters != linesWithParameters.count else { return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift index ca3ab5b784..4abcb60e39 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift @@ -196,7 +196,14 @@ internal struct MultilineParametersRuleExamples { param3: [String] ) { } } - """, configuration: ["allows_single_line": false]) + """, configuration: ["allows_single_line": false]), + Example("func foo(param1: Int, param2: Bool, param3: [String]) { }", + configuration: ["max_number_of_single_line_parameters": 3]), + Example(""" + func foo(param1: Int, + param2: Bool, + param3: [String]) { } + """, configuration: ["max_number_of_single_line_parameters": 3]), ] static let triggeringExamples: [Example] = [ @@ -335,6 +342,13 @@ internal struct MultilineParametersRuleExamples { Example("func ↓foo(param1: Int, param2: Bool) { }", configuration: ["allows_single_line": false]), Example("func ↓foo(param1: Int, param2: Bool, param3: [String]) { }", - configuration: ["allows_single_line": false]) + configuration: ["allows_single_line": false]), + Example("func ↓foo(param1: Int, param2: Bool, param3: [String]) { }", + configuration: ["max_number_of_single_line_parameters": 2]), + Example(""" + func ↓foo(param1: Int, + param2: Bool, param3: [String]) { } + """, + configuration: ["max_number_of_single_line_parameters": 3]), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultipleClosuresWithTrailingClosureRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultipleClosuresWithTrailingClosureRule.swift index 76d294bc7b..288c3fce40 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultipleClosuresWithTrailingClosureRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultipleClosuresWithTrailingClosureRule.swift @@ -20,7 +20,7 @@ struct MultipleClosuresWithTrailingClosureRule: Rule { } """), Example("foo.method { print(0) } arg2: { print(1) }"), - Example("foo.methodWithParenArgs((0, 1), arg2: (0, 1, 2)) { $0 } arg4: { $0 }") + Example("foo.methodWithParenArgs((0, 1), arg2: (0, 1, 2)) { $0 } arg4: { $0 }"), ], triggeringExamples: [ Example("foo.something(param1: { $0 }) ↓{ $0 + 1 }"), @@ -32,7 +32,7 @@ struct MultipleClosuresWithTrailingClosureRule: Rule { } """), Example("foo.multipleTrailing(arg1: { $0 }) { $0 } arg3: { $0 }"), - Example("foo.methodWithParenArgs(param1: { $0 }, param2: (0, 1), (0, 1)) { $0 }") + Example("foo.methodWithParenArgs(param1: { $0 }, param2: (0, 1), (0, 1)) { $0 }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/NoSpaceInMethodCallRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/NoSpaceInMethodCallRule.swift index 493d44257d..9c185a2aa4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/NoSpaceInMethodCallRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/NoSpaceInMethodCallRule.swift @@ -23,7 +23,7 @@ struct NoSpaceInMethodCallRule: Rule { } label: { Text("Button") } - """) + """), ], triggeringExamples: [ Example("foo↓ ()"), @@ -32,7 +32,7 @@ struct NoSpaceInMethodCallRule: Rule { Example("object.foo↓ (value: 1)"), Example("object.foo↓ () {}"), Example("object.foo↓ ()"), - Example("object.foo↓ (value: 1) { x in print(x) }") + Example("object.foo↓ (value: 1) { x in print(x) }"), ], corrections: [ Example("foo↓ ()"): Example("foo()"), @@ -40,7 +40,7 @@ struct NoSpaceInMethodCallRule: Rule { Example("object.foo↓ (1)"): Example("object.foo(1)"), Example("object.foo↓ (value: 1)"): Example("object.foo(value: 1)"), Example("object.foo↓ () {}"): Example("object.foo() {}"), - Example("object.foo↓ ()"): Example("object.foo()") + Example("object.foo↓ ()"): Example("object.foo()"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/NonOverridableClassDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/NonOverridableClassDeclarationRule.swift index fa407544c9..a4ede58399 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/NonOverridableClassDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/NonOverridableClassDeclarationRule.swift @@ -9,7 +9,7 @@ struct NonOverridableClassDeclarationRule: SwiftSyntaxCorrectableRule, OptInRule name: "Class Declaration in Final Class", description: """ Class methods and properties in final classes should themselves be final, just as if the declarations - are private. In both cases, they cannot be overriden. Using `final class` or `static` makes this explicit. + are private. In both cases, they cannot be overridden. Using `final class` or `static` makes this explicit. """, kind: .style, nonTriggeringExamples: [ @@ -50,7 +50,7 @@ struct NonOverridableClassDeclarationRule: SwiftSyntaxCorrectableRule, OptInRule class func f() {} } } - """) + """), ], triggeringExamples: [ Example(""" @@ -72,7 +72,7 @@ struct NonOverridableClassDeclarationRule: SwiftSyntaxCorrectableRule, OptInRule private ↓class var b: Bool { true } private ↓class func f() {} } - """) + """), ], corrections: [ Example(""" @@ -92,7 +92,7 @@ struct NonOverridableClassDeclarationRule: SwiftSyntaxCorrectableRule, OptInRule final class C { static var b: Bool { true } } - """) + """), ] ) } @@ -108,7 +108,7 @@ private extension NonOverridableClassDeclarationRule { return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { + override func visitPost(_: ClassDeclSyntax) { _ = finalClassScope.pop() } @@ -127,20 +127,18 @@ private extension NonOverridableClassDeclarationRule { inFinalClass || modifiers.contains(keyword: .private) else { return } - violations.append(ReasonedRuleViolation( + violations.append(.init( position: classKeyword.positionAfterSkippingLeadingTrivia, reason: inFinalClass - ? "Class \(types) in final classes should themselves be final" - : "Private class methods and properties should be declared final", - severity: configuration.severity - )) - violationCorrections.append( - ViolationCorrection( + ? "Class \(types) in final classes should themselves be final" + : "Private class methods and properties should be declared final", + severity: configuration.severity, + correction: .init( start: classKeyword.positionAfterSkippingLeadingTrivia, end: classKeyword.endPositionBeforeTrailingTrivia, replacement: configuration.finalClassModifier.rawValue ) - ) + )) } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRuleExamples.swift index d17fbe0614..a63da2377c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRuleExamples.swift @@ -1,6 +1,6 @@ internal struct NumberSeparatorRuleExamples { static let nonTriggeringExamples: [Example] = { - return ["-", "+", ""].flatMap { (sign: String) -> [Example] in + ["-", "+", ""].flatMap { (sign: String) -> [Example] in [ Example("let foo = \(sign)100"), Example("let foo = \(sign)1_000"), @@ -22,7 +22,7 @@ internal struct NumberSeparatorRuleExamples { """, excludeFromDocumentation: true), Example(""" let color = #colorLiteral(red: 0.354_398_250_6, green: 0.318_749_547, blue: 0.636_701_524_3, alpha: 1) - """, configuration: ["minimum_fraction_length": 3], excludeFromDocumentation: true) + """, configuration: ["minimum_fraction_length": 3], excludeFromDocumentation: true), ] } }() @@ -33,7 +33,7 @@ internal struct NumberSeparatorRuleExamples { static let corrections = makeCorrections(signs: [("-↓", "-"), ("+↓", "+"), ("↓", "")]) private static func makeTriggeringExamples(signs: [String]) -> [Example] { - return signs.flatMap { (sign: String) -> [Example] in + signs.flatMap { (sign: String) -> [Example] in [ Example("let foo = \(sign)10_0"), Example("let foo = \(sign)1000"), @@ -43,7 +43,7 @@ internal struct NumberSeparatorRuleExamples { Example("let foo = \(sign)1.0001", configuration: ["minimum_fraction_length": 3]), Example("let foo = \(sign)1_000_000.000000_1", configuration: ["minimum_fraction_length": 3]), Example("let foo = \(sign)1000000.000000_1"), - Example("let foo = \(sign)6.2832e-6", configuration: ["minimum_fraction_length": 3]) + Example("let foo = \(sign)6.2832e-6", configuration: ["minimum_fraction_length": 3]), ] } } @@ -54,7 +54,10 @@ internal struct NumberSeparatorRuleExamples { [ Example("let foo: Double = \(sign)100000)"), Example("let foo: Double = \(sign)10.000000_1)", configuration: ["minimum_fraction_length": 3]), - Example("let foo: Double = \(sign)123456 / ↓447.214214)", configuration: ["minimum_fraction_length": 3]) + Example( + "let foo: Double = \(sign)123456 / ↓447.214214)", + configuration: ["minimum_fraction_length": 3] + ), ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift index 92e90eeedc..fe5b55f517 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift @@ -8,8 +8,14 @@ struct OpeningBraceRule: SwiftSyntaxCorrectableRule { static let description = RuleDescription( identifier: "opening_brace", name: "Opening Brace Spacing", - description: "Opening braces should be preceded by a single space and on the same line " + - "as the declaration", + description: """ + The correct positioning of braces that introduce a block of code or member list is highly controversial. \ + No matter which style is preferred, consistency is key. Apart from different tastes, \ + the positioning of braces can also have a significant impact on the readability of the code, \ + especially for visually impaired developers. This rule ensures that braces are preceded \ + by a single space and on the same line as the declaration. Comments between the declaration and the \ + opening brace are respected. Check out the `contrasted_opening_brace` rule for a different style. + """, kind: .style, nonTriggeringExamples: OpeningBraceRuleExamples.nonTriggeringExamples, triggeringExamples: OpeningBraceRuleExamples.triggeringExamples, @@ -18,224 +24,186 @@ struct OpeningBraceRule: SwiftSyntaxCorrectableRule { } private extension OpeningBraceRule { - final class Visitor: ViolationsSyntaxVisitor { + final class Visitor: CodeBlockVisitor { + // MARK: - Type Declarations + override func visitPost(_ node: ActorDeclSyntax) { - let body = node.memberBlock - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineTypeHeaders, + hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) { + return } + + super.visitPost(node) } override func visitPost(_ node: ClassDeclSyntax) { - let body = node.memberBlock - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineTypeHeaders, + hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) { + return } + + super.visitPost(node) } override func visitPost(_ node: EnumDeclSyntax) { - let body = node.memberBlock - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineTypeHeaders, + hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) { + return } + + super.visitPost(node) } override func visitPost(_ node: ExtensionDeclSyntax) { - let body = node.memberBlock - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineTypeHeaders, + hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) { + return } + + super.visitPost(node) } override func visitPost(_ node: ProtocolDeclSyntax) { - let body = node.memberBlock - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineTypeHeaders, + hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) { + return } - } - override func visitPost(_ node: StructDeclSyntax) { - let body = node.memberBlock - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + super.visitPost(node) } - override func visitPost(_ node: CatchClauseSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + override func visitPost(_ node: StructDeclSyntax) { + if configuration.ignoreMultilineTypeHeaders, + hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) { + return } - } - override func visitPost(_ node: DeferStmtSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + super.visitPost(node) } - override func visitPost(_ node: DoStmtSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } - } + // MARK: - Conditional Statements override func visitPost(_ node: ForStmtSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineStatementConditions, + hasMultilinePredecessors(node.body, keyword: node.forKeyword) { + return } - } - override func visitPost(_ node: GuardStmtSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + super.visitPost(node) } override func visitPost(_ node: IfExprSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } - if case let .codeBlock(body) = node.elseBody, let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineStatementConditions, + hasMultilinePredecessors(node.body, keyword: node.ifKeyword) { + return } - } - override func visitPost(_ node: RepeatStmtSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + super.visitPost(node) } override func visitPost(_ node: WhileStmtSyntax) { - let body = node.body - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } - } - - override func visitPost(_ node: SwitchExprSyntax) { - if let correction = node.violationCorrection(locationConverter) { - violations.append(node.openingPosition) - violationCorrections.append(correction) + if configuration.ignoreMultilineStatementConditions, + hasMultilinePredecessors(node.body, keyword: node.whileKeyword) { + return } - } - override func visitPost(_ node: AccessorDeclSyntax) { - if let body = node.body, let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + super.visitPost(node) } - override func visitPost(_ node: PatternBindingSyntax) { - if let body = node.accessorBlock, let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } - } - - override func visitPost(_ node: PrecedenceGroupDeclSyntax) { - if let correction = node.violationCorrection(locationConverter) { - violations.append(node.openingPosition) - violationCorrections.append(correction) - } - } - - override func visitPost(_ node: ClosureExprSyntax) { - guard let parent = node.parent else { - return - } - if parent.is(LabeledExprSyntax.self) { - // Function parameter - return - } - if parent.is(FunctionCallExprSyntax.self) || parent.is(MultipleTrailingClosureElementSyntax.self), - node.keyPathInParent != \FunctionCallExprSyntax.calledExpression, - let correction = node.violationCorrection(locationConverter) { - // Trailing closure - violations.append(node.openingPosition) - violationCorrections.append(correction) - } - } + // MARK: - Functions and Initializers override func visitPost(_ node: FunctionDeclSyntax) { - guard let body = node.body else { - return - } - if configuration.allowMultilineFunc, refersToMultilineFunction(body, functionIndicator: node.funcKeyword) { + if let body = node.body, + configuration.shouldIgnoreMultilineFunctionSignatures, + hasMultilinePredecessors(body, keyword: node.funcKeyword) { return } - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + + super.visitPost(node) } override func visitPost(_ node: InitializerDeclSyntax) { - guard let body = node.body else { - return - } - if configuration.allowMultilineFunc, refersToMultilineFunction(body, functionIndicator: node.initKeyword) { + if let body = node.body, + configuration.shouldIgnoreMultilineFunctionSignatures, + hasMultilinePredecessors(body, keyword: node.initKeyword) { return } - if let correction = body.violationCorrection(locationConverter) { - violations.append(body.openingPosition) - violationCorrections.append(correction) - } + + super.visitPost(node) } - private func refersToMultilineFunction(_ body: CodeBlockSyntax, functionIndicator: TokenSyntax) -> Bool { + // MARK: - Other Methods + + /// Checks if a `BracedSyntax` has a multiline predecessor. + /// For type declarations, the predecessor is the header. For conditional statements, + /// it is the condition list, and for functions, it is the signature. + private func hasMultilinePredecessors(_ body: some BracedSyntax, keyword: TokenSyntax) -> Bool { guard let endToken = body.previousToken(viewMode: .sourceAccurate) else { return false } - let startLocation = functionIndicator.endLocation(converter: locationConverter) + let startLocation = keyword.endLocation(converter: locationConverter) let endLocation = endToken.endLocation(converter: locationConverter) let braceLocation = body.leftBrace.endLocation(converter: locationConverter) return startLocation.line != endLocation.line && endLocation.line != braceLocation.line } - } -} -private extension BracedSyntax { - var openingPosition: AbsolutePosition { - leftBrace.positionAfterSkippingLeadingTrivia - } + override func collectViolations(for bracedItem: (some BracedSyntax)?) { + if let bracedItem, let correction = violationCorrection(bracedItem) { + violations.append( + ReasonedRuleViolation( + position: bracedItem.openingPosition, + reason: """ + Opening braces should be preceded by a single space and on the same line \ + as the declaration + """, + correction: correction + ) + ) + } + } - func violationCorrection(_ locationConverter: SourceLocationConverter) -> ViolationCorrection? { - if let previousToken = leftBrace.previousToken(viewMode: .sourceAccurate) { + private func violationCorrection(_ node: some BracedSyntax) -> ReasonedRuleViolation.ViolationCorrection? { + let leftBrace = node.leftBrace + guard let previousToken = leftBrace.previousToken(viewMode: .sourceAccurate) else { + return nil + } + let openingPosition = node.openingPosition + let triviaBetween = previousToken.trailingTrivia + leftBrace.leadingTrivia let previousLocation = previousToken.endLocation(converter: locationConverter) let leftBraceLocation = leftBrace.startLocation(converter: locationConverter) - if previousLocation.line != leftBraceLocation.line - || previousLocation.column + 1 != leftBraceLocation.column { - return ViolationCorrection( + if previousLocation.line != leftBraceLocation.line { + let trailingCommentText = previousToken.trailingTrivia.description.trimmingCharacters(in: .whitespaces) + return .init( start: previousToken.endPositionBeforeTrailingTrivia, - end: leftBrace.positionAfterSkippingLeadingTrivia, + end: openingPosition.advanced(by: trailingCommentText.isNotEmpty ? 1 : 0), + replacement: trailingCommentText.isNotEmpty ? " { \(trailingCommentText)" : " " + ) + } + if previousLocation.column + 1 == leftBraceLocation.column { + return nil + } + if triviaBetween.containsComments { + if triviaBetween.pieces.last == .spaces(1) { + return nil + } + let comment = triviaBetween.description.trimmingTrailingCharacters(in: .whitespaces) + return .init( + start: previousToken.endPositionBeforeTrailingTrivia + SourceLength(of: comment), + end: openingPosition, replacement: " " ) } + return .init( + start: previousToken.endPositionBeforeTrailingTrivia, + end: openingPosition, + replacement: " " + ) } - return nil + } +} + +private extension BracedSyntax { + var openingPosition: AbsolutePosition { + leftBrace.positionAfterSkippingLeadingTrivia } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRuleExamples.swift index 73b50146bc..d423c231f2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRuleExamples.swift @@ -52,7 +52,12 @@ struct OpeningBraceRuleExamples { Example(""" if c {} else {} - """) + """), + Example(""" + if c /* comment */ { + return + } + """), ] static let triggeringExamples = [ @@ -193,10 +198,10 @@ struct OpeningBraceRuleExamples { let bar: String? = "bar" if - let foooo = foo, - let barrr = bar + let foo = foo, + let bar = bar ↓{ - print(foooo + barrr) + print(foo + bar) } } """), @@ -210,8 +215,8 @@ struct OpeningBraceRuleExamples { """), Example(""" if c ↓{} - else ↓{} - """) + else /* comment */ ↓{} + """), ] static let corrections = [ @@ -531,6 +536,72 @@ struct OpeningBraceRuleExamples { precedencegroup Group { assignment: true } - """) + """), + Example(""" + if c /* comment */ { + return + } + """): Example(""" + if c /* comment */ { + return + } + """), + // https://github.com/realm/SwiftLint/issues/5598 + Example(""" + if c // A comment + { + return + } + """): Example(""" + if c { // A comment + return + } + """), + // https://github.com/realm/SwiftLint/issues/5751 + Example(""" + if c // A comment + { // Another comment + return + } + """): Example(""" + if c { // A comment // Another comment + return + } + """), + // https://github.com/realm/SwiftLint/issues/5751 + Example(""" + func foo() { + if q1, q2 + { + do1() + } else if q3, q4 + { + do2() + } + } + """): Example(""" + func foo() { + if q1, q2 { + do1() + } else if q3, q4 { + do2() + } + } + """), + Example(""" + if + "test".isEmpty + // swiftlint:disable:next opening_brace + { + // code here + } + """): Example(""" + if + "test".isEmpty + // swiftlint:disable:next opening_brace + { + // code here + } + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorFunctionWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorFunctionWhitespaceRule.swift index 9d6df406a6..e2d548888a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorFunctionWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorFunctionWhitespaceRule.swift @@ -12,7 +12,7 @@ struct OperatorFunctionWhitespaceRule: Rule { nonTriggeringExamples: [ Example("func <| (lhs: Int, rhs: Int) -> Int {}"), Example("func <|< (lhs: A, rhs: A) -> A {}"), - Example("func abc(lhs: Int, rhs: Int) -> Int {}") + Example("func abc(lhs: Int, rhs: Int) -> Int {}"), ], triggeringExamples: [ Example("↓func <|(lhs: Int, rhs: Int) -> Int {}"), // no spaces after @@ -20,7 +20,7 @@ struct OperatorFunctionWhitespaceRule: Rule { Example("↓func <| (lhs: Int, rhs: Int) -> Int {}"), // 2 spaces after Example("↓func <|< (lhs: A, rhs: A) -> A {}"), // 2 spaces after Example("↓func <| (lhs: Int, rhs: Int) -> Int {}"), // 2 spaces before - Example("↓func <|< (lhs: A, rhs: A) -> A {}") // 2 spaces before + Example("↓func <|< (lhs: A, rhs: A) -> A {}"), // 2 spaces before ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift index af8818806e..b732349283 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift @@ -16,7 +16,7 @@ struct OperatorUsageWhitespaceRule: OptInRule, CorrectableRule, SourceKitFreeRul ) func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(file: file).map { range, _ in + violationRanges(file: file).map { range, _ in StyleViolation(ruleDescription: Self.description, severity: configuration.severityConfiguration.severity, location: Location(file: file, byteOffset: range.location)) @@ -45,7 +45,7 @@ struct OperatorUsageWhitespaceRule: OptInRule, CorrectableRule, SourceKitFreeRul return (range, correction) } .filter { range, _ in - return file.ruleEnabled(violatingRanges: [range], for: self).isNotEmpty + file.ruleEnabled(violatingRanges: [range], for: self).isNotEmpty } var correctedContents = file.contents @@ -100,7 +100,7 @@ struct OperatorUsageWhitespaceRule: OptInRule, CorrectableRule, SourceKitFreeRul .flatMap { [lineIndex + $0, lineIndex - $0] } func isValidIndex(_ idx: Int) -> Bool { - return idx != lineIndex && idx >= 0 && idx < file.stringView.lines.count + idx != lineIndex && idx >= 0 && idx < file.stringView.lines.count } for lineIndex in lineIndexesAround where isValidIndex(lineIndex) { @@ -214,7 +214,7 @@ private class OperatorUsageWhitespaceVisitor: SyntaxVisitor { private extension Trivia { var containsTooMuchWhitespacing: Bool { - return contains { element in + contains { element in guard case let .spaces(spaces) = element, spaces > 1 else { return false } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift index 432748d683..e6fd2e39f0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift @@ -21,10 +21,10 @@ internal enum OperatorUsageWhitespaceRuleExamples { Example("let foo = SignalProducer, Error>([ self.signal, next ]).flatten(.concat)"), Example("\"let foo = 1\""), Example(""" - enum Enum { - case hello = 1 - case hello2 = 1 - } + enum Enum { + case hello = 1 + case hello2 = 1 + } """), Example(""" let something = Something SyntaxVisitorContinueKind { + override func visit(_: AttributeSyntax) -> SyntaxVisitorContinueKind { if case .skipReferences = variableDeclScopes.peek() { return .skipChildren } @@ -60,16 +60,16 @@ private extension PreferSelfInStaticReferencesRule { return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { + override func visitPost(_: ClassDeclSyntax) { parentDeclScopes.pop() } - override func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind { variableDeclScopes.push(.handleReferences) return .visitChildren } - override func visitPost(_ node: CodeBlockItemListSyntax) { + override func visitPost(_: CodeBlockItemListSyntax) { variableDeclScopes.pop() } @@ -78,16 +78,16 @@ private extension PreferSelfInStaticReferencesRule { return .visitChildren } - override func visitPost(_ node: EnumDeclSyntax) { + override func visitPost(_: EnumDeclSyntax) { parentDeclScopes.pop() } - override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { parentDeclScopes.push(.skipReferences) return .visitChildren } - override func visitPost(_ node: ExtensionDeclSyntax) { + override func visitPost(_: ExtensionDeclSyntax) { parentDeclScopes.pop() } @@ -111,14 +111,14 @@ private extension PreferSelfInStaticReferencesRule { addViolation(on: node.baseName) } - override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: InitializerClauseSyntax) -> SyntaxVisitorContinueKind { if case .skipReferences = variableDeclScopes.peek() { return .skipChildren } return .visitChildren } - override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: MemberBlockSyntax) -> SyntaxVisitorContinueKind { if case .likeClass = parentDeclScopes.peek() { variableDeclScopes.push(.skipReferences) } else { @@ -127,7 +127,7 @@ private extension PreferSelfInStaticReferencesRule { return .visitChildren } - override func visitPost(_ node: MemberBlockSyntax) { + override func visitPost(_: MemberBlockSyntax) { variableDeclScopes.pop() } @@ -138,23 +138,23 @@ private extension PreferSelfInStaticReferencesRule { return .skipChildren } - override func visit(_ node: FunctionParameterClauseSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: FunctionParameterClauseSyntax) -> SyntaxVisitorContinueKind { if case .likeStruct = parentDeclScopes.peek() { return .visitChildren } return .skipChildren } - override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { parentDeclScopes.push(.skipReferences) return .skipChildren } - override func visitPost(_ node: ProtocolDeclSyntax) { + override func visitPost(_: ProtocolDeclSyntax) { parentDeclScopes.pop() } - override func visit(_ node: ReturnClauseSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ReturnClauseSyntax) -> SyntaxVisitorContinueKind { if case .likeStruct = parentDeclScopes.peek() { return .visitChildren } @@ -166,11 +166,11 @@ private extension PreferSelfInStaticReferencesRule { return .visitChildren } - override func visitPost(_ node: StructDeclSyntax) { + override func visitPost(_: StructDeclSyntax) { parentDeclScopes.pop() } - override func visit(_ node: GenericArgumentListSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: GenericArgumentListSyntax) -> SyntaxVisitorContinueKind { if case .likeClass = parentDeclScopes.peek() { return .skipChildren } @@ -191,7 +191,7 @@ private extension PreferSelfInStaticReferencesRule { } } - override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind { if case .likeClass = parentDeclScopes.peek() { return .skipChildren } @@ -214,9 +214,9 @@ private extension PreferSelfInStaticReferencesRule { private func addViolation(on node: TokenSyntax) { if let parentName = parentDeclScopes.peek()?.parentName, node.tokenKind == .identifier(parentName) { - violations.append(node.positionAfterSkippingLeadingTrivia) - violationCorrections.append( - ViolationCorrection( + violations.append( + at: node.positionAfterSkippingLeadingTrivia, + correction: .init( start: node.positionAfterSkippingLeadingTrivia, end: node.endPositionBeforeTrailingTrivia, replacement: "Self" diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfInStaticReferencesRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfInStaticReferencesRuleExamples.swift index bb4602393c..668e3e5627 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfInStaticReferencesRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfInStaticReferencesRuleExamples.swift @@ -4,7 +4,7 @@ enum PreferSelfInStaticReferencesRuleExamples { class C { static let primes = [2, 3, 5, 7] func isPrime(i: Int) -> Bool { Self.primes.contains(i) } - """), + """), Example(""" struct T { static let i = 0 @@ -16,13 +16,13 @@ enum PreferSelfInStaticReferencesRuleExamples { static let j = S.i + T.i static let k = { T.j }() } - """), + """), Example(""" class `Self` { static let i = 0 func f() -> Int { Self.i } } - """), + """), Example(""" class C { static private(set) var i = 0, j = C.i @@ -31,7 +31,7 @@ enum PreferSelfInStaticReferencesRuleExamples { var n: Int = C.k { didSet { m += 1 } } @GreaterThan(C.j) var m: Int } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" struct S { struct T { @@ -45,7 +45,7 @@ enum PreferSelfInStaticReferencesRuleExamples { static let j = Self.T.R.i + Self.R.j let h = Self.T.R.i + Self.R.j } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class C { static let s = 2 @@ -55,29 +55,29 @@ enum PreferSelfInStaticReferencesRuleExamples { } func g() -> Any { C.self } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" struct Record { static func get() -> Record { Record() } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" @objc class C: NSObject { @objc var s = "" @objc func f() { _ = #keyPath(C.s) } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class C { let i = 1 let c: C = C() func f(c: C) -> KeyPath { \\Self.i } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class C1 {} class C2: C1 {} - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class C1 {} class C2: C1 { @@ -91,7 +91,7 @@ enum PreferSelfInStaticReferencesRuleExamples { class C4 {} } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class S1 { class S2 {} @@ -100,21 +100,21 @@ enum PreferSelfInStaticReferencesRuleExamples { let s2 = S1() } } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] static let triggeringExamples = [ Example(""" - final class CheckCellView: NSTableCellView { - @IBOutlet var checkButton: NSButton! + final class CheckCellView: NSTableCellView { + @IBOutlet var checkButton: NSButton! - override func awakeFromNib() { - checkButton.action = #selector(↓CheckCellView.check(_:)) - } + override func awakeFromNib() { + checkButton.action = #selector(↓CheckCellView.check(_:)) + } - @objc func check(_ button: AnyObject?) {} - } - """), + @objc func check(_ button: AnyObject?) {} + } + """), Example(""" class C { static let i = 1 @@ -123,7 +123,7 @@ enum PreferSelfInStaticReferencesRuleExamples { return ii } } - """), + """), Example(""" class C { func f() { @@ -131,7 +131,7 @@ enum PreferSelfInStaticReferencesRuleExamples { _ = [Int: ↓C]() } } - """), + """), Example(""" struct S { let j: Int @@ -142,7 +142,7 @@ enum PreferSelfInStaticReferencesRuleExamples { func i() -> KeyPath<↓S, Int> { \\↓S.j } func j(@Wrap(-↓S.i, ↓S.i) n: Int = ↓S.i) {} } - """), + """), Example(""" struct S { struct T { @@ -153,14 +153,14 @@ enum PreferSelfInStaticReferencesRuleExamples { } static let h = ↓S.T.i + ↓S.R.j } - """), + """), Example(""" enum E { case A static func f() -> ↓E { ↓E.A } static func g() -> ↓E { ↓E.f() } } - """), + """), Example(""" extension E { class C { @@ -176,7 +176,7 @@ enum PreferSelfInStaticReferencesRuleExamples { } } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class C { typealias A = C @@ -214,7 +214,7 @@ enum PreferSelfInStaticReferencesRuleExamples { } func g(a: [↓S]) -> [↓S] { a } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class T { let child: T @@ -222,32 +222,32 @@ enum PreferSelfInStaticReferencesRuleExamples { child = (input as! T).child } } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ] static let corrections = [ Example(""" - final class CheckCellView: NSTableCellView { - @IBOutlet var checkButton: NSButton! + final class CheckCellView: NSTableCellView { + @IBOutlet var checkButton: NSButton! - override func awakeFromNib() { - checkButton.action = #selector(↓CheckCellView.check(_:)) - } + override func awakeFromNib() { + checkButton.action = #selector(↓CheckCellView.check(_:)) + } - @objc func check(_ button: AnyObject?) {} - } - """): + @objc func check(_ button: AnyObject?) {} + } + """): Example(""" - final class CheckCellView: NSTableCellView { - @IBOutlet var checkButton: NSButton! + final class CheckCellView: NSTableCellView { + @IBOutlet var checkButton: NSButton! - override func awakeFromNib() { - checkButton.action = #selector(Self.check(_:)) - } + override func awakeFromNib() { + checkButton.action = #selector(Self.check(_:)) + } - @objc func check(_ button: AnyObject?) {} - } - """), + @objc func check(_ button: AnyObject?) {} + } + """), Example(""" struct S { static let i = 1 @@ -256,14 +256,14 @@ enum PreferSelfInStaticReferencesRuleExamples { static func f(_ l: Int = ↓S.i) -> Int { l*↓S.j } func g() { ↓S.i + ↓S.f() + k } } - """): Example(""" - struct S { - static let i = 1 - static let j = Self.i - let k = Self . j - static func f(_ l: Int = Self.i) -> Int { l*Self.j } - func g() { Self.i + Self.f() + k } - } - """) + """): Example(""" + struct S { + static let i = 1 + static let j = Self.i + let k = Self . j + static func f(_ l: Int = Self.i) -> Int { l*Self.j } + func g() { Self.i + Self.f() + k } + } + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift index eb5be49558..6266949b9d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift @@ -38,7 +38,7 @@ struct PreferSelfTypeOverTypeOfSelfRule: OptInRule { print(type(of: self)) } } - """) + """), ], triggeringExamples: [ Example(""" @@ -61,7 +61,7 @@ struct PreferSelfTypeOverTypeOfSelfRule: OptInRule { print(↓Swift.type(of: self).baz) } } - """) + """), ], corrections: [ Example(""" @@ -102,7 +102,7 @@ struct PreferSelfTypeOverTypeOfSelfRule: OptInRule { print(Self.baz) } } - """) + """), ] ) } @@ -135,7 +135,7 @@ private extension PreferSelfTypeOverTypeOfSelfRule { private extension FunctionCallExprSyntax { var hasViolation: Bool { - return isTypeOfSelfCall && + isTypeOfSelfCall && arguments.map(\.label?.text) == ["of"] && arguments.first?.expression.as(DeclReferenceExprSyntax.self)?.baseName.tokenKind == .keyword(.self) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/PrefixedTopLevelConstantRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/PrefixedTopLevelConstantRule.swift index 7e4050a6f3..74e10f47f1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/PrefixedTopLevelConstantRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/PrefixedTopLevelConstantRule.swift @@ -60,7 +60,7 @@ struct PrefixedTopLevelConstantRule: OptInRule { print("\(number) is a small number") } } - """#) + """#), ], triggeringExamples: [ Example("private let ↓Foo = 20.0"), @@ -76,7 +76,7 @@ struct PrefixedTopLevelConstantRule: OptInRule { let ↓foo = { return a + b }() - """) + """), ] ) } @@ -106,11 +106,11 @@ private extension PrefixedTopLevelConstantRule { } } - override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind { .skipChildren } - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ProtocolPropertyAccessorsOrderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ProtocolPropertyAccessorsOrderRule.swift index dbcbb289de..22b161f535 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ProtocolPropertyAccessorsOrderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ProtocolPropertyAccessorsOrderRule.swift @@ -12,14 +12,14 @@ struct ProtocolPropertyAccessorsOrderRule: Rule { nonTriggeringExamples: [ Example("protocol Foo {\n var bar: String { get set }\n }"), Example("protocol Foo {\n var bar: String { get }\n }"), - Example("protocol Foo {\n var bar: String { set }\n }") + Example("protocol Foo {\n var bar: String { set }\n }"), ], triggeringExamples: [ Example("protocol Foo {\n var bar: String { ↓set get }\n }") ], corrections: [ Example("protocol Foo {\n var bar: String { ↓set get }\n }"): - Example("protocol Foo {\n var bar: String { get set }\n }") + Example("protocol Foo {\n var bar: String { get set }\n }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift index 149a67976a..76904825a0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift @@ -15,15 +15,15 @@ struct RedundantDiscardableLetRule: Rule { Example("guard let _ = foo() else { return }"), Example("let _: ExplicitType = foo()"), Example("while let _ = SplashStyle(rawValue: maxValue) { maxValue += 1 }"), - Example("async let _ = await foo()") + Example("async let _ = await foo()"), ], triggeringExamples: [ Example("↓let _ = foo()"), - Example("if _ = foo() { ↓let _ = bar() }") + Example("if _ = foo() { ↓let _ = bar() }"), ], corrections: [ Example("↓let _ = foo()"): Example("_ = foo()"), - Example("if _ = foo() { ↓let _ = bar() }"): Example("if _ = foo() { _ = bar() }") + Example("if _ = foo() { ↓let _ = bar() }"): Example("if _ = foo() { _ = bar() }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRule.swift index 78f7db239b..17dde72a66 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRule.swift @@ -39,21 +39,21 @@ private extension RedundantSelfInClosureRule { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] } - override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ActorDeclSyntax) -> SyntaxVisitorContinueKind { typeDeclarations.push(.likeClass) return .visitChildren } - override func visitPost(_ node: ActorDeclSyntax) { + override func visitPost(_: ActorDeclSyntax) { typeDeclarations.pop() } - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClassDeclSyntax) -> SyntaxVisitorContinueKind { typeDeclarations.push(.likeClass) return .visitChildren } - override func visitPost(_ node: ClassDeclSyntax) { + override func visitPost(_: ClassDeclSyntax) { typeDeclarations.pop() } @@ -79,18 +79,17 @@ private extension RedundantSelfInClosureRule { functionCallType: activeFunctionCallType, selfCaptureKind: activeSelfCaptureKind, scope: scope - ).walk(tree: node.statements, handler: \.violationCorrections) - violations.append(contentsOf: localViolationCorrections.map(\.start)) - violationCorrections.append(contentsOf: localViolationCorrections) + ).walk(tree: node.statements, handler: \.violations) + violations.append(contentsOf: localViolationCorrections) selfCaptures.pop() } - override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: EnumDeclSyntax) -> SyntaxVisitorContinueKind { typeDeclarations.push(.likeStruct) return .visitChildren } - override func visitPost(_ node: EnumDeclSyntax) { + override func visitPost(_: EnumDeclSyntax) { typeDeclarations.pop() } @@ -103,16 +102,16 @@ private extension RedundantSelfInClosureRule { return .visitChildren } - override func visitPost(_ node: FunctionCallExprSyntax) { + override func visitPost(_: FunctionCallExprSyntax) { functionCalls.pop() } - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: StructDeclSyntax) -> SyntaxVisitorContinueKind { typeDeclarations.push(.likeStruct) return .visitChildren } - override func visitPost(_ node: StructDeclSyntax) { + override func visitPost(_: StructDeclSyntax) { typeDeclarations.pop() } } @@ -137,8 +136,9 @@ private class ExplicitSelfVisitor: DeclaredIde override func visitPost(_ node: MemberAccessExprSyntax) { if !hasSeenDeclaration(for: node.declName.baseName.text), node.isBaseSelf, isSelfRedundant { - violationCorrections.append( - ViolationCorrection( + violations.append( + at: node.positionAfterSkippingLeadingTrivia, + correction: .init( start: node.positionAfterSkippingLeadingTrivia, end: node.period.endPositionBeforeTrailingTrivia, replacement: "" @@ -147,7 +147,7 @@ private class ExplicitSelfVisitor: DeclaredIde } } - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { // Will be handled separately by the parent visitor. .skipChildren } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRuleExamples.swift index 21ab61ea6d..e9924e66dd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantSelfInClosureRuleExamples.swift @@ -12,7 +12,7 @@ struct RedundantSelfInClosureRuleExamples { } } } - """), + """), Example(""" class C { var x = 0 @@ -28,7 +28,7 @@ struct RedundantSelfInClosureRuleExamples { f { [weak self] in if let self { x = 1 } } } } - """), + """), Example(""" struct S { var x = 0, error = 0, exception = 0 @@ -58,7 +58,7 @@ struct RedundantSelfInClosureRuleExamples { } } } - """), + """), Example(""" enum E { case a(Int) @@ -79,7 +79,20 @@ struct RedundantSelfInClosureRuleExamples { } } } - """) + """), + Example(""" + class C { + var a = 0 + init(_ a: Int) { + self.a = a + f { [weak self] in + guard let self else { return } + self.a = 1 + } + } + func f(_: () -> Void) {} + } + """, excludeFromDocumentation: true), ] static let triggeringExamples = [ @@ -94,7 +107,7 @@ struct RedundantSelfInClosureRuleExamples { } } } - """), + """), Example(""" class C { var x = 0 @@ -105,7 +118,7 @@ struct RedundantSelfInClosureRuleExamples { }() } } - """), + """), Example(""" class C { var x = 0 @@ -118,7 +131,7 @@ struct RedundantSelfInClosureRuleExamples { } } } - """), + """), Example(""" class C { var x = 0 @@ -129,7 +142,7 @@ struct RedundantSelfInClosureRuleExamples { f { [s = self] in s.x = 1 } } } - """), + """), Example(""" struct S { var x = 0 @@ -158,7 +171,7 @@ struct RedundantSelfInClosureRuleExamples { } } } - """), + """), Example(""" struct S { func f(_ work: @escaping () -> Void) { work() } @@ -166,7 +179,7 @@ struct RedundantSelfInClosureRuleExamples { f { let g = ↓self.g() } } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" extension E { static func f(_ work: @escaping () -> Void) { work() } @@ -175,7 +188,7 @@ struct RedundantSelfInClosureRuleExamples { func g() { E.f { ↓self.g() } } } } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" class C { var x = 0 @@ -198,7 +211,7 @@ struct RedundantSelfInClosureRuleExamples { } } } - """) + """), ] static let corrections = [ @@ -213,17 +226,17 @@ struct RedundantSelfInClosureRuleExamples { } } } - """): Example(""" - struct S { - var x = 0 - func f(_ work: @escaping () -> Void) { work() } - func g() { - f { - x = 1 - if x == 1 { g() } + """): Example(""" + struct S { + var x = 0 + func f(_ work: @escaping () -> Void) { work() } + func g() { + f { + x = 1 + if x == 1 { g() } + } } } - } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift index 31f55ce9bd..7248f2ff24 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift @@ -23,7 +23,7 @@ struct ReturnArrowWhitespaceRule: SwiftSyntaxCorrectableRule { return 1 } """), - Example("typealias SuccessBlock = ((Data) -> Void)") + Example("typealias SuccessBlock = ((Data) -> Void)"), ], triggeringExamples: [ Example("func abc()↓->Int {}"), @@ -39,7 +39,7 @@ struct ReturnArrowWhitespaceRule: SwiftSyntaxCorrectableRule { Example("func abc()\n ↓-> Int {}"), Example("func abc()\n ↓-> Int {}"), Example("func abc()↓ ->\n Int {}"), - Example("func abc()↓ ->\nInt {}") + Example("func abc()↓ ->\nInt {}"), ], corrections: [ Example("func abc()↓->Int {}"): Example("func abc() -> Int {}"), @@ -49,7 +49,7 @@ struct ReturnArrowWhitespaceRule: SwiftSyntaxCorrectableRule { Example("func abc()\n ↓-> Int {}"): Example("func abc()\n -> Int {}"), Example("func abc()\n ↓-> Int {}"): Example("func abc()\n -> Int {}"), Example("func abc()↓ ->\n Int {}"): Example("func abc() ->\n Int {}"), - Example("func abc()↓ ->\nInt {}"): Example("func abc() ->\nInt {}") + Example("func abc()↓ ->\nInt {}"): Example("func abc() ->\nInt {}"), ] ) } @@ -57,36 +57,27 @@ struct ReturnArrowWhitespaceRule: SwiftSyntaxCorrectableRule { private extension ReturnArrowWhitespaceRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: FunctionTypeSyntax) { - guard let violation = node.returnClause.arrow.arrowViolation else { - return + if let violation = node.returnClause.arrow.arrowViolation { + violations.append(violation) } - - violations.append(violation.start) - violationCorrections.append(violation) } override func visitPost(_ node: FunctionSignatureSyntax) { - guard let output = node.returnClause, let violation = output.arrow.arrowViolation else { - return + if let output = node.returnClause, let violation = output.arrow.arrowViolation { + violations.append(violation) } - - violations.append(violation.start) - violationCorrections.append(violation) } override func visitPost(_ node: ClosureSignatureSyntax) { - guard let output = node.returnClause, let violation = output.arrow.arrowViolation else { - return + if let output = node.returnClause, let violation = output.arrow.arrowViolation { + violations.append(violation) } - - violations.append(violation.start) - violationCorrections.append(violation) } } } private extension TokenSyntax { - var arrowViolation: ViolationCorrection? { + var arrowViolation: ReasonedRuleViolation? { guard let previousToken = previousToken(viewMode: .sourceAccurate), let nextToken = nextToken(viewMode: .sourceAccurate) else { return nil @@ -119,6 +110,6 @@ private extension TokenSyntax { return nil } - return ViolationCorrection(start: start, end: end, replacement: correction) + return .init(position: start, correction: .init(start: start, end: end, replacement: correction)) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift index 2c666742d8..25e2672917 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift @@ -17,7 +17,7 @@ struct SelfBindingRule: OptInRule { Example("if let this = this { return }"), Example("guard let this = this else { return }"), Example("if let this = self { return }", configuration: ["bind_identifier": "this"]), - Example("guard let this = self else { return }", configuration: ["bind_identifier": "this"]) + Example("guard let this = self else { return }", configuration: ["bind_identifier": "this"]), ], triggeringExamples: [ Example("if let ↓`self` = self { return }"), @@ -27,7 +27,7 @@ struct SelfBindingRule: OptInRule { Example("if let ↓self = self { return }", configuration: ["bind_identifier": "this"]), Example("guard let ↓self = self else { return }", configuration: ["bind_identifier": "this"]), Example("if let ↓self { return }", configuration: ["bind_identifier": "this"]), - Example("guard let ↓self else { return }", configuration: ["bind_identifier": "this"]) + Example("guard let ↓self else { return }", configuration: ["bind_identifier": "this"]), ], corrections: [ Example("if let ↓`self` = self { return }"): @@ -43,7 +43,7 @@ struct SelfBindingRule: OptInRule { Example("if let ↓self { return }", configuration: ["bind_identifier": "this"]): Example("if let this = self { return }", configuration: ["bind_identifier": "this"]), Example("guard let ↓self else { return }", configuration: ["bind_identifier": "this"]): - Example("guard let this = self else { return }", configuration: ["bind_identifier": "this"]) + Example("guard let this = self else { return }", configuration: ["bind_identifier": "this"]), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift index 1bb1df43ac..f8068dafd2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift @@ -17,22 +17,22 @@ struct ShorthandArgumentRule: OptInRule { nonTriggeringExamples: [ Example(""" f { $0 } - """), + """), Example(""" f { $0 + $1 + $2 } - """), + """), Example(""" f { $0.a + $0.b } - """), + """), Example(""" f { $0 + g { $0 } - """, configuration: ["allow_until_line_after_opening_brace": 1]) + """, configuration: ["allow_until_line_after_opening_brace": 1]), ], triggeringExamples: [ Example(""" @@ -43,7 +43,7 @@ struct ShorthandArgumentRule: OptInRule { + ↓$0 } - """), + """), Example(""" f { $0 @@ -53,18 +53,18 @@ struct ShorthandArgumentRule: OptInRule { + $0 + ↓$1 } - """, configuration: ["allow_until_line_after_opening_brace": 5]), + """, configuration: ["allow_until_line_after_opening_brace": 5]), Example(""" f { ↓$0 + ↓$1 } - """, configuration: ["always_disallow_more_than_one": true]), + """, configuration: ["always_disallow_more_than_one": true]), Example(""" f { ↓$0.a + ↓$0.b + $1 + ↓$2.c - } - """, configuration: ["always_disallow_member_access": true, "allow_until_line_after_opening_brace": 3]) + } + """, configuration: ["always_disallow_member_access": true, "allow_until_line_after_opening_brace": 3]), ] ) } @@ -145,7 +145,7 @@ private final class ShorthandArgumentCollector: SyntaxVisitor { } } - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind { .skipChildren } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandOperatorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandOperatorRule.swift index ead260cfb9..7912bf25a5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandOperatorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandOperatorRule.swift @@ -25,7 +25,7 @@ struct ShorthandOperatorRule: Rule { """), Example("var helloWorld = \"world!\"\n helloWorld = \"Hello, \" + helloWorld"), Example("angle = someCheck ? angle : -angle"), - Example("seconds = seconds * 60 + value") + Example("seconds = seconds * 60 + value"), ], triggeringExamples: [ Example("↓foo = foo * 1"), @@ -34,7 +34,7 @@ struct ShorthandOperatorRule: Rule { Example("↓foo.aProperty = foo.aProperty - 1"), Example("↓self.aProperty = self.aProperty * 1"), Example("↓n = n + i / outputLength"), - Example("↓n = n - i / outputLength") + Example("↓n = n - i / outputLength"), ] ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SingleTestClassRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SingleTestClassRule.swift index 906b91e92c..b21194b5dd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SingleTestClassRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SingleTestClassRule.swift @@ -11,7 +11,7 @@ struct SingleTestClassRule: SourceKitFreeRule, OptInRule { nonTriggeringExamples: [ Example("class FooTests { }"), Example("class FooTests: QuickSpec { }"), - Example("class FooTests: XCTestCase { }") + Example("class FooTests: XCTestCase { }"), ], triggeringExamples: [ Example(""" @@ -45,7 +45,7 @@ struct SingleTestClassRule: SourceKitFreeRule, OptInRule { final ↓class FooTests: QuickSpec { } ↓class BarTests: XCTestCase { } class TotoTests { } - """) + """), ] ) @@ -56,10 +56,10 @@ struct SingleTestClassRule: SourceKitFreeRule, OptInRule { guard classes.count > 1 else { return [] } return classes.map { position in - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, position: position.position), - reason: "\(classes.count) test classes found in this file") + StyleViolation(ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, position: position.position), + reason: "\(classes.count) test classes found in this file") } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SortedEnumCasesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SortedEnumCasesRule.swift index a78b926c46..31c8b3b087 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SortedEnumCasesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SortedEnumCasesRule.swift @@ -60,7 +60,7 @@ struct SortedEnumCasesRule: OptInRule { case a case c, f, d } - """) + """), ], triggeringExamples: [ Example(""" @@ -104,7 +104,7 @@ struct SortedEnumCasesRule: OptInRule { case a(foo: Foo) case ↓c, ↓b(String) } - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift index 22cfd90478..d0f0b16506 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift @@ -3,19 +3,19 @@ import SourceKittenFramework fileprivate extension Line { var contentRange: NSRange { - return NSRange(location: range.location, length: content.bridge().length) + NSRange(location: range.location, length: content.bridge().length) } // `Line` in this rule always contains word import // This method returns contents of line that are before import func importAttributes() -> String { - return content[importAttributesRange()].trimmingCharacters(in: .whitespaces) + content[importAttributesRange()].trimmingCharacters(in: .whitespaces) } // `Line` in this rule always contains word import // This method returns contents of line that are after import func importModule() -> Substring { - return content[importModuleRange()] + content[importModuleRange()] } func importAttributesRange() -> Range { @@ -37,7 +37,7 @@ private extension Sequence where Element == Line { // Groups lines, so that lines that are one after the other // will end up in same group. func grouped() -> [[Line]] { - return reduce(into: [[]]) { result, line in + reduce(into: [[]]) { result, line in guard let last = result.last?.last else { result = [[line]] return @@ -93,8 +93,8 @@ struct SortedImportsRule: CorrectableRule, OptInRule { } private func violatingOffsets(inGroups groups: [[Line]]) -> [Int] { - return groups.flatMap { group in - return zip(group, group.dropFirst()).reduce(into: []) { violatingOffsets, groupPair in + groups.flatMap { group in + zip(group, group.dropFirst()).reduce(into: []) { violatingOffsets, groupPair in let (previous, current) = groupPair let isOrderedCorrectly = should(previous, comeBefore: current) if isOrderedCorrectly { @@ -142,9 +142,9 @@ struct SortedImportsRule: CorrectableRule, OptInRule { guard let first = group.first?.contentRange else { continue } - let result = group.map { $0.content }.joined(separator: "\n") + let result = group.map(\.content).joined(separator: "\n") let union = group.dropFirst().reduce(first) { result, line in - return NSUnionRange(result, line.contentRange) + NSUnionRange(result, line.contentRange) } correctedContents.replaceCharacters(in: union, with: result) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRuleExamples.swift index d7940bc44c..d245b15af8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRuleExamples.swift @@ -58,7 +58,7 @@ internal struct SortedImportsRuleExamples { Example(""" @_exported @testable import BBB import AAA - """, configuration: groupByAttributesConfiguration, excludeFromDocumentation: true) + """, configuration: groupByAttributesConfiguration, excludeFromDocumentation: true), ] static let triggeringExamples = [ @@ -114,7 +114,7 @@ internal struct SortedImportsRuleExamples { Example(""" import AAA @_exported @testable import ↓BBB - """, configuration: groupByAttributesConfiguration, excludeFromDocumentation: true) + """, configuration: groupByAttributesConfiguration, excludeFromDocumentation: true), ] static let corrections = [ @@ -228,6 +228,6 @@ internal struct SortedImportsRuleExamples { Example(""" @_exported @testable import BBB import AAA - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift index 960e81365a..9a99e7a276 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift @@ -15,18 +15,18 @@ struct StatementPositionRule: CorrectableRule { Example("} catch {"), Example("\"}else{\""), Example("struct A { let catchphrase: Int }\nlet a = A(\n catchphrase: 0\n)"), - Example("struct A { let `catch`: Int }\nlet a = A(\n `catch`: 0\n)") + Example("struct A { let `catch`: Int }\nlet a = A(\n `catch`: 0\n)"), ], triggeringExamples: [ Example("↓}else if {"), Example("↓} else {"), Example("↓}\ncatch {"), - Example("↓}\n\t catch {") + Example("↓}\n\t catch {"), ], corrections: [ Example("↓}\n else {"): Example("} else {"), Example("↓}\n else if {"): Example("} else if {"), - Example("↓}\n catch {"): Example("} catch {") + Example("↓}\n catch {"): Example("} catch {"), ] ) @@ -44,19 +44,19 @@ struct StatementPositionRule: CorrectableRule { Example("\n\n }\n catch {"), Example("\"}\nelse{\""), Example("struct A { let catchphrase: Int }\nlet a = A(\n catchphrase: 0\n)"), - Example("struct A { let `catch`: Int }\nlet a = A(\n `catch`: 0\n)") + Example("struct A { let `catch`: Int }\nlet a = A(\n `catch`: 0\n)"), ], triggeringExamples: [ Example("↓ }else if {"), Example("↓}\n else {"), Example("↓ }\ncatch {"), - Example("↓}\n\t catch {") + Example("↓}\n\t catch {"), ], corrections: [ Example(" }else if {"): Example(" }\n else if {"), Example("}\n else {"): Example("}\nelse {"), Example(" }\ncatch {"): Example(" }\n catch {"), - Example("}\n\t catch {"): Example("}\ncatch {") + Example("}\n\t catch {"): Example("}\ncatch {"), ] ) @@ -87,7 +87,7 @@ private extension StatementPositionRule { static let defaultPattern = "\\}(?:[\\s\\n\\r]{2,}|[\\n\\t\\r]+)?\\b(else|catch)\\b" func defaultValidate(file: SwiftLintFile) -> [StyleViolation] { - return defaultViolationRanges(in: file, matching: Self.defaultPattern).compactMap { range in + defaultViolationRanges(in: file, matching: Self.defaultPattern).compactMap { range in StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, characterOffset: range.location)) @@ -95,8 +95,8 @@ private extension StatementPositionRule { } func defaultViolationRanges(in file: SwiftLintFile, matching pattern: String) -> [NSRange] { - return file.match(pattern: pattern).filter { _, syntaxKinds in - return syntaxKinds.starts(with: [.keyword]) + file.match(pattern: pattern).filter { _, syntaxKinds in + syntaxKinds.starts(with: [.keyword]) }.compactMap { $0.0 } } @@ -122,7 +122,7 @@ private extension StatementPositionRule { // Uncuddled Behaviors private extension StatementPositionRule { func uncuddledValidate(file: SwiftLintFile) -> [StyleViolation] { - return uncuddledViolationRanges(in: file).compactMap { range in + uncuddledViolationRanges(in: file).compactMap { range in StyleViolation(ruleDescription: Self.uncuddledDescription, severity: configuration.severity, location: Location(file: file, characterOffset: range.location)) @@ -139,7 +139,7 @@ private extension StatementPositionRule { static func uncuddledMatchValidator(contents: StringView) -> ((NSTextCheckingResult) -> NSTextCheckingResult?) { - return { match in + { match in if match.numberOfRanges != 5 { return match } @@ -159,7 +159,7 @@ private extension StatementPositionRule { static func uncuddledMatchFilter(contents: StringView, syntaxMap: SwiftLintSyntaxMap) -> ((NSTextCheckingResult) -> Bool) { - return { match in + { match in let range = match.range guard let matchRange = contents.NSRangeToByteRange(start: range.location, length: range.length) else { @@ -176,7 +176,7 @@ private extension StatementPositionRule { let validator = Self.uncuddledMatchValidator(contents: contents) let filterMatches = Self.uncuddledMatchFilter(contents: contents, syntaxMap: syntaxMap) - return matches.compactMap(validator).filter(filterMatches).map({ $0.range }) + return matches.compactMap(validator).filter(filterMatches).map(\.range) } func uncuddledCorrect(file: SwiftLintFile) -> [Correction] { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SuperfluousElseRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SuperfluousElseRule.swift index 08b79c7d0f..d15ddd7322 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SuperfluousElseRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SuperfluousElseRule.swift @@ -19,7 +19,7 @@ struct SuperfluousElseRule: OptInRule { } else { return 3 } - """), + """), Example(""" if i > 0 { let a = 1 @@ -32,7 +32,7 @@ struct SuperfluousElseRule: OptInRule { } else { return 3 } - """), + """), Example(""" if i > 0 { if a > 1 { @@ -41,7 +41,7 @@ struct SuperfluousElseRule: OptInRule { } else { return 3 } - """), + """), Example(""" if i > 0 { if a > 1 { @@ -54,7 +54,7 @@ struct SuperfluousElseRule: OptInRule { } else { return 3 } - """, excludeFromDocumentation: true), + """, excludeFromDocumentation: true), Example(""" for i in list { if i > 12 { @@ -68,7 +68,14 @@ struct SuperfluousElseRule: OptInRule { break } } - """) + """), + Example(""" + if #available(iOS 13, *) { + return + } else { + deprecatedFunction() + } + """), ], triggeringExamples: [ Example(""" @@ -78,7 +85,7 @@ struct SuperfluousElseRule: OptInRule { } ↓else { return 2 } - """), + """), Example(""" if i > 0 { return 1 @@ -87,7 +94,7 @@ struct SuperfluousElseRule: OptInRule { } ↓else if i > 18 { return 3 } - """), + """), Example(""" if i > 0 { if i < 12 { @@ -106,7 +113,7 @@ struct SuperfluousElseRule: OptInRule { } ↓else { return 3 } - """), + """), Example(""" for i in list { if i > 13 { @@ -119,7 +126,7 @@ struct SuperfluousElseRule: OptInRule { throw error } } - """) + """), ], corrections: [ Example(""" @@ -133,17 +140,17 @@ struct SuperfluousElseRule: OptInRule { // yet another comment } } - """): Example(""" - func f() -> Int { - if i > 0 { - return 1 - // comment + """): Example(""" + func f() -> Int { + if i > 0 { + return 1 + // comment + } + // another comment + return 2 + // yet another comment } - // another comment - return 2 - // yet another comment - } - """), + """), Example(""" func f() -> Int { if i > 0 { @@ -155,18 +162,18 @@ struct SuperfluousElseRule: OptInRule { return 3 } } - """): Example(""" - func f() -> Int { - if i > 0 { - return 1 - // comment - } - if i < 10 { - return 2 + """): Example(""" + func f() -> Int { + if i > 0 { + return 1 + // comment + } + if i < 10 { + return 2 + } + return 3 } - return 3 - } - """), + """), Example(""" func f() -> Int { @@ -178,19 +185,19 @@ struct SuperfluousElseRule: OptInRule { return 2 } } - """): Example(""" - func f() -> Int { + """): Example(""" + func f() -> Int { - if i > 0 { - return 1 - // comment - } - if i < 10 { - // another comment - return 2 + if i > 0 { + return 1 + // comment + } + if i < 10 { + // another comment + return 2 + } } - } - """), + """), Example(""" { if i > 0 { @@ -199,14 +206,14 @@ struct SuperfluousElseRule: OptInRule { return 2 } }() - """): Example(""" - { - if i > 0 { - return 1 - } - return 2 - }() - """), + """): Example(""" + { + if i > 0 { + return 1 + } + return 2 + }() + """), Example(""" for i in list { if i > 13 { @@ -223,24 +230,24 @@ struct SuperfluousElseRule: OptInRule { } } - """): Example(""" - for i in list { - if i > 13 { - return - } - if i > 12 { - continue // continue with next index - } - if i > 11 { - break - // end of loop - } - if i > 10 { - // Some error - throw error + """): Example(""" + for i in list { + if i > 13 { + return + } + if i > 12 { + continue // continue with next index + } + if i > 11 { + break + // end of loop + } + if i > 10 { + // Some error + throw error + } } - } - """) + """), ] ) } @@ -324,10 +331,9 @@ private extension SuperfluousElseRule { private extension IfExprSyntax { var superfluousElse: TokenSyntax? { - if elseKeyword == nil { - return nil - } - if !lastStatementExitsScope(in: body) { + guard elseKeyword != nil, + conditions.onlyElement?.condition.is(AvailabilityConditionSyntax.self) != true, + lastStatementExitsScope(in: body) else { return nil } if let parent = parent?.as(IfExprSyntax.self) { @@ -350,7 +356,7 @@ private extension IfExprSyntax { } private func lastStatementExitsScope(in block: CodeBlockSyntax) -> Bool { - guard let lastItem = block.statements.last?.as(CodeBlockItemSyntax.self)?.item else { + guard let lastItem = block.statements.last?.item else { return false } if [.returnStmt, .throwStmt, .continueStmt, .breakStmt].contains(lastItem.kind) { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift index 1e509776a3..46592dd7a6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift @@ -34,7 +34,7 @@ struct SwitchCaseAlignmentRule: Rule { } } } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: Examples(indentedCases: false).triggeringExamples ) @@ -93,14 +93,13 @@ extension SwitchCaseAlignmentRule { } var triggeringExamples: [Example] { - return (indentedCasesOption ? nonIndentedCases : indentedCases) + (indentedCasesOption ? nonIndentedCases : indentedCases) + invalidCases + invalidOneLiners } var nonTriggeringExamples: [Example] { - return indentedCasesOption ? indentedCases : nonIndentedCases - + validOneLiners + indentedCasesOption ? indentedCases : nonIndentedCases + validOneLiners } private var indentedCases: [Example] { @@ -140,7 +139,7 @@ extension SwitchCaseAlignmentRule { \(violationMarker)case 1: 1 \(violationMarker)default: 2 } - """) + """), ] } @@ -193,7 +192,7 @@ extension SwitchCaseAlignmentRule { \(violationMarker)default: 2 } } - """) + """), ] } @@ -224,7 +223,7 @@ extension SwitchCaseAlignmentRule { \(indentation)case 1: 1 \(indentation)\(indentedCasesOption ? "" : violationMarker)default: 2 } - """) + """), ] } @@ -236,7 +235,7 @@ extension SwitchCaseAlignmentRule { Example( "let a = switch i { case .x: 1 default: 0 }", configuration: ["ignore_one_liners": true] - ) + ), ] private var invalidOneLiners: [Example] { @@ -265,7 +264,7 @@ extension SwitchCaseAlignmentRule { Example(""" let a = switch i { \(violationMarker)case .x: 1 \(violationMarker)default: 0 } - """, configuration: ["ignore_one_liners": true]) + """, configuration: ["ignore_one_liners": true]), ] } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseOnNewlineRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseOnNewlineRule.swift index 6581f5ca9f..71159534e5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseOnNewlineRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseOnNewlineRule.swift @@ -1,7 +1,7 @@ import SwiftSyntax -private func wrapInSwitch(_ str: String, file: StaticString = #file, line: UInt = #line) -> Example { - return Example(""" +private func wrapInSwitch(_ str: String, file: StaticString = #filePath, line: UInt = #line) -> Example { + Example(""" switch foo { \(str) } @@ -46,7 +46,7 @@ struct SwitchCaseOnNewlineRule: OptInRule { let loadedToken = try tokenManager.decodeToken(from: response) return loadedToken } catch { throw error } - """) + """), ], triggeringExamples: [ wrapInSwitch("↓case 1: return true"), @@ -57,7 +57,7 @@ struct SwitchCaseOnNewlineRule: OptInRule { wrapInSwitch("↓case let .myCase(value) where value > 10: return false"), wrapInSwitch("↓case #selector(aFunction(_:)): return false"), wrapInSwitch("↓case let .myCase(value)\n where value > 10: return false"), - wrapInSwitch("↓case .first,\n .second: return false") + wrapInSwitch("↓case .first,\n .second: return false"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingClosureRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingClosureRule.swift index a91c8165aa..204d602568 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingClosureRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingClosureRule.swift @@ -35,7 +35,7 @@ struct TrailingClosureRule: OptInRule { for i in h({ [1,2,3] }) { print(i) } - """) + """), ], triggeringExamples: [ Example("foo.map(↓{ $0 + 1 })"), @@ -47,7 +47,7 @@ struct TrailingClosureRule: OptInRule { for n in list { n.forEach(↓{ print($0) }) } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], corrections: [ Example("foo.map(↓{ $0 + 1 })"): @@ -100,7 +100,7 @@ struct TrailingClosureRule: OptInRule { f(a: 2, /* comment1 */ c /* comment2 */ : /* comment3 */ { 3 } /* comment4 */) """): Example(""" f(a: 2) /* comment1 */ /* comment2 */ /* comment3 */ { 3 } /* comment4 */ - """) + """), ] ) } @@ -119,7 +119,7 @@ private extension TrailingClosureRule { } } - override func visit(_ node: ConditionElementListSyntax) -> SyntaxVisitorContinueKind { + override func visit(_: ConditionElementListSyntax) -> SyntaxVisitorContinueKind { .skipChildren } @@ -256,7 +256,7 @@ private extension Trivia { func removingLeadingNewlines() -> Self { if startsWithNewline { - Trivia(pieces: pieces.drop(while: { $0.isNewline })) + Trivia(pieces: pieces.drop(while: \.isNewline)) } else { self } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift index 6b99d41410..0383ff8051 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift @@ -14,7 +14,7 @@ struct TrailingCommaRule: Rule { Example("let example = [ 1,\n2↓,\n // 3,\n]"), Example("let foo = [\"אבג\", \"αβγ\", \"🇺🇸\"↓,]"), Example("class C {\n #if true\n func f() {\n let foo = [1, 2, 3↓,]\n }\n #endif\n}"), - Example("foo([1: \"\\(error)\"↓,])") + Example("foo([1: \"\\(error)\"↓,])"), ] private static let corrections: [Example: Example] = { @@ -42,7 +42,7 @@ struct TrailingCommaRule: Rule { Example("let foo = [Void]()"), Example("let example = [ 1,\n 2\n // 3,\n]"), Example("foo([1: \"\\(error)\"])"), - Example("let foo = [Int]()") + Example("let foo = [Int]()"), ], triggeringExamples: Self.triggeringExamples, corrections: Self.corrections diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingNewlineRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingNewlineRule.swift index 174a02282a..1cf4cb22a7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingNewlineRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingNewlineRule.swift @@ -14,7 +14,7 @@ extension String { } fileprivate func trailingNewlineCount() -> Int? { - return countOfTrailingCharacters(in: .newlines) + countOfTrailingCharacters(in: .newlines) } } @@ -31,12 +31,12 @@ struct TrailingNewlineRule: CorrectableRule, SourceKitFreeRule { ], triggeringExamples: [ Example("let a = 0"), - Example("let a = 0\n\n") + Example("let a = 0\n\n"), ].skipWrappingInCommentTests().skipWrappingInStringTests(), corrections: [ Example("let a = 0"): Example("let a = 0\n"), Example("let b = 0\n\n"): Example("let b = 0\n"), - Example("let c = 0\n\n\n\n"): Example("let c = 0\n") + Example("let c = 0\n\n\n\n"): Example("let c = 0\n"), ] ) @@ -44,9 +44,13 @@ struct TrailingNewlineRule: CorrectableRule, SourceKitFreeRule { if file.contents.trailingNewlineCount() == 1 { return [] } - return [StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file.path, line: max(file.lines.count, 1)))] + return [ + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file.path, line: max(file.lines.count, 1)) + ), + ] } func correct(file: SwiftLintFile) -> [Correction] { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift index 8e52a821b4..653d87fb48 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift @@ -11,14 +11,14 @@ struct TrailingWhitespaceRule: CorrectableRule { kind: .style, nonTriggeringExamples: [ Example("let name: String\n"), Example("//\n"), Example("// \n"), - Example("let name: String //\n"), Example("let name: String // \n") + Example("let name: String //\n"), Example("let name: String // \n"), ], triggeringExamples: [ Example("let name: String \n"), Example("/* */ let name: String \n") ], corrections: [ Example("let name: String \n"): Example("let name: String\n"), - Example("/* */ let name: String \n"): Example("/* */ let name: String\n") + Example("/* */ let name: String \n"): Example("/* */ let name: String\n"), ] ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift index 57f4082adf..daae7c6252 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift @@ -51,7 +51,7 @@ struct TypeContentsOrderRule: OptInRule { let typeContentOffset = orderedTypeContentOffsets[index] let content = typeContentOffset.typeContent.rawValue - let expected = expectedTypesContents.map { $0.rawValue }.joined(separator: ",") + let expected = expectedTypesContents.map(\.rawValue).joined(separator: ",") let article = ["a", "e", "i", "o", "u"].contains(content.substring(from: 0, length: 1)) ? "An" : "A" let styleViolation = StyleViolation( @@ -68,8 +68,8 @@ struct TypeContentsOrderRule: OptInRule { } private func typeContentOffsets(in typeStructure: SourceKittenDictionary) -> [TypeContentOffset] { - return typeStructure.substructure.compactMap { typeContentStructure in - guard let typeContent = self.typeContent(for: typeContentStructure) else { return nil } + typeStructure.substructure.compactMap { typeContentStructure in + guard let typeContent = typeContent(for: typeContentStructure) else { return nil } return (typeContent, typeContentStructure.offset!) } } @@ -118,7 +118,7 @@ struct TypeContentsOrderRule: OptInRule { "viewDidAppear(", "viewWillDisappear(", "viewDidDisappear(", - "willMove(" + "willMove(", ] if typeContentStructure.name!.starts(with: "init(") { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRuleExamples.swift index 1ea70c3362..74c20dd940 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRuleExamples.swift @@ -1,10 +1,10 @@ internal struct TypeContentsOrderRuleExamples { static let defaultOrderParts = [ - """ + """ // Type Aliases typealias CompletionHandler = ((TestEnum) -> Void) - """, - """ + """, + """ // Subtypes class TestClass { // 10 lines @@ -17,12 +17,12 @@ internal struct TypeContentsOrderRuleExamples { enum TestEnum { // 5 lines } - """, - """ + """, + """ // Type Properties static let cellIdentifier: String = "AmazingCell" - """, - """ + """, + """ // Instance Properties var shouldLayoutView1: Bool! weak var delegate: TestViewControllerDelegate? @@ -32,13 +32,13 @@ internal struct TypeContentsOrderRuleExamples { private var hasAnyLayoutedView: Bool { return hasLayoutedView1 || hasLayoutedView2 } - """, - """ + """, + """ // IBOutlets @IBOutlet private var view1: UIView! @IBOutlet private var view2: UIView! - """, - """ + """, + """ // Initializers override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) @@ -47,14 +47,14 @@ internal struct TypeContentsOrderRuleExamples { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - """, - """ + """, + """ // Type Methods static func makeViewController() -> TestViewController { // some code } - """, - """ + """, + """ // View Life-Cycle Methods override func viewDidLoad() { super.viewDidLoad() @@ -82,15 +82,15 @@ internal struct TypeContentsOrderRuleExamples { override func viewIsAppearing(_ animated: Bool) { super.viewIsAppearing(animated) } - """, - """ + """, + """ // IBActions @IBAction func goNextButtonPressed() { goToNextVc() delegate?.didPressTrackedButton() } - """, - """ + """, + """ // Other Methods func goToNextVc() { /* TODO */ } @@ -102,8 +102,8 @@ internal struct TypeContentsOrderRuleExamples { } private func getRandomVc() -> UIViewController { return UIViewController() } - """, - """ + """, + """ // Subscripts subscript(_ someIndexThatIsNotEvenUsed: Int) -> String { get { @@ -114,12 +114,12 @@ internal struct TypeContentsOrderRuleExamples { log.warning("Just a test", newValue) } } - """, - """ + """, + """ deinit { log.debug("deinit") } - """ + """, ] static let nonTriggeringExamples = [ @@ -127,7 +127,7 @@ internal struct TypeContentsOrderRuleExamples { class TestViewController: UIViewController { \(Self.defaultOrderParts.joined(separator: "\n\n")), } - """) + """), ] static let triggeringExamples = [ @@ -254,6 +254,6 @@ internal struct TypeContentsOrderRuleExamples { // MARK: Other Methods func goToNextVc() { /* TODO */ } } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift index bf690e1d67..91c362bee6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/UnneededParenthesesInClosureArgumentRule.swift @@ -23,7 +23,7 @@ struct UnneededParenthesesInClosureArgumentRule: OptInRule { registerFilter(name) { any, args throws -> Any? in doSomething(any, args) } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], triggeringExamples: [ Example("call(arg: { ↓(bar) in })"), @@ -59,7 +59,7 @@ struct UnneededParenthesesInClosureArgumentRule: OptInRule { registerFilter(name) { ↓(any, args) throws -> Any? in doSomething(any, args) } - """, excludeFromDocumentation: true) + """, excludeFromDocumentation: true), ], corrections: [ Example("call(arg: { ↓(bar) in })"): Example("call(arg: { bar in })"), @@ -69,7 +69,7 @@ struct UnneededParenthesesInClosureArgumentRule: OptInRule { Example("let foo = { bar -> Bool in return true }"), Example("method { ↓(foo, bar) in }"): Example("method { foo, bar in }"), Example("foo.map { ($0, $0) }.forEach { ↓(x, y) in }"): Example("foo.map { ($0, $0) }.forEach { x, y in }"), - Example("foo.bar { [weak self] ↓(x, y) in }"): Example("foo.bar { [weak self] x, y in }") + Example("foo.bar { [weak self] ↓(x, y) in }"): Example("foo.bar { [weak self] x, y in }"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/UnusedOptionalBindingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/UnusedOptionalBindingRule.swift index 39018eba01..a635b5c554 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/UnusedOptionalBindingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/UnusedOptionalBindingRule.swift @@ -16,7 +16,7 @@ struct UnusedOptionalBindingRule: Rule { Example("if foo() { let _ = bar() }"), Example("if foo() { _ = bar() }"), Example("if case .some(_) = self {}"), - Example("if let point = state.find({ _ in true }) {}") + Example("if let point = state.find({ _ in true }) {}"), ], triggeringExamples: [ Example("if let ↓_ = Foo.optionalValue {}"), @@ -26,7 +26,7 @@ struct UnusedOptionalBindingRule: Rule { Example("if let (first, _) = getOptionalTuple(), let ↓_ = Foo.optionalValue {}"), Example("if let (_, second) = getOptionalTuple(), let ↓_ = Foo.optionalValue {}"), Example("if let ↓(_, _, _) = getOptionalTuple(), let bar = Foo.optionalValue {}"), - Example("func foo() { if let ↓_ = bar {} }") + Example("func foo() { if let ↓_ = bar {} }"), ] ) } @@ -56,9 +56,7 @@ private extension ExprSyntax { return true } if let tuple = self.as(TupleExprSyntax.self) { - return tuple.elements.allSatisfy { elem in - elem.expression.isDiscardExpression - } + return tuple.elements.allSatisfy(\.expression.isDiscardExpression) } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift index 6eb28fab13..a7b9bedc38 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift @@ -82,7 +82,7 @@ struct VerticalParameterAlignmentOnCallRule: OptInRule { } completion: { _ in // completion } - """) + """), ], triggeringExamples: [ Example(""" @@ -119,7 +119,7 @@ struct VerticalParameterAlignmentOnCallRule: OptInRule { Example(""" myFunc(foo: 0, bar: baz == 0, ↓baz: true) - """) + """), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRuleExamples.swift index 743390ff3d..1bde7d086f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRuleExamples.swift @@ -59,7 +59,7 @@ internal struct VerticalParameterAlignmentRuleExamples { init(foo: Int, bar: String) } - """) + """), ] static let triggeringExamples: [Example] = [ @@ -85,6 +85,6 @@ internal struct VerticalParameterAlignmentRuleExamples { init(data: Data, ↓@ViewBuilder content: @escaping (Data.Element.IdentifiedValue) -> Content) {} } - """) + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift index 5f9a026b75..9b964d2f00 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift @@ -3,7 +3,7 @@ import SourceKittenFramework private extension SwiftLintFile { func violatingRanges(for pattern: String) -> [NSRange] { - return match(pattern: pattern, excludingSyntaxKinds: SyntaxKind.commentAndStringKinds) + match(pattern: pattern, excludingSyntaxKinds: SyntaxKind.commentKinds) } } @@ -12,38 +12,41 @@ struct VerticalWhitespaceBetweenCasesRule: Rule { private static let nonTriggeringExamples: [Example] = [ Example(""" - switch x { + switch x { - case 0..<5: - print("x is low") + case 0..<5: + print("x is low") - case 5..<10: - print("x is high") + case 5..<10: + print("x is high") - default: - print("x is invalid") + default: + print("x is invalid") - } - """), + @unknown default: + print("x is out of this world") + } + """), Example(""" - switch x { - case 0..<5: - print("x is low") + switch x { + case 0..<5: + print("x is low") - case 5..<10: - print("x is high") + case 5..<10: + print("x is high") - default: - print("x is invalid") - } - """), + default: + print("x is invalid") + } + """), Example(""" - switch x { - case 0..<5: print("x is low") - case 5..<10: print("x is high") - default: print("x is invalid") - } - """), + switch x { + case 0..<5: print("x is low") + case 5..<10: print("x is high") + default: print("x is invalid") + @unknown default: print("x is out of this world") + } + """), // Testing handling of trailing spaces: do not convert to """ style Example([ "switch x { \n", @@ -52,69 +55,90 @@ struct VerticalWhitespaceBetweenCasesRule: Rule { " \n", "default: \n", " print(\"not one\") \n", - "} " - ].joined()) + "} ", + ].joined()), ] private static let violatingToValidExamples: [Example: Example] = [ Example(""" switch x { case 0..<5: - print("x is valid") - ↓ default: - print("x is invalid") + return "x is valid" + ↓default: + return "x is invalid" + ↓@unknown default: + print("x is out of this world") } - """): Example(""" + """): Example(""" + switch x { + case 0..<5: + return "x is valid" + + default: + return "x is invalid" + + @unknown default: + print("x is out of this world") + } + """), + Example(""" switch x { case 0..<5: print("x is valid") - - default: + ↓default: print("x is invalid") } - """), + """): Example(""" + switch x { + case 0..<5: + print("x is valid") + + default: + print("x is invalid") + } + """), Example(""" switch x { case .valid: print("x is valid") - ↓ case .invalid: - print("x is invalid") - } - """): Example(""" - switch x { - case .valid: - print("x is valid") - - case .invalid: + ↓case .invalid: print("x is invalid") } - """), + """): Example(""" + switch x { + case .valid: + print("x is valid") + + case .invalid: + print("x is invalid") + } + """), Example(""" switch x { case .valid: print("multiple ...") print("... lines") - ↓ case .invalid: - print("multiple ...") - print("... lines") - } - """): Example(""" - switch x { - case .valid: - print("multiple ...") - print("... lines") - - case .invalid: + ↓case .invalid: print("multiple ...") print("... lines") } - """) + """): Example(""" + switch x { + case .valid: + print("multiple ...") + print("... lines") + + case .invalid: + print("multiple ...") + print("... lines") + } + """), ] - private let pattern = "([^\\n{][ \\t]*\\n)([ \\t]*(?:case[^\\n]+|default):[ \\t]*\\n)" + private let pattern = "([^\\n{][ \\t]*\\n)([ \\t]*(?:case[^\\n]+|default|@unknown default):[ \\t]*\\n)" private func violationRanges(in file: SwiftLintFile) -> [NSRange] { - return file.violatingRanges(for: pattern).filter { + file.violatingRanges(for: pattern).filter { !isFalsePositive(in: file, range: $0) } } @@ -137,7 +161,7 @@ struct VerticalWhitespaceBetweenCasesRule: Rule { extension VerticalWhitespaceBetweenCasesRule: OptInRule { static let description = RuleDescription( identifier: "vertical_whitespace_between_cases", - name: "Vertical Whitespace between Cases", + name: "Vertical Whitespace Between Cases", description: "Include a single empty line between switch cases", kind: .style, nonTriggeringExamples: (violatingToValidExamples.values + nonTriggeringExamples).sorted(), diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRule.swift index 2dda75ade3..6ca1625927 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRule.swift @@ -70,6 +70,6 @@ struct VerticalWhitespaceClosingBracesRule: CorrectableRule, OptInRule { private extension SwiftLintFile { func violatingRanges(for pattern: String) -> [NSRange] { - return match(pattern: pattern, excludingSyntaxKinds: SyntaxKind.commentAndStringKinds) + match(pattern: pattern, excludingSyntaxKinds: SyntaxKind.commentAndStringKinds) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRuleExamples.swift index c5488efb37..e482f2a0af 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceClosingBracesRuleExamples.swift @@ -28,7 +28,7 @@ internal struct VerticalWhitespaceClosingBracesRuleExamples { // do something // do something } - """, configuration: beforeTrivialLinesConfiguration, excludeFromDocumentation: true) + """, configuration: beforeTrivialLinesConfiguration, excludeFromDocumentation: true), ] static let violatingToValidExamples = [ @@ -142,6 +142,6 @@ internal struct VerticalWhitespaceClosingBracesRuleExamples { for i in 1...5 { mul *= i } return mul }]) - """, configuration: beforeTrivialLinesConfiguration) + """, configuration: beforeTrivialLinesConfiguration), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceOpeningBracesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceOpeningBracesRule.swift index 1df1c9ddd4..b6150b2aac 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceOpeningBracesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceOpeningBracesRule.swift @@ -3,7 +3,7 @@ import SourceKittenFramework private extension SwiftLintFile { func violatingRanges(for pattern: String) -> [NSRange] { - return match(pattern: pattern, excludingSyntaxKinds: SyntaxKind.commentAndStringKinds) + match(pattern: pattern, excludingSyntaxKinds: SyntaxKind.commentAndStringKinds) } } @@ -22,7 +22,7 @@ struct VerticalWhitespaceOpeningBracesRule: Rule { } */ - """) + """), ] private static let violatingToValidExamples: [Example: Example] = [ @@ -134,15 +134,13 @@ struct VerticalWhitespaceOpeningBracesRule: Rule { self.dismiss(animated: false, completion: { }) } - """) + """), ] private let pattern = "([{(\\[][ \\t]*(?:[^\\n{]+ in[ \\t]*$)?)((?:\\n[ \\t]*)+)(\\n)" } extension VerticalWhitespaceOpeningBracesRule: OptInRule { - init(configuration: Any) throws {} - static let description = RuleDescription( identifier: "vertical_whitespace_opening_braces", name: "Vertical Whitespace after Opening Braces", diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 70751ab2a9..b8b64ca4cd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -15,17 +15,17 @@ struct VerticalWhitespaceRule: CorrectableRule { Example("let abc = 0\n"), Example("let abc = 0\n\n"), Example("/* bcs \n\n\n\n*/"), - Example("// bca \n\n") + Example("// bca \n\n"), ], triggeringExamples: [ Example("let aaaa = 0\n\n\n"), Example("struct AAAA {}\n\n\n\n"), - Example("class BBBB {}\n\n\n") + Example("class BBBB {}\n\n\n"), ], corrections: [ Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\nclass AAA {}\n"), Example("let c = 0\n\n\nlet num = 1\n"): Example("let c = 0\n\nlet num = 1\n"), - Example("// bca \n\n\n"): Example("// bca \n\n") + Example("// bca \n\n\n"): Example("// bca \n\n"), ] // End of line autocorrections are handled by Trailing Newline Rule. ) @@ -43,7 +43,7 @@ struct VerticalWhitespaceRule: CorrectableRule { } return linesSections.map { eachLastLine, eachSectionCount in - return StyleViolation( + StyleViolation( ruleDescription: Self.description, severity: configuration.severityConfiguration.severity, location: Location(file: file.path, line: eachLastLine.index), diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift index 71ae16ec54..8fcb01086d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift @@ -23,7 +23,7 @@ struct VoidReturnRule: Rule { Example("let foo: () -> () async -> Void"), Example("func foo() -> () async throws -> Void {}"), Example("func foo() async throws -> () async -> Void { return {} }"), - Example("func foo() -> () async -> Int { 1 }") + Example("func foo() -> () async -> Int { 1 }"), ], triggeringExamples: [ Example("let abc: () -> ↓() = {}"), @@ -34,7 +34,7 @@ struct VoidReturnRule: Rule { Example("func foo(completion: () -> ↓(Void))"), Example("let foo: (ConfigurationTests) -> () throws -> ↓()"), Example("func foo() async -> ↓()"), - Example("func foo() async throws -> ↓()") + Example("func foo() async throws -> ↓()"), ], corrections: [ Example("let abc: () -> ↓() = {}"): Example("let abc: () -> Void = {}"), @@ -45,7 +45,7 @@ struct VoidReturnRule: Rule { Example("func foo(completion: () -> ↓(Void))"): Example("func foo(completion: () -> Void)"), Example("let foo: (ConfigurationTests) -> () throws -> ↓()"): Example("let foo: (ConfigurationTests) -> () throws -> Void"), - Example("func foo() async throws -> ↓()"): Example("func foo() async throws -> Void") + Example("func foo() async throws -> ↓()"): Example("func foo() async throws -> Void"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Visitors/CodeBlockVisitor.swift b/Source/SwiftLintBuiltInRules/Visitors/CodeBlockVisitor.swift new file mode 100644 index 0000000000..07184901d7 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Visitors/CodeBlockVisitor.swift @@ -0,0 +1,105 @@ +import SwiftLintCore +import SwiftSyntax + +class CodeBlockVisitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: AccessorDeclSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: ActorDeclSyntax) { + collectViolations(for: node.memberBlock) + } + + override func visitPost(_ node: CatchClauseSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: ClassDeclSyntax) { + collectViolations(for: node.memberBlock) + } + + override func visitPost(_ node: ClosureExprSyntax) { + guard let parent = node.parent else { + return + } + if parent.is(LabeledExprSyntax.self) { + // Function parameter + return + } + if parent.is(FunctionCallExprSyntax.self) || parent.is(MultipleTrailingClosureElementSyntax.self), + node.keyPathInParent != \FunctionCallExprSyntax.calledExpression { + // Trailing closure + collectViolations(for: node) + } + } + + override func visitPost(_ node: DeferStmtSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: DoStmtSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: EnumDeclSyntax) { + collectViolations(for: node.memberBlock) + } + + override func visitPost(_ node: ExtensionDeclSyntax) { + collectViolations(for: node.memberBlock) + } + + override func visitPost(_ node: FunctionDeclSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: ForStmtSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: GuardStmtSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: IfExprSyntax) { + collectViolations(for: node.body) + if case let .codeBlock(body) = node.elseBody { + collectViolations(for: body) + } + } + override func visitPost(_ node: InitializerDeclSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: PatternBindingSyntax) { + collectViolations(for: node.accessorBlock) + } + + override func visitPost(_ node: PrecedenceGroupDeclSyntax) { + collectViolations(for: node) + } + + override func visitPost(_ node: ProtocolDeclSyntax) { + collectViolations(for: node.memberBlock) + } + + override func visitPost(_ node: RepeatStmtSyntax) { + collectViolations(for: node.body) + } + + override func visitPost(_ node: StructDeclSyntax) { + collectViolations(for: node.memberBlock) + } + + override func visitPost(_ node: SwitchExprSyntax) { + collectViolations(for: node) + } + + override func visitPost(_ node: WhileStmtSyntax) { + collectViolations(for: node.body) + } + + func collectViolations(for _: (some BracedSyntax)?) { + // Intended to be overridden. + } +} diff --git a/Source/SwiftLintCore/Documentation/RuleDocumentation.swift b/Source/SwiftLintCore/Documentation/RuleDocumentation.swift index c46729fb00..00dd8f35e8 100644 --- a/Source/SwiftLintCore/Documentation/RuleDocumentation.swift +++ b/Source/SwiftLintCore/Documentation/RuleDocumentation.swift @@ -31,10 +31,10 @@ struct RuleDocumentation { var ruleName: String { ruleType.description.name } /// The identifier of the documented rule. - var ruleIdentifier: String { ruleType.description.identifier } + var ruleIdentifier: String { ruleType.identifier } /// The name of the file on disk for this rule documentation. - var fileName: String { "\(ruleType.description.identifier).md" } + var fileName: String { "\(ruleType.identifier).md" } /// The contents of the file for this rule documentation. var fileContents: String { @@ -52,6 +52,27 @@ struct RuleDocumentation { } return content.joined(separator: "\n\n") } + + private func formattedCode(_ example: Example) -> String { + if let config = example.configuration, let configuredRule = try? ruleType.init(configuration: config) { + let configDescription = configuredRule.createConfigurationDescription(exclusiveOptions: Set(config.keys)) + return """ + ```swift + // + // \(configDescription.yaml().linesPrefixed(with: "// ")) + // + + \(example.code) + + ``` + """ + } + return """ + ```swift + \(example.code) + ``` + """ + } } private func h1(_ text: String) -> String { "# \(text)" } @@ -60,30 +81,20 @@ private func h2(_ text: String) -> String { "## \(text)" } private func detailsSummary(_ rule: some Rule) -> String { let ruleDescription = """ - * **Identifier:** \(type(of: rule).description.identifier) + * **Identifier:** `\(type(of: rule).identifier)` * **Enabled by default:** \(rule is any OptInRule ? "No" : "Yes") * **Supports autocorrection:** \(rule is any CorrectableRule ? "Yes" : "No") * **Kind:** \(type(of: rule).description.kind) * **Analyzer rule:** \(rule is any AnalyzerRule ? "Yes" : "No") * **Minimum Swift compiler version:** \(type(of: rule).description.minSwiftVersion.rawValue) """ - if rule.configurationDescription.hasContent { - let configurationTable = rule.configurationDescription.markdown() - .split(separator: "\n") - .joined(separator: "\n ") + let description = rule.createConfigurationDescription() + if description.hasContent { return ruleDescription + """ * **Default configuration:** - \(configurationTable) + \(description.markdown().linesPrefixed(with: " ")) """ } return ruleDescription } - -private func formattedCode(_ example: Example) -> String { - return """ - ```swift - \(example.code) - ``` - """ -} diff --git a/Source/SwiftLintCore/Documentation/RuleListDocumentation.swift b/Source/SwiftLintCore/Documentation/RuleListDocumentation.swift index 2f707e54c6..eef3a75af7 100644 --- a/Source/SwiftLintCore/Documentation/RuleListDocumentation.swift +++ b/Source/SwiftLintCore/Documentation/RuleListDocumentation.swift @@ -35,7 +35,7 @@ public struct RuleListDocumentation { private var indexContents: String { let defaultRuleDocumentations = ruleDocumentations.filter { !$0.isOptInRule } let optInRuleDocumentations = ruleDocumentations.filter { $0.isOptInRule && !$0.isAnalyzerRule } - let analyzerRuleDocumentations = ruleDocumentations.filter { $0.isAnalyzerRule } + let analyzerRuleDocumentations = ruleDocumentations.filter(\.isAnalyzerRule) return """ # Rule Directory diff --git a/Source/SwiftLintCore/Extensions/Array+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Array+SwiftLint.swift index 65d8c62e63..9981e91bdd 100644 --- a/Source/SwiftLintCore/Extensions/Array+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Array+SwiftLint.swift @@ -59,7 +59,7 @@ public extension Array { /// /// - returns: The elements grouped by applying the specified transformation. func group(by transform: (Element) -> U) -> [U: [Element]] { - return Dictionary(grouping: self, by: { transform($0) }) + Dictionary(grouping: self, by: { transform($0) }) } /// Returns the elements failing the `belongsInSecondPartition` test, followed by the elements passing the @@ -83,7 +83,7 @@ public extension Array { /// /// - returns: The result of applying `transform` on every element and flattening the results. func parallelFlatMap(transform: (Element) -> [T]) -> [T] { - return parallelMap(transform: transform).flatMap { $0 } + parallelMap(transform: transform).flatMap { $0 } } /// Same as `compactMap` but spreads the work in the `transform` block in parallel using GCD's `concurrentPerform`. @@ -92,7 +92,7 @@ public extension Array { /// /// - returns: The result of applying `transform` on every element and discarding the `nil` ones. func parallelCompactMap(transform: (Element) -> T?) -> [T] { - return parallelMap(transform: transform).compactMap { $0 } + parallelMap(transform: transform).compactMap { $0 } } /// Same as `map` but spreads the work in the `transform` block in parallel using GCD's `concurrentPerform`. @@ -103,10 +103,36 @@ public extension Array { func parallelMap(transform: (Element) -> T) -> [T] { var result = ContiguousArray(repeating: nil, count: count) return result.withUnsafeMutableBufferPointer { buffer in + let buffer = Wrapper(buffer: buffer) DispatchQueue.concurrentPerform(iterations: buffer.count) { idx in buffer[idx] = transform(self[idx]) } - return buffer.map { $0! } + return buffer.data + } + } + + private class Wrapper: @unchecked Sendable { + let buffer: UnsafeMutableBufferPointer + + init(buffer: UnsafeMutableBufferPointer) { + self.buffer = buffer + } + + var data: [T] { + buffer.map { $0! } + } + + var count: Int { + buffer.count + } + + subscript(index: Int) -> T { + get { + queuedFatalError("Do not call this getter.") + } + set(newValue) { + buffer[index] = newValue + } } } } @@ -114,7 +140,7 @@ public extension Array { public extension Collection { /// Whether this collection has one or more element. var isNotEmpty: Bool { - return !isEmpty + !isEmpty } /// Get the only element in the collection. diff --git a/Source/SwiftLintCore/Extensions/Configuration+Cache.swift b/Source/SwiftLintCore/Extensions/Configuration+Cache.swift index 6a2f616578..c3e575a8b4 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+Cache.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+Cache.swift @@ -60,7 +60,7 @@ extension Configuration { } let cacheRulesDescriptions = rules - .map { rule in [type(of: rule).description.identifier, rule.cacheDescription] } + .map { rule in [type(of: rule).identifier, rule.cacheDescription] } .sorted { $0[0] < $1[0] } let jsonObject: [Any] = [rootDirectory, cacheRulesDescriptions] if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject) { @@ -84,7 +84,7 @@ extension Configuration { let versionedDirectory = [ "SwiftLint", Version.current.value, - ExecutableInfo.buildID + ExecutableInfo.buildID, ].compactMap({ $0 }).joined(separator: "/") let folder = baseURL.appendingPathComponent(versionedDirectory) diff --git a/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift b/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift index 09ae4f30b3..464553659e 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift @@ -41,6 +41,7 @@ package extension Configuration { // MARK: - Methods internal mutating func resultingConfiguration( enableAllRules: Bool, + onlyRule: [String], cachePath: String? ) throws -> Configuration { // Build if needed @@ -51,6 +52,7 @@ package extension Configuration { return try merged( configurationData: try validate(), enableAllRules: enableAllRules, + onlyRule: onlyRule, cachePath: cachePath ) } @@ -88,18 +90,44 @@ package extension Configuration { ) if !ignoreParentAndChildConfigs { - try processPossibleReference( + try processPossibleReferenceIgnoringFileAbsence( ofType: .childConfig, from: vertex, remoteConfigTimeoutOverride: remoteConfigTimeoutOverride, - remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCachedOverride - ) - try processPossibleReference( + remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCachedOverride) + + try processPossibleReferenceIgnoringFileAbsence( ofType: .parentConfig, from: vertex, remoteConfigTimeoutOverride: remoteConfigTimeoutOverride, + remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCachedOverride) + } + } + + private mutating func processPossibleReferenceIgnoringFileAbsence( + ofType type: EdgeType, + from vertex: Vertex, + remoteConfigTimeoutOverride: TimeInterval?, + remoteConfigTimeoutIfCachedOverride: TimeInterval? + ) throws { + do { + try processPossibleReference( + ofType: type, + from: vertex, + remoteConfigTimeoutOverride: remoteConfigTimeoutOverride, remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCachedOverride ) + } catch { + // If a child or parent config file doesn't exist, do not fail the rest of the config tree. + // Instead, just ignore this leaf of the config. Otherwise, rethrow the error. + guard case let Issue.fileNotFound(path) = error else { + throw error + } + queuedPrintError(""" + A local configuration at \(path) was not found. \ + Ignoring this part of the configuration. + """ + ) } } @@ -153,7 +181,7 @@ package extension Configuration { } private func findPossiblyExistingVertex(sameAs vertex: Vertex) -> Vertex? { - return vertices.first { + vertices.first { $0.originalRemoteString != nil && $0.originalRemoteString == vertex.originalRemoteString } ?? vertices.first { $0.filePath == vertex.filePath } } @@ -211,7 +239,7 @@ package extension Configuration { } return verticesToMerge.map { - return ( + ( configurationDict: $0.configurationDict, rootDirectory: $0.rootDirectory ) @@ -222,6 +250,7 @@ package extension Configuration { private func merged( configurationData: [(configurationDict: [String: Any], rootDirectory: String)], enableAllRules: Bool, + onlyRule: [String], cachePath: String? ) throws -> Configuration { // Split into first & remainder; use empty dict for first if the array is empty @@ -232,6 +261,7 @@ package extension Configuration { var firstConfiguration = try Configuration( dict: firstConfigurationData.configurationDict, enableAllRules: enableAllRules, + onlyRule: onlyRule, cachePath: cachePath ) @@ -247,8 +277,10 @@ package extension Configuration { // Build succeeding configurations return try configurationData.reduce(firstConfiguration) { var childConfiguration = try Configuration( + parentConfiguration: $0, dict: $1.configurationDict, enableAllRules: enableAllRules, + onlyRule: onlyRule, cachePath: cachePath ) childConfiguration.fileGraph = Self(rootDirectory: $1.rootDirectory) diff --git a/Source/SwiftLintCore/Extensions/Configuration+FileGraphSubtypes.swift b/Source/SwiftLintCore/Extensions/Configuration+FileGraphSubtypes.swift index 9af6186879..f1a8283cf7 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+FileGraphSubtypes.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+FileGraphSubtypes.swift @@ -72,16 +72,16 @@ internal extension Configuration.FileGraph { private func read(at path: String) throws -> String { guard !path.isEmpty && FileManager.default.fileExists(atPath: path) else { - throw isInitialVertex ? - Issue.initialFileNotFound(path: path) : - Issue.genericWarning("File \(path) can't be found.") + throw isInitialVertex + ? Issue.initialFileNotFound(path: path) + : Issue.fileNotFound(path: path) } return try String(contentsOfFile: path, encoding: .utf8) } internal static func == (lhs: Vertex, rhs: Vertex) -> Bool { - return lhs.filePath == rhs.filePath + lhs.filePath == rhs.filePath && lhs.originalRemoteString == rhs.originalRemoteString && lhs.rootDirectory == rhs.rootDirectory } @@ -95,8 +95,10 @@ internal extension Configuration.FileGraph { // MARK: - Edge struct Edge: Hashable { + // swiftlint:disable implicitly_unwrapped_optional var parent: Vertex! var child: Vertex! + // swiftlint:enable implicitly_unwrapped_optional } // MARK: - EdgeType diff --git a/Source/SwiftLintCore/Extensions/Configuration+LintableFiles.swift b/Source/SwiftLintCore/Extensions/Configuration+LintableFiles.swift index b9c4466db3..79617bf622 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+LintableFiles.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+LintableFiles.swift @@ -16,9 +16,10 @@ extension Configuration { /// - parameter excludeByPrefix: Whether or not uses excluding by prefix algorithm. /// /// - returns: Files to lint. - public func lintableFiles(inPath path: String, forceExclude: Bool, + public func lintableFiles(inPath path: String, + forceExclude: Bool, excludeBy: ExcludeBy) -> [SwiftLintFile] { - return lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy) + lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy) .compactMap(SwiftLintFile.init(pathDeferringReading:)) } @@ -109,7 +110,7 @@ extension Configuration { /// /// - returns: The expanded excluded file paths. public func excludedPaths(fileManager: some LintableFileManager = FileManager.default) -> [String] { - return excludedPaths + excludedPaths .flatMap(Glob.resolveGlob) .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) } } diff --git a/Source/SwiftLintCore/Extensions/Configuration+Merging.swift b/Source/SwiftLintCore/Extensions/Configuration+Merging.swift index 45a871873a..82a11e342d 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+Merging.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+Merging.swift @@ -8,7 +8,7 @@ extension Configuration { // MARK: - Methods: Merging package func merged( withChild childConfiguration: Configuration, - rootDirectory: String + rootDirectory: String = "" ) -> Configuration { let mergedIncludedAndExcluded = self.mergedIncludedAndExcluded( with: childConfiguration, @@ -25,7 +25,11 @@ extension Configuration { reporter: reporter, cachePath: cachePath, allowZeroLintableFiles: childConfiguration.allowZeroLintableFiles, - strict: childConfiguration.strict + strict: childConfiguration.strict, + lenient: childConfiguration.lenient, + baseline: childConfiguration.baseline, + writeBaseline: childConfiguration.writeBaseline, + checkForUpdates: childConfiguration.checkForUpdates ) } @@ -81,7 +85,7 @@ extension Configuration { /// /// - returns: A new configuration. public func configuration(for file: SwiftLintFile) -> Configuration { - return (file.path?.bridge().deletingLastPathComponent).map(configuration(forDirectory:)) ?? self + (file.path?.bridge().deletingLastPathComponent).map(configuration(forDirectory:)) ?? self } private func configuration(forDirectory directory: String) -> Configuration { diff --git a/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift b/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift index 0b06fbc495..c8326c0776 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift @@ -15,6 +15,10 @@ extension Configuration { case analyzerRules = "analyzer_rules" case allowZeroLintableFiles = "allow_zero_lintable_files" case strict = "strict" + case lenient = "lenient" + case baseline = "baseline" + case writeBaseline = "write_baseline" + case checkForUpdates = "check_for_updates" case childConfig = "child_config" case parentConfig = "parent_config" case remoteConfigTimeout = "remote_timeout" @@ -22,11 +26,12 @@ extension Configuration { } // MARK: - Properties - private static let validGlobalKeys: Set = Set(Key.allCases.map { $0.rawValue }) + private static let validGlobalKeys: Set = Set(Key.allCases.map(\.rawValue)) // MARK: - Initializers /// Creates a Configuration value based on the specified parameters. /// + /// - parameter parentConfiguration: The parent configuration, if any. /// - parameter dict: The untyped dictionary to serve as the input for this typed configuration. /// Typically generated from a YAML-formatted file. /// - parameter ruleList: The list of rules to be available to this configuration. @@ -34,12 +39,14 @@ extension Configuration { /// settings in `dict`. /// - parameter cachePath: The location of the persisted cache on disk. public init( + parentConfiguration: Configuration? = nil, dict: [String: Any], ruleList: RuleList = RuleRegistry.shared.list, enableAllRules: Bool = false, + onlyRule: [String] = [], cachePath: String? = nil ) throws { - func defaultStringArray(_ object: Any?) -> [String] { return [String].array(of: object) ?? [] } + func defaultStringArray(_ object: Any?) -> [String] { [String].array(of: object) ?? [] } // Use either the new 'opt_in_rules' or fallback to the deprecated 'enabled_rules' let optInRules = defaultStringArray(dict[Key.optInRules.rawValue] ?? dict[Key.enabledRules.rawValue]) @@ -60,7 +67,7 @@ extension Configuration { allRulesWrapped = try ruleList.allRulesWrapped(configurationDict: dict) } catch let RuleListError.duplicatedConfigurations(ruleType) { let aliases = ruleType.description.deprecatedAliases.map { "'\($0)'" }.joined(separator: ", ") - let identifier = ruleType.description.identifier + let identifier = ruleType.identifier throw Issue.genericWarning( "Multiple configurations found for '\(identifier)'. Check for any aliases: \(aliases)." ) @@ -68,15 +75,21 @@ extension Configuration { let rulesMode = try RulesMode( enableAllRules: enableAllRules, + onlyRule: onlyRule, onlyRules: onlyRules, optInRules: optInRules, disabledRules: disabledRules, analyzerRules: analyzerRules ) - Self.validateConfiguredRulesAreEnabled( - configurationDictionary: dict, ruleList: ruleList, rulesMode: rulesMode - ) + if onlyRule.isEmpty { + Self.validateConfiguredRulesAreEnabled( + parentConfiguration: parentConfiguration, + configurationDictionary: dict, + ruleList: ruleList, + rulesMode: rulesMode + ) + } self.init( rulesMode: rulesMode, @@ -90,13 +103,17 @@ extension Configuration { cachePath: cachePath ?? dict[Key.cachePath.rawValue] as? String, pinnedVersion: dict[Key.swiftlintVersion.rawValue].map { ($0 as? String) ?? String(describing: $0) }, allowZeroLintableFiles: dict[Key.allowZeroLintableFiles.rawValue] as? Bool ?? false, - strict: dict[Key.strict.rawValue] as? Bool ?? false + strict: dict[Key.strict.rawValue] as? Bool ?? false, + lenient: dict[Key.lenient.rawValue] as? Bool ?? false, + baseline: dict[Key.baseline.rawValue] as? String, + writeBaseline: dict[Key.writeBaseline.rawValue] as? String, + checkForUpdates: dict[Key.checkForUpdates.rawValue] as? Bool ?? false ) } // MARK: - Methods: Validations private static func validKeys(ruleList: RuleList) -> Set { - return validGlobalKeys.union(ruleList.allValidIdentifiers()) + validGlobalKeys.union(ruleList.allValidIdentifiers()) } private static func getIndentationLogIfInvalid(from dict: [String: Any]) -> IndentationStyle { @@ -125,12 +142,12 @@ extension Configuration { // Deprecation warning for rules let deprecatedRulesIdentifiers = ruleList.list.flatMap { identifier, rule -> [(String, String)] in - return rule.description.deprecatedAliases.map { ($0, identifier) } + rule.description.deprecatedAliases.map { ($0, identifier) } } let userProvidedRuleIDs = Set(disabledRules + optInRules + onlyRules) let deprecatedUsages = deprecatedRulesIdentifiers.filter { deprecatedIdentifier, _ in - return dict[deprecatedIdentifier] != nil || userProvidedRuleIDs.contains(deprecatedIdentifier) + dict[deprecatedIdentifier] != nil || userProvidedRuleIDs.contains(deprecatedIdentifier) } for (deprecatedIdentifier, identifier) in deprecatedUsages { @@ -147,35 +164,117 @@ extension Configuration { } private static func validateConfiguredRulesAreEnabled( + parentConfiguration: Configuration?, configurationDictionary dict: [String: Any], ruleList: RuleList, rulesMode: RulesMode ) { for key in dict.keys where !validGlobalKeys.contains(key) { guard let identifier = ruleList.identifier(for: key), - let rule = ruleList.list[identifier] else { + let ruleType = ruleList.list[identifier] else { continue } - let message = "Found a configuration for '\(identifier)' rule" - switch rulesMode { - case .allEnabled: + case .allCommandLine, .onlyCommandLine: return + case .onlyConfiguration(let onlyRules): + let issue = validateConfiguredRuleIsEnabled(onlyRules: onlyRules, ruleType: ruleType) + issue?.print() + case let .defaultConfiguration(disabled: disabledRules, optIn: optInRules): + let issue = validateConfiguredRuleIsEnabled( + parentConfiguration: parentConfiguration, + disabledRules: disabledRules, + optInRules: optInRules, + ruleType: ruleType + ) + issue?.print() + } + } + } - case .only(let onlyRules): - if Set(onlyRules).isDisjoint(with: rule.description.allIdentifiers) { - Issue.genericWarning("\(message), but it is not present on '\(Key.onlyRules.rawValue)'.").print() - } + static func validateConfiguredRuleIsEnabled( + parentConfiguration: Configuration?, + disabledRules: Set, + optInRules: Set, + ruleType: any Rule.Type + ) -> Issue? { + var enabledInParentRules: Set = [] + var disabledInParentRules: Set = [] + var allEnabledRules: Set = [] + + if case .onlyConfiguration(let onlyRules) = parentConfiguration?.rulesMode { + enabledInParentRules = onlyRules + } else if case .defaultConfiguration( + let parentDisabledRules, let parentOptInRules + ) = parentConfiguration?.rulesMode { + enabledInParentRules = parentOptInRules + disabledInParentRules = parentDisabledRules + } + allEnabledRules = enabledInParentRules + .subtracting(disabledInParentRules) + .union(optInRules) + .subtracting(disabledRules) + + return validateConfiguredRuleIsEnabled( + parentConfiguration: parentConfiguration, + enabledInParentRules: enabledInParentRules, + disabledInParentRules: disabledInParentRules, + disabledRules: disabledRules, + optInRules: optInRules, + allEnabledRules: allEnabledRules, + ruleType: ruleType + ) + } - case let .default(disabled: disabledRules, optIn: optInRules): - if rule is any OptInRule.Type, Set(optInRules).isDisjoint(with: rule.description.allIdentifiers) { - Issue.genericWarning("\(message), but it is not enabled on '\(Key.optInRules.rawValue)'.").print() - } else if Set(disabledRules).isSuperset(of: rule.description.allIdentifiers) { - Issue.genericWarning("\(message), but it is disabled on '\(Key.disabledRules.rawValue)'.").print() + static func validateConfiguredRuleIsEnabled( + onlyRules: Set, + ruleType: any Rule.Type + ) -> Issue? { + if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { + return Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier) + } + return nil + } + + // swiftlint:disable:next function_parameter_count + static func validateConfiguredRuleIsEnabled( + parentConfiguration: Configuration?, + enabledInParentRules: Set, + disabledInParentRules: Set, + disabledRules: Set, + optInRules: Set, + allEnabledRules: Set, + ruleType: any Rule.Type + ) -> Issue? { + if case .allCommandLine = parentConfiguration?.rulesMode { + if disabledRules.contains(ruleType.identifier) { + return Issue.ruleDisabledInDisabledRules(ruleID: ruleType.identifier) + } + return nil + } + + let allIdentifiers = ruleType.description.allIdentifiers + + if allEnabledRules.isDisjoint(with: allIdentifiers) { + if !disabledRules.isDisjoint(with: allIdentifiers) { + return Issue.ruleDisabledInDisabledRules(ruleID: ruleType.identifier) + } + if !disabledInParentRules.isDisjoint(with: allIdentifiers) { + return Issue.ruleDisabledInParentConfiguration(ruleID: ruleType.identifier) + } + + if ruleType is any OptInRule.Type { + if enabledInParentRules.union(optInRules).isDisjoint(with: allIdentifiers) { + return Issue.ruleNotEnabledInOptInRules(ruleID: ruleType.identifier) } + } else if case .onlyConfiguration(let enabledInParentRules) = parentConfiguration?.rulesMode, + enabledInParentRules.isDisjoint(with: allIdentifiers) { + return Issue.ruleNotEnabledInParentOnlyRules(ruleID: ruleType.identifier) } } + + return nil } private static func warnAboutMisplacedAnalyzerRules(optInRules: [String], ruleList: RuleList) { diff --git a/Source/SwiftLintCore/Extensions/Configuration+Remote.swift b/Source/SwiftLintCore/Extensions/Configuration+Remote.swift index 186dc88e47..e8eec05c86 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+Remote.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+Remote.swift @@ -7,19 +7,19 @@ import FoundationNetworking internal extension Configuration.FileGraph.FilePath { // MARK: - Properties: Remote Cache /// This should never be touched. - private static let swiftlintPath: String = ".swiftlint" + private static let swiftlintPath = ".swiftlint" /// This should never be touched. Change the version number for changes to the cache format - private static let remoteCachePath: String = "\(swiftlintPath)/RemoteConfigCache" + private static let remoteCachePath = "\(swiftlintPath)/RemoteConfigCache" /// If the format of the caching is changed in the future, change this version number - private static let remoteCacheVersionNumber: String = "v1" + private static let remoteCacheVersionNumber = "v1" /// Use this to get the path to the cache directory for the current cache format - private static let versionedRemoteCachePath: String = "\(remoteCachePath)/\(remoteCacheVersionNumber)" + private static let versionedRemoteCachePath = "\(remoteCachePath)/\(remoteCacheVersionNumber)" /// The path to the gitignore file. - private static let gitignorePath: String = ".gitignore" + private static let gitignorePath = ".gitignore" /// This dictionary has URLs as its keys and contents of those URLs as its values /// In production mode, this should be empty. For tests, it may be filled. @@ -94,7 +94,7 @@ internal extension Configuration.FileGraph.FilePath { guard taskResult.2 == nil, // No error (taskResult.1 as? HTTPURLResponse)?.statusCode == 200, - let configStr = (taskResult.0.flatMap { String(decoding: $0, as: UTF8.self) }) + let configStr = (taskResult.0.flatMap { String(data: $0, encoding: .utf8) }) else { return try handleWrongData( urlString: urlString, diff --git a/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift b/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift index 941cb7e124..b5540cf3ad 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift @@ -6,7 +6,7 @@ public extension Configuration { /// - returns: The rule for the specified ID, if configured in this configuration. func configuredRule(forID ruleID: String) -> (any Rule)? { rules.first { rule in - if type(of: rule).description.identifier == ruleID { + if type(of: rule).identifier == ruleID { if let customRules = rule as? CustomRules { return customRules.configuration.customRuleConfigurations.isNotEmpty } @@ -17,20 +17,25 @@ public extension Configuration { } /// Represents how a Configuration object can be configured with regards to rules. - enum RulesMode { + enum RulesMode: Equatable { /// The default rules mode, which will enable all rules that aren't defined as being opt-in /// (conforming to the `OptInRule` protocol), minus the rules listed in `disabled`, plus the rules listed in /// `optIn`. - case `default`(disabled: Set, optIn: Set) + case defaultConfiguration(disabled: Set, optIn: Set) - /// Only enable the rules explicitly listed. - case only(Set) + /// Only enable the rules explicitly listed in the configuration files. + case onlyConfiguration(Set) + + /// Only enable the rule(s) explicitly listed on the command line (and their aliases). `--only-rule` can be + /// specified multiple times to enable multiple rules. + case onlyCommandLine(Set) /// Enable all available rules. - case allEnabled + case allCommandLine internal init( enableAllRules: Bool, + onlyRule: [String], onlyRules: [String], optInRules: [String], disabledRules: [String], @@ -47,7 +52,9 @@ public extension Configuration { } if enableAllRules { - self = .allEnabled + self = .allCommandLine + } else if onlyRule.isNotEmpty { + self = .onlyCommandLine(Set(onlyRule)) } else if onlyRules.isNotEmpty { if disabledRules.isNotEmpty || optInRules.isNotEmpty { throw Issue.genericWarning( @@ -58,7 +65,7 @@ public extension Configuration { } warnAboutDuplicates(in: onlyRules + analyzerRules) - self = .only(Set(onlyRules + analyzerRules)) + self = .onlyConfiguration(Set(onlyRules + analyzerRules)) } else { warnAboutDuplicates(in: disabledRules) @@ -72,35 +79,51 @@ public extension Configuration { effectiveOptInRules = optInRules } - warnAboutDuplicates(in: effectiveOptInRules + analyzerRules) - self = .default(disabled: Set(disabledRules), optIn: Set(effectiveOptInRules + analyzerRules)) + let effectiveAnalyzerRules: [String] + if analyzerRules.contains(RuleIdentifier.all.stringRepresentation) { + let allAnalyzerRules = RuleRegistry.shared.list.list.compactMap { ruleID, ruleType in + ruleType is any AnalyzerRule.Type ? ruleID : nil + } + effectiveAnalyzerRules = allAnalyzerRules + } else { + effectiveAnalyzerRules = analyzerRules + } + + warnAboutDuplicates(in: effectiveOptInRules + effectiveAnalyzerRules) + self = .defaultConfiguration( + disabled: Set(disabledRules), optIn: Set(effectiveOptInRules + effectiveAnalyzerRules) + ) } } - internal func applied(aliasResolver: (String) -> String) -> RulesMode { + internal func applied(aliasResolver: (String) -> String) -> Self { switch self { - case let .default(disabled, optIn): - return .default( + case let .defaultConfiguration(disabled, optIn): + return .defaultConfiguration( disabled: Set(disabled.map(aliasResolver)), optIn: Set(optIn.map(aliasResolver)) ) - case let .only(onlyRules): - return .only(Set(onlyRules.map(aliasResolver))) + case let .onlyConfiguration(onlyRules): + return .onlyConfiguration(Set(onlyRules.map(aliasResolver))) + + case let .onlyCommandLine(onlyRules): + return .onlyCommandLine(Set(onlyRules.map(aliasResolver))) - case .allEnabled: - return .allEnabled + case .allCommandLine: + return .allCommandLine } } - internal func activateCustomRuleIdentifiers(allRulesWrapped: [ConfigurationRuleWrapper]) -> RulesMode { + internal func activateCustomRuleIdentifiers(allRulesWrapped: [ConfigurationRuleWrapper]) -> Self { // In the only mode, if the custom rules rule is enabled, all custom rules are also enabled implicitly // This method makes the implicitly explicit switch self { - case let .only(onlyRules) where onlyRules.contains { $0 == CustomRules.description.identifier }: + case let .onlyConfiguration(onlyRules) where onlyRules.contains { + $0 == CustomRules.identifier + }: let customRulesRule = (allRulesWrapped.first { $0.rule is CustomRules })?.rule as? CustomRules - let customRuleIdentifiers = customRulesRule?.configuration.customRuleConfigurations.map(\.identifier) - return .only(onlyRules.union(Set(customRuleIdentifiers ?? []))) + return .onlyConfiguration(onlyRules.union(Set(customRulesRule?.customRuleIdentifiers ?? []))) default: return self diff --git a/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift b/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift index ac008e9958..6e3295d951 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift @@ -11,10 +11,9 @@ internal extension Configuration { private var invalidRuleIdsWarnedAbout: Set = [] private var validRuleIdentifiers: Set { - let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).description.identifier } + let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).identifier } let configurationCustomRulesIdentifiers = - (allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules)? - .configuration.customRuleConfigurations.map { $0.identifier } ?? [] + (allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules)?.customRuleIdentifiers ?? [] return Set(regularRuleIdentifiers + configurationCustomRulesIdentifiers) } @@ -35,26 +34,26 @@ internal extension Configuration { let customRulesFilter: (RegexConfiguration) -> (Bool) var resultingRules = [any Rule]() switch mode { - case .allEnabled: + case .allCommandLine: customRulesFilter = { _ in true } - resultingRules = allRulesWrapped.map { $0.rule } + resultingRules = allRulesWrapped.map(\.rule) - case var .only(onlyRulesRuleIdentifiers): + case let .onlyConfiguration(onlyRulesRuleIdentifiers), let .onlyCommandLine(onlyRulesRuleIdentifiers): customRulesFilter = { onlyRulesRuleIdentifiers.contains($0.identifier) } - onlyRulesRuleIdentifiers = validate(ruleIds: onlyRulesRuleIdentifiers, valid: validRuleIdentifiers) + let onlyRulesRuleIdentifiers = validate(ruleIds: onlyRulesRuleIdentifiers, valid: validRuleIdentifiers) resultingRules = allRulesWrapped.filter { tuple in - onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).description.identifier) - }.map { $0.rule } + onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).identifier) + }.map(\.rule) - case var .default(disabledRuleIdentifiers, optInRuleIdentifiers): + case var .defaultConfiguration(disabledRuleIdentifiers, optInRuleIdentifiers): customRulesFilter = { !disabledRuleIdentifiers.contains($0.identifier) } disabledRuleIdentifiers = validate(ruleIds: disabledRuleIdentifiers, valid: validRuleIdentifiers) optInRuleIdentifiers = validate(optInRuleIds: optInRuleIdentifiers, valid: validRuleIdentifiers) resultingRules = allRulesWrapped.filter { tuple in - let id = type(of: tuple.rule).description.identifier + let id = type(of: tuple.rule).identifier return !disabledRuleIdentifiers.contains(id) && (!(tuple.rule is any OptInRule) || optInRuleIdentifiers.contains(id)) - }.map { $0.rule } + }.map(\.rule) } // Filter custom rules @@ -66,7 +65,7 @@ internal extension Configuration { // Sort by name resultingRules = resultingRules.sorted { - type(of: $0).description.identifier < type(of: $1).description.identifier + type(of: $0).identifier < type(of: $1).identifier } // Store & return @@ -76,20 +75,20 @@ internal extension Configuration { lazy var disabledRuleIdentifiers: [String] = { switch mode { - case let .default(disabled, _): + case let .defaultConfiguration(disabled, _): return validate(ruleIds: disabled, valid: validRuleIdentifiers, silent: true) .sorted(by: <) - case let .only(onlyRules): + case let .onlyConfiguration(onlyRules), let .onlyCommandLine(onlyRules): return validate( ruleIds: Set(allRulesWrapped - .map { type(of: $0.rule).description.identifier } + .map { type(of: $0.rule).identifier } .filter { !onlyRules.contains($0) }), valid: validRuleIdentifiers, silent: true ).sorted(by: <) - case .allEnabled: + case .allCommandLine: return [] } }() @@ -148,7 +147,7 @@ internal extension Configuration { let validRuleIdentifiers = self.validRuleIdentifiers.union(child.validRuleIdentifiers) let newMode: RulesMode switch child.mode { - case let .default(childDisabled, childOptIn): + case let .defaultConfiguration(childDisabled, childOptIn): newMode = mergeDefaultMode( newAllRulesWrapped: newAllRulesWrapped, child: child, @@ -157,13 +156,21 @@ internal extension Configuration { validRuleIdentifiers: validRuleIdentifiers ) - case let .only(childOnlyRules): - // Always use the child only rules - newMode = .only(childOnlyRules) + case let .onlyConfiguration(childOnlyRules): + // Use the child only rules, unless the parent is onlyRule + switch mode { + case let .onlyCommandLine(onlyRules): + newMode = .onlyCommandLine(onlyRules) + default: + newMode = .onlyConfiguration(childOnlyRules) + } + case let .onlyCommandLine(onlyRules): + // Always use the only rule + newMode = .onlyCommandLine(onlyRules) - case .allEnabled: + case .allCommandLine: // Always use .allEnabled mode - newMode = .allEnabled + newMode = .allCommandLine } // Assemble & return merged rules @@ -178,15 +185,14 @@ internal extension Configuration { private func mergedAllRulesWrapped(with child: RulesWrapper) -> [ConfigurationRuleWrapper] { let mainConfigSet = Set(allRulesWrapped.map(HashableConfigurationRuleWrapperWrapper.init)) let childConfigSet = Set(child.allRulesWrapped.map(HashableConfigurationRuleWrapperWrapper.init)) - let childConfigRulesWithConfig = childConfigSet.filter { - $0.configurationRuleWrapper.initializedWithNonEmptyConfiguration - } + let childConfigRulesWithConfig = childConfigSet + .filter(\.configurationRuleWrapper.initializedWithNonEmptyConfiguration) let rulesUniqueToChildConfig = childConfigSet.subtracting(mainConfigSet) return childConfigRulesWithConfig // Include, if rule is configured in child .union(rulesUniqueToChildConfig) // Include, if rule is in child config only .union(mainConfigSet) // Use configurations from parent for remaining rules - .map { $0.configurationRuleWrapper } + .map(\.configurationRuleWrapper) } private func mergedCustomRules( @@ -227,12 +233,12 @@ internal extension Configuration { let childOptIn = child.validate(optInRuleIds: childOptIn, valid: validRuleIdentifiers) switch mode { // Switch parent's mode. Child is in default mode. - case var .default(disabled, optIn): + case var .defaultConfiguration(disabled, optIn): disabled = validate(ruleIds: disabled, valid: validRuleIdentifiers) optIn = child.validate(optInRuleIds: optIn, valid: validRuleIdentifiers) // Only use parent disabled / optIn if child config doesn't tell the opposite - return .default( + return .defaultConfiguration( disabled: Set(childDisabled).union(Set(disabled.filter { !childOptIn.contains($0) })), optIn: Set(childOptIn).union(Set(optIn.filter { !childDisabled.contains($0) })) .filter { @@ -240,15 +246,15 @@ internal extension Configuration { } ) - case var .only(onlyRules): + case var .onlyConfiguration(onlyRules): // Also add identifiers of child custom rules iff the custom_rules rule is enabled // (parent custom rules are already added) - if (onlyRules.contains { $0 == CustomRules.description.identifier }) { + if (onlyRules.contains { $0 == CustomRules.identifier }) { if let childCustomRulesRule = (child.allRulesWrapped.first { $0.rule is CustomRules })?.rule as? CustomRules { onlyRules = onlyRules.union( Set( - childCustomRulesRule.configuration.customRuleConfigurations.map { $0.identifier } + childCustomRulesRule.customRuleIdentifiers ) ) } @@ -258,18 +264,22 @@ internal extension Configuration { // Allow parent only rules that weren't disabled via the child config // & opt-ins from the child config - return .only(Set( + return .onlyConfiguration(Set( childOptIn.union(onlyRules).filter { !childDisabled.contains($0) } )) - case .allEnabled: + case let .onlyCommandLine(onlyRules): + // Like .allEnabled, rules can be disabled in a child config + return .onlyCommandLine(onlyRules.filter { !childDisabled.contains($0) }) + + case .allCommandLine: // Opt-in to every rule that isn't disabled via child config - return .default( + return .defaultConfiguration( disabled: childDisabled .filter { !isOptInRule($0, allRulesWrapped: newAllRulesWrapped) }, - optIn: Set(newAllRulesWrapped.map { type(of: $0.rule).description.identifier } + optIn: Set(newAllRulesWrapped.map { type(of: $0.rule).identifier } .filter { !childDisabled.contains($0) && isOptInRule($0, allRulesWrapped: newAllRulesWrapped) @@ -288,7 +298,7 @@ internal extension Configuration { } let isOptInRule = allRulesWrapped - .first { type(of: $0.rule).description.identifier == identifier }?.rule is any OptInRule + .first { type(of: $0.rule).identifier == identifier }?.rule is any OptInRule Self.isOptInRuleCache[identifier] = isOptInRule return isOptInRule } diff --git a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift index af8c31c1e5..c9221ccebf 100644 --- a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift @@ -6,7 +6,7 @@ public struct SourceKittenDictionary { /// The underlying SourceKitten dictionary. public let value: [String: any SourceKitRepresentable] /// The cached substructure for this dictionary. Empty if there is no substructure. - public let substructure: [SourceKittenDictionary] + public let substructure: [Self] /// The kind of Swift expression represented by this dictionary, if it is an expression. public let expressionKind: SwiftExpressionKind? @@ -38,12 +38,12 @@ public struct SourceKittenDictionary { /// Body length public var bodyLength: ByteCount? { - return (value["key.bodylength"] as? Int64).map(ByteCount.init) + (value["key.bodylength"] as? Int64).map(ByteCount.init) } /// Body offset. public var bodyOffset: ByteCount? { - return (value["key.bodyoffset"] as? Int64).map(ByteCount.init) + (value["key.bodyoffset"] as? Int64).map(ByteCount.init) } /// Body byte range. @@ -54,26 +54,26 @@ public struct SourceKittenDictionary { /// Kind. public var kind: String? { - return value["key.kind"] as? String + value["key.kind"] as? String } /// Length. public var length: ByteCount? { - return (value["key.length"] as? Int64).map(ByteCount.init) + (value["key.length"] as? Int64).map(ByteCount.init) } /// Name. public var name: String? { - return value["key.name"] as? String + value["key.name"] as? String } /// Name length. public var nameLength: ByteCount? { - return (value["key.namelength"] as? Int64).map(ByteCount.init) + (value["key.namelength"] as? Int64).map(ByteCount.init) } /// Name offset. public var nameOffset: ByteCount? { - return (value["key.nameoffset"] as? Int64).map(ByteCount.init) + (value["key.nameoffset"] as? Int64).map(ByteCount.init) } /// Byte range of name. @@ -84,7 +84,7 @@ public struct SourceKittenDictionary { /// Offset. public var offset: ByteCount? { - return (value["key.offset"] as? Int64).map(ByteCount.init) + (value["key.offset"] as? Int64).map(ByteCount.init) } /// Returns byte range starting from `offset` with `length` bytes @@ -95,66 +95,66 @@ public struct SourceKittenDictionary { /// Setter accessibility. public var setterAccessibility: String? { - return value["key.setter_accessibility"] as? String + value["key.setter_accessibility"] as? String } /// Type name. public var typeName: String? { - return value["key.typename"] as? String + value["key.typename"] as? String } /// Documentation length. public var docLength: ByteCount? { - return (value["key.doclength"] as? Int64).flatMap(ByteCount.init) + (value["key.doclength"] as? Int64).flatMap(ByteCount.init) } /// The attribute for this dictionary, as returned by SourceKit. public var attribute: String? { - return value["key.attribute"] as? String + value["key.attribute"] as? String } /// Module name in `@import` expressions. public var moduleName: String? { - return value["key.modulename"] as? String + value["key.modulename"] as? String } /// The line number for this declaration. public var line: Int64? { - return value["key.line"] as? Int64 + value["key.line"] as? Int64 } /// The column number for this declaration. public var column: Int64? { - return value["key.column"] as? Int64 + value["key.column"] as? Int64 } /// The `SwiftDeclarationAttributeKind` values associated with this dictionary. public var enclosedSwiftAttributes: [SwiftDeclarationAttributeKind] { - return swiftAttributes.compactMap { $0.attribute } + swiftAttributes.compactMap(\.attribute) .compactMap(SwiftDeclarationAttributeKind.init(rawValue:)) } /// The fully preserved SourceKitten dictionaries for all the attributes associated with this dictionary. - public var swiftAttributes: [SourceKittenDictionary] { + public var swiftAttributes: [Self] { let array = value["key.attributes"] as? [any SourceKitRepresentable] ?? [] return array.compactMap { $0 as? [String: any SourceKitRepresentable] } .map(Self.init) } - public var elements: [SourceKittenDictionary] { + public var elements: [Self] { let elements = value["key.elements"] as? [any SourceKitRepresentable] ?? [] return elements.compactMap { $0 as? [String: any SourceKitRepresentable] } .map(Self.init) } - public var entities: [SourceKittenDictionary] { + public var entities: [Self] { let entities = value["key.entities"] as? [any SourceKitRepresentable] ?? [] return entities.compactMap { $0 as? [String: any SourceKitRepresentable] } .map(Self.init) } - public var enclosedVarParameters: [SourceKittenDictionary] { - return substructure.flatMap { subDict -> [SourceKittenDictionary] in + public var enclosedVarParameters: [Self] { + substructure.flatMap { subDict -> [Self] in if subDict.declarationKind == .varParameter { return [subDict] } @@ -167,8 +167,8 @@ public struct SourceKittenDictionary { } } - public var enclosedArguments: [SourceKittenDictionary] { - return substructure.flatMap { subDict -> [SourceKittenDictionary] in + public var enclosedArguments: [Self] { + substructure.flatMap { subDict -> [Self] in guard subDict.expressionKind == .argument else { return [] } @@ -182,7 +182,7 @@ public struct SourceKittenDictionary { return array.compactMap { ($0 as? [String: String]).flatMap { $0["key.name"] } } } - public var secondarySymbols: [SourceKittenDictionary] { + public var secondarySymbols: [Self] { let array = value["key.secondary_symbols"] as? [any SourceKitRepresentable] ?? [] return array.compactMap { $0 as? [String: any SourceKitRepresentable] } .map(Self.init) @@ -190,7 +190,10 @@ public struct SourceKittenDictionary { } extension SourceKittenDictionary { - /// Traversing all substuctures of the dictionary hierarchically, calling `traverseBlock` on each node. + /// Block executed for every encountered entity during traversal of a dictionary. + public typealias TraverseBlock = (_ parent: SourceKittenDictionary, _ entity: SourceKittenDictionary) -> T? + + /// Traversing all substructures of the dictionary hierarchically, calling `traverseBlock` on each node. /// Traversing using depth first strategy, so deepest substructures will be passed to `traverseBlock` first. /// /// - parameter traverseBlock: block that will be called for each substructure in the dictionary. @@ -216,21 +219,20 @@ extension SourceKittenDictionary { /// Traversing all entities of the dictionary hierarchically, calling `traverseBlock` on each node. /// Traversing using depth first strategy, so deepest substructures will be passed to `traverseBlock` first. /// - /// - parameter traverseBlock: block that will be called for each entity in the dictionary. + /// - parameter traverseBlock: Block that will be called for each entity and its parent in the dictionary. /// /// - returns: The list of entity dictionaries with updated values from the traverse block. - public func traverseEntitiesDepthFirst(traverseBlock: (SourceKittenDictionary) -> T?) -> [T] { + public func traverseEntitiesDepthFirst(traverseBlock: TraverseBlock) -> [T] { var result: [T] = [] traverseEntitiesDepthFirst(collectingValuesInto: &result, traverseBlock: traverseBlock) return result } - private func traverseEntitiesDepthFirst(collectingValuesInto array: inout [T], - traverseBlock: (SourceKittenDictionary) -> T?) { + private func traverseEntitiesDepthFirst(collectingValuesInto array: inout [T], traverseBlock: TraverseBlock) { entities.forEach { subDict in subDict.traverseEntitiesDepthFirst(collectingValuesInto: &array, traverseBlock: traverseBlock) - if let collectedValue = traverseBlock(subDict) { + if let collectedValue = traverseBlock(self, subDict) { array.append(collectedValue) } } @@ -242,8 +244,8 @@ public extension Dictionary where Key == Example { /// /// - returns: A new `Dictionary`. func removingViolationMarkers() -> [Key: Value] { - return Dictionary(uniqueKeysWithValues: map { key, value in - return (key.removingViolationMarkers(), value) + Dictionary(uniqueKeysWithValues: map { key, value in + (key.removingViolationMarkers(), value) }) } } diff --git a/Source/SwiftLintCore/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintCore/Extensions/FileManager+SwiftLint.swift index 4435ed037d..c5aad909a5 100644 --- a/Source/SwiftLintCore/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/FileManager+SwiftLint.swift @@ -47,7 +47,7 @@ extension FileManager: LintableFileManager { } public func modificationDate(forFileAtPath path: String) -> Date? { - return (try? attributesOfItem(atPath: path))?[.modificationDate] as? Date + (try? attributesOfItem(atPath: path))?[.modificationDate] as? Date } public func isFile(atPath path: String) -> Bool { diff --git a/Source/SwiftLintCore/Extensions/NSRange+SwiftLint.swift b/Source/SwiftLintCore/Extensions/NSRange+SwiftLint.swift index 69df673e93..cbe3d1e391 100644 --- a/Source/SwiftLintCore/Extensions/NSRange+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/NSRange+SwiftLint.swift @@ -2,7 +2,7 @@ import Foundation extension NSRange { func intersects(_ range: NSRange) -> Bool { - return NSIntersectionRange(self, range).length > 0 + NSIntersectionRange(self, range).length > 0 } func intersects(_ ranges: [NSRange]) -> Bool { diff --git a/Source/SwiftLintCore/Extensions/NSRegularExpression+SwiftLint.swift b/Source/SwiftLintCore/Extensions/NSRegularExpression+SwiftLint.swift index 1a0c7c94c0..4f392828ea 100644 --- a/Source/SwiftLintCore/Extensions/NSRegularExpression+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/NSRegularExpression+SwiftLint.swift @@ -7,7 +7,7 @@ private let regexCacheLock = NSLock() public struct RegularExpression: Hashable, Comparable, ExpressibleByStringLiteral { public let regex: NSRegularExpression - public init(pattern: String, options: NSRegularExpression.Options? = nil) throws { + public init(pattern: String, options _: NSRegularExpression.Options? = nil) throws { regex = try .cached(pattern: pattern) } public init(stringLiteral value: String) { @@ -34,12 +34,6 @@ private struct RegexCacheKey: Hashable { } } -extension NSRegularExpression: Comparable { - public static func < (lhs: NSRegularExpression, rhs: NSRegularExpression) -> Bool { - lhs.pattern < rhs.pattern - } -} - public extension NSRegularExpression { static func cached(pattern: String, options: Options? = nil) throws -> NSRegularExpression { let options = options ?? [.anchorsMatchLines, .dotMatchesLineSeparators] @@ -57,17 +51,17 @@ public extension NSRegularExpression { func matches(in stringView: StringView, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] { - return matches(in: stringView.string, options: options, range: stringView.range) + matches(in: stringView.string, options: options, range: stringView.range) } func matches(in stringView: StringView, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> [NSTextCheckingResult] { - return matches(in: stringView.string, options: options, range: range) + matches(in: stringView.string, options: options, range: range) } func matches(in file: SwiftLintFile, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] { - return matches(in: file.stringView.string, options: options, range: file.stringView.range) + matches(in: file.stringView.string, options: options, range: file.stringView.range) } } diff --git a/Source/SwiftLintCore/Extensions/QueuedPrint.swift b/Source/SwiftLintCore/Extensions/QueuedPrint.swift index f4be8d4172..bc898c5b5d 100644 --- a/Source/SwiftLintCore/Extensions/QueuedPrint.swift +++ b/Source/SwiftLintCore/Extensions/QueuedPrint.swift @@ -17,7 +17,10 @@ private let outputQueue: DispatchQueue = { private func setupAtExitHandler() { atexit { - outputQueue.sync(flags: .barrier) {} + // Ensure all queued output is written before exiting. + outputQueue.sync(flags: .barrier) { + // Just wait. + } } } diff --git a/Source/SwiftLintCore/Extensions/RandomAccessCollection+Swiftlint.swift b/Source/SwiftLintCore/Extensions/RandomAccessCollection+Swiftlint.swift index 1678cb8f3f..e347d3f38b 100644 --- a/Source/SwiftLintCore/Extensions/RandomAccessCollection+Swiftlint.swift +++ b/Source/SwiftLintCore/Extensions/RandomAccessCollection+Swiftlint.swift @@ -24,7 +24,7 @@ public extension RandomAccessCollection where Index == Int { @inlinable func firstIndexAssumingSorted(where predicate: (Self.Element) throws -> Bool) rethrows -> Int? { // Predicate should divide a collection to two pairs of values - // "bad" values for which predicate returns `false`` + // "bad" values for which predicate returns `false` // "good" values for which predicate return `true` // false false false false false true true true diff --git a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift index 5638f9dea4..34acd32dc4 100644 --- a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift @@ -19,7 +19,7 @@ public extension Request { "key.offset": Int64(offset.value), "key.compilerargs": arguments, "key.cancel_on_subsequent_request": 0, - "key.retrieve_symbol_graph": 0 + "key.retrieve_symbol_graph": 0, ]) } } diff --git a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift index 44a7969490..49839438b7 100644 --- a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift @@ -15,11 +15,11 @@ public extension String { } func isUppercase() -> Bool { - return self == uppercased() + self == uppercased() } func isLowercase() -> Bool { - return self == lowercased() + self == lowercased() } private subscript (range: Range) -> String { @@ -63,14 +63,14 @@ public extension String { } var fullNSRange: NSRange { - return NSRange(location: 0, length: utf16.count) + NSRange(location: 0, length: utf16.count) } /// Returns a new string, converting the path to a canonical absolute path. /// /// - returns: A new `String`. func absolutePathStandardized() -> String { - return bridge().absolutePathRepresentation().bridge().standardizingPath + bridge().absolutePathRepresentation().bridge().standardizingPath } var isFile: Bool { @@ -88,7 +88,7 @@ public extension String { /// - Parameter character: Character to count /// - Returns: Number of times `character` occurs in `self` func countOccurrences(of character: Character) -> Int { - return self.reduce(0, { + self.reduce(0, { $1 == character ? $0 + 1 : $0 }) } @@ -124,4 +124,8 @@ public extension String { .map { String(repeating: " ", count: spaces) + $0 } .joined(separator: "\n") } + + func linesPrefixed(with prefix: Self) -> Self { + split(separator: "\n").joined(separator: "\n\(prefix)") + } } diff --git a/Source/SwiftLintCore/Extensions/String+XML.swift b/Source/SwiftLintCore/Extensions/String+XML.swift index 280e1b00c6..bdb486c337 100644 --- a/Source/SwiftLintCore/Extensions/String+XML.swift +++ b/Source/SwiftLintCore/Extensions/String+XML.swift @@ -6,7 +6,7 @@ extension String { ("\"", """), ("'", "'"), (">", ">"), - ("<", "<") + ("<", "<"), ] var newString = self for (key, value) in htmlEscapes { diff --git a/Source/SwiftLintCore/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift b/Source/SwiftLintCore/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift index ace97f9a29..26d2f2ef5b 100644 --- a/Source/SwiftLintCore/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift @@ -2,11 +2,11 @@ import SourceKittenFramework public extension SwiftDeclarationAttributeKind { static var attributesRequiringFoundation: Set { - return [ + [ .objc, .objcName, .objcMembers, - .objcNonLazyRealization + .objcNonLazyRealization, ] } @@ -27,7 +27,7 @@ public extension SwiftDeclarationAttributeKind { public init?(rawAttribute: String) { let allModifierGroups: Set = [ .acl, .setterACL, .mutators, .override, .owned, .atPrefixed, .dynamic, .final, .typeMethods, - .required, .convenience, .lazy + .required, .convenience, .lazy, ] let modifierGroup = allModifierGroups.first { $0.swiftDeclarationAttributeKinds.contains(where: { $0.rawValue == rawAttribute }) @@ -48,7 +48,7 @@ public extension SwiftDeclarationAttributeKind { .fileprivate, .internal, .public, - .open + .open, ] case .setterACL: return [ @@ -56,12 +56,12 @@ public extension SwiftDeclarationAttributeKind { .setterFilePrivate, .setterInternal, .setterPublic, - .setterOpen + .setterOpen, ] case .mutators: return [ .mutating, - .nonmutating + .nonmutating, ] case .override: return [.override] @@ -89,13 +89,13 @@ public extension SwiftDeclarationAttributeKind { .ibdesignable, .ibinspectable, .nsManaged, - .nsCopying + .nsCopying, ] } } public var debugDescription: String { - return self.rawValue + self.rawValue } } } diff --git a/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift index 8bf1ae85ce..17cd5f2b51 100644 --- a/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift @@ -7,7 +7,7 @@ public extension SwiftDeclarationKind { .varInstance, .varLocal, .varParameter, - .varStatic + .varStatic, ] static let functionKinds: Set = [ @@ -24,7 +24,7 @@ public extension SwiftDeclarationKind { .functionMethodInstance, .functionMethodStatic, .functionOperator, - .functionSubscript + .functionSubscript, ] static let typeKinds: Set = [ @@ -32,7 +32,7 @@ public extension SwiftDeclarationKind { .struct, .typealias, .associatedtype, - .enum + .enum, ] static let extensionKinds: Set = [ @@ -40,6 +40,6 @@ public extension SwiftDeclarationKind { .extensionClass, .extensionEnum, .extensionProtocol, - .extensionStruct + .extensionStruct, ] } diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift index 3676560514..65a9bf58bd 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift @@ -22,18 +22,18 @@ private let responseCache = Cache { file -> [String: any SourceKitRepresentable] } } private let structureDictionaryCache = Cache { file in - return responseCache.get(file).map(Structure.init).map { SourceKittenDictionary($0.dictionary) } + responseCache.get(file).map(Structure.init).map { SourceKittenDictionary($0.dictionary) } } private let syntaxTreeCache = Cache { file -> SourceFileSyntax in - return Parser.parse(source: file.contents) + Parser.parse(source: file.contents) } private let foldedSyntaxTreeCache = Cache { file -> SourceFileSyntax? in - return OperatorTable.standardOperators - .foldAll(file.syntaxTree) { _ in } + OperatorTable.standardOperators + .foldAll(file.syntaxTree) { _ in /* Don't handle errors. */ } .as(SourceFileSyntax.self) } private let locationConverterCache = Cache { file -> SourceLocationConverter in - return SourceLocationConverter(fileName: file.path ?? "", tree: file.syntaxTree) + SourceLocationConverter(fileName: file.path ?? "", tree: file.syntaxTree) } private let commandsCache = Cache { file -> [Command] in guard file.contents.contains("swiftlint:") else { @@ -98,12 +98,12 @@ private class Cache { extension SwiftLintFile { fileprivate var cacheKey: FileCacheKey { - return id + id } public var sourcekitdFailed: Bool { get { - return responseCache.get(self) == nil + responseCache.get(self) == nil } set { if newValue { @@ -116,7 +116,7 @@ extension SwiftLintFile { internal var assertHandler: AssertHandler? { get { - return assertHandlerCache.get(self) + assertHandlerCache.get(self) } set { assertHandlerCache.set(key: cacheKey, value: newValue) @@ -165,7 +165,7 @@ extension SwiftLintFile { public var locationConverter: SourceLocationConverter { locationConverterCache.get(self) } - public var commands: [Command] { commandsCache.get(self).filter { $0.isValid } } + public var commands: [Command] { commandsCache.get(self).filter(\.isValid) } public var invalidCommands: [Command] { commandsCache.get(self).filter { !$0.isValid } } diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift index 41d8b10204..c8fe43d36d 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift @@ -18,7 +18,7 @@ extension SwiftLintFile { let commands: [Command] if let restrictingRuleIdentifiers { commands = self.commands().filter { command in - return command.ruleIdentifiers.contains(where: restrictingRuleIdentifiers.contains) + command.ruleIdentifiers.contains(where: restrictingRuleIdentifiers.contains) } } else { commands = self.commands() @@ -94,9 +94,9 @@ extension SwiftLintFile { } public func match(pattern: String, with syntaxKinds: [SyntaxKind], range: NSRange? = nil) -> [NSRange] { - return match(pattern: pattern, range: range) + match(pattern: pattern, range: range) .filter { $0.1 == syntaxKinds } - .map { $0.0 } + .map(\.0) } public func matchesAndTokens(matching pattern: String, @@ -112,18 +112,18 @@ extension SwiftLintFile { public func matchesAndSyntaxKinds(matching pattern: String, range: NSRange? = nil) -> [(NSTextCheckingResult, [SyntaxKind])] { - return matchesAndTokens(matching: pattern, range: range).map { textCheckingResult, tokens in + matchesAndTokens(matching: pattern, range: range).map { textCheckingResult, tokens in (textCheckingResult, tokens.kinds) } } public func rangesAndTokens(matching pattern: String, range: NSRange? = nil) -> [(NSRange, [SwiftLintSyntaxToken])] { - return matchesAndTokens(matching: pattern, range: range).map { ($0.0.range, $0.1) } + matchesAndTokens(matching: pattern, range: range).map { ($0.0.range, $0.1) } } public func match(pattern: String, range: NSRange? = nil, captureGroup: Int = 0) -> [(NSRange, [SyntaxKind])] { - return matchesAndSyntaxKinds(matching: pattern, range: range).map { textCheckingResult, syntaxKinds in + matchesAndSyntaxKinds(matching: pattern, range: range).map { textCheckingResult, syntaxKinds in (textCheckingResult.range(at: captureGroup), syntaxKinds) } } @@ -185,7 +185,7 @@ extension SwiftLintFile { return nil } - return tokens.map { $0.kinds } + return tokens.map(\.kinds) } /** @@ -203,9 +203,9 @@ extension SwiftLintFile { excludingSyntaxKinds syntaxKinds: Set, range: NSRange? = nil, captureGroup: Int = 0) -> [NSRange] { - return match(pattern: pattern, range: range, captureGroup: captureGroup) + match(pattern: pattern, range: range, captureGroup: captureGroup) .filter { syntaxKinds.isDisjoint(with: $0.1) } - .map { $0.0 } + .map(\.0) } public typealias MatchMapping = (NSTextCheckingResult) -> NSRange @@ -214,7 +214,7 @@ extension SwiftLintFile { range: NSRange? = nil, excludingSyntaxKinds: Set, excludingPattern: String, - exclusionMapping: MatchMapping = { $0.range }) -> [NSRange] { + exclusionMapping: MatchMapping = \.range) -> [NSRange] { let matches = match(pattern: pattern, excludingSyntaxKinds: excludingSyntaxKinds) if matches.isEmpty { return [] @@ -279,7 +279,7 @@ extension SwiftLintFile { } public func ruleEnabled(violatingRange: NSRange, for rule: some Rule) -> NSRange? { - return ruleEnabled(violatingRanges: [violatingRange], for: rule).first + ruleEnabled(violatingRanges: [violatingRange], for: rule).first } public func isACL(token: SwiftLintSyntaxToken) -> Bool { @@ -292,6 +292,6 @@ extension SwiftLintFile { } public func contents(for token: SwiftLintSyntaxToken) -> String? { - return stringView.substringWithByteRange(token.range) + stringView.substringWithByteRange(token.range) } } diff --git a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift index c00399fc60..521c6fcbc6 100644 --- a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift @@ -12,7 +12,7 @@ public extension SwiftLintSyntaxVisitor { } func walk(file: SwiftLintFile, handler: (Self) -> [T]) -> [T] { - return walk(tree: file.syntaxTree, handler: handler) + walk(tree: file.syntaxTree, handler: handler) } } @@ -100,6 +100,16 @@ public extension DeclModifierListSyntax { first { $0.asAccessLevelModifier != nil } } + func accessLevelModifier(setter: Bool = false) -> DeclModifierSyntax? { + first { + if $0.asAccessLevelModifier == nil { + return false + } + let hasSetDetail = $0.detail?.detail.tokenKind == .identifier("set") + return setter ? hasSetDetail : !hasSetDetail + } + } + func contains(keyword: Keyword) -> Bool { contains { $0.name.tokenKind == .keyword(keyword) } } @@ -163,7 +173,7 @@ public extension EnumDeclSyntax { "Int", "Int8", "Int16", "Int32", "Int64", "UInt", "UInt8", "UInt16", "UInt32", "UInt64", "Double", "Float", "Float80", "Decimal", "NSNumber", - "NSDecimalNumber", "NSInteger", "String", "CGFloat" + "NSDecimalNumber", "NSInteger", "String", "CGFloat", ] return inheritedTypeCollection.contains { element in @@ -254,6 +264,12 @@ public extension Trivia { } } + var containsComments: Bool { + isNotEmpty && contains { piece in + !piece.isWhitespace && !piece.isNewline + } + } + var isSingleSpace: Bool { self == .spaces(1) } @@ -333,8 +349,6 @@ public extension ClosureCaptureSyntax { } } -extension PrecedenceGroupDeclSyntax: BracedSyntax {} - private extension String { var isZero: Bool { if self == "0" { // fast path diff --git a/Source/SwiftLintCore/Extensions/SyntaxClassification+isComment.swift b/Source/SwiftLintCore/Extensions/SyntaxClassification+isComment.swift index 80178a11c4..e579c65d4b 100644 --- a/Source/SwiftLintCore/Extensions/SyntaxClassification+isComment.swift +++ b/Source/SwiftLintCore/Extensions/SyntaxClassification+isComment.swift @@ -6,7 +6,7 @@ public extension SyntaxClassification { switch self { case .lineComment, .docLineComment, .blockComment, .docBlockComment: return true - case .none, .keyword, .identifier, .type, .operator, .dollarIdentifier, .integerLiteral, + case .none, .keyword, .identifier, .type, .operator, .dollarIdentifier, .integerLiteral, .argumentLabel, .floatLiteral, .stringLiteral, .ifConfigDirective, .attribute, .editorPlaceholder, .regexLiteral: return false } diff --git a/Source/SwiftLintCore/Extensions/SyntaxKind+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SyntaxKind+SwiftLint.swift index 75261f2d28..8099f0ca37 100644 --- a/Source/SwiftLintCore/Extensions/SyntaxKind+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SyntaxKind+SwiftLint.swift @@ -10,18 +10,22 @@ public extension SyntaxKind { static let commentAndStringKinds: Set = commentKinds.union([.string]) - static let commentKinds: Set = [.comment, .commentMark, .commentURL, - .docComment, .docCommentField] + static let commentKinds: Set = [ + .comment, .commentMark, .commentURL, + .docComment, .docCommentField, + ] - static let allKinds: Set = [.argument, .attributeBuiltin, .attributeID, .buildconfigID, - .buildconfigKeyword, .comment, .commentMark, .commentURL, - .docComment, .docCommentField, .identifier, .keyword, .number, - .objectLiteral, .parameter, .placeholder, .string, - .stringInterpolationAnchor, .typeidentifier] + static let allKinds: Set = [ + .argument, .attributeBuiltin, .attributeID, .buildconfigID, + .buildconfigKeyword, .comment, .commentMark, .commentURL, + .docComment, .docCommentField, .identifier, .keyword, .number, + .objectLiteral, .parameter, .placeholder, .string, + .stringInterpolationAnchor, .typeidentifier, + ] /// Syntax kinds that don't have associated module info when getting their cursor info. static var kindsWithoutModuleInfo: Set { - return [ + [ .attributeBuiltin, .keyword, .number, @@ -33,7 +37,7 @@ public extension SyntaxKind { .buildconfigID, .commentURL, .comment, - .docCommentField + .docCommentField, ] } } diff --git a/Source/SwiftLintCore/Helpers/Macros.swift b/Source/SwiftLintCore/Helpers/Macros.swift index 45df01a6a1..7d6270d2ac 100644 --- a/Source/SwiftLintCore/Helpers/Macros.swift +++ b/Source/SwiftLintCore/Helpers/Macros.swift @@ -4,9 +4,20 @@ member, names: named(apply) ) +public macro AutoConfigParser() = #externalMacro( + module: "SwiftLintCoreMacros", + type: "AutoConfigParser" +) + +/// Deprecated. Use `AutoConfigParser` instead. +@available(*, deprecated, renamed: "AutoConfigParser") +@attached( + member, + names: named(apply) +) public macro AutoApply() = #externalMacro( module: "SwiftLintCoreMacros", - type: "AutoApply" + type: "AutoConfigParser" ) /// Macro that lets an enum with a ``String`` raw type automatically conform to ``AcceptableByConfigurationElement``. @@ -15,9 +26,21 @@ public macro AutoApply() = #externalMacro( conformances: AcceptableByConfigurationElement, names: named(init(fromAny:context:)), named(asOption) ) +public macro AcceptableByConfigurationElement() = #externalMacro( + module: "SwiftLintCoreMacros", + type: "AcceptableByConfigurationElement" +) + +/// Deprecated. Use `AcceptableByConfigurationElement` instead. +@available(*, deprecated, renamed: "AcceptableByConfigurationElement") +@attached( + extension, + conformances: AcceptableByConfigurationElement, + names: named(init(fromAny:context:)), named(asOption) +) public macro MakeAcceptableByConfigurationElement() = #externalMacro( module: "SwiftLintCoreMacros", - type: "MakeAcceptableByConfigurationElement" + type: "AcceptableByConfigurationElement" ) /// Macro that adds a conformance to the ``SwiftSyntaxRule`` protocol and a default `makeVisitor(file:)` implementation @@ -27,7 +50,7 @@ public macro MakeAcceptableByConfigurationElement() = #externalMacro( /// - foldExpressions: Setting it to `true` adds an implementation of `preprocess(file:)` which folds expressions /// before they are passed to the visitor. /// - explicitRewriter: Set it to `true` to add a `makeRewriter(file:)` implementation which creates a rewriter -/// defined in the rule struct. In this case, the rule automatically conforms to +/// defined in the rule struct. In this case, the rule automatically conforms to /// ``SwiftSyntaxCorrectableRule``. @attached( extension, diff --git a/Source/SwiftLintCore/Helpers/Stack.swift b/Source/SwiftLintCore/Helpers/Stack.swift index f5b8573678..794919bf26 100644 --- a/Source/SwiftLintCore/Helpers/Stack.swift +++ b/Source/SwiftLintCore/Helpers/Stack.swift @@ -3,7 +3,7 @@ public struct Stack { private var elements = [Element]() /// Creates an empty `Stack`. - public init() {} + public init() { /* Publish no-op initializer */ } /// The number of elements in this stack. public var count: Int { @@ -49,6 +49,12 @@ public struct Stack { } } +extension Stack: Sequence { + public func makeIterator() -> [Element].Iterator { + elements.makeIterator() + } +} + extension Stack: CustomDebugStringConvertible where Element: CustomDebugStringConvertible { public var debugDescription: String { let intermediateElements = count > 1 ? elements[1 ..< count - 1] : [] diff --git a/Source/SwiftLintCore/Models/AccessControlLevel.swift b/Source/SwiftLintCore/Models/AccessControlLevel.swift index 5e23aca586..160956d0b8 100644 --- a/Source/SwiftLintCore/Models/AccessControlLevel.swift +++ b/Source/SwiftLintCore/Models/AccessControlLevel.swift @@ -8,6 +8,8 @@ public enum AccessControlLevel: String, CustomStringConvertible { case `fileprivate` = "source.lang.swift.accessibility.fileprivate" /// Accessible by the declaration's same module, or modules importing it with the `@testable` attribute. case `internal` = "source.lang.swift.accessibility.internal" + /// Accessible by all the modules defined in the same Swift package. + case `package` = "source.lang.swift.accessibility.package" /// Accessible by the declaration's same program. case `public` = "source.lang.swift.accessibility.public" /// Accessible and customizable (via subclassing or overrides) by the declaration's same program. @@ -21,6 +23,7 @@ public enum AccessControlLevel: String, CustomStringConvertible { case "private": self = .private case "fileprivate": self = .fileprivate case "internal": self = .internal + case "package": self = .package case "public": self = .public case "open": self = .open default: return nil @@ -39,6 +42,7 @@ public enum AccessControlLevel: String, CustomStringConvertible { case .private: return "private" case .fileprivate: return "fileprivate" case .internal: return "internal" + case .package: return "package" case .public: return "public" case .open: return "open" } @@ -46,7 +50,7 @@ public enum AccessControlLevel: String, CustomStringConvertible { /// Returns true if is `private` or `fileprivate` public var isPrivate: Bool { - return self == .private || self == .fileprivate + self == .private || self == .fileprivate } } @@ -56,12 +60,13 @@ extension AccessControlLevel: Comparable { case .private: return 1 case .fileprivate: return 2 case .internal: return 3 - case .public: return 4 - case .open: return 5 + case .package: return 4 + case .public: return 5 + case .open: return 6 } } public static func < (lhs: AccessControlLevel, rhs: AccessControlLevel) -> Bool { - return lhs.priority < rhs.priority + lhs.priority < rhs.priority } } diff --git a/Source/SwiftLintCore/Models/Baseline.swift b/Source/SwiftLintCore/Models/Baseline.swift new file mode 100644 index 0000000000..62aa573e2f --- /dev/null +++ b/Source/SwiftLintCore/Models/Baseline.swift @@ -0,0 +1,206 @@ +import Foundation + +private typealias BaselineViolations = [BaselineViolation] +private typealias ViolationsPerFile = [String: BaselineViolations] +private typealias ViolationsPerRule = [String: BaselineViolations] + +private struct BaselineViolation: Codable, Hashable, Comparable { + let violation: StyleViolation + let text: String + var key: String { text + violation.reason } + + init(violation: StyleViolation, text: String) { + let location = violation.location + self.violation = violation.with(location: Location( + // Within the baseline, we use relative paths, so that + // comparisons are independent of the absolute path + file: location.relativeFile, + line: location.line, + character: location.character) + ) + self.text = text + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.violation == rhs.violation && lhs.text == rhs.text + } + + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.violation.location == rhs.violation.location + ? lhs.violation.ruleIdentifier < rhs.violation.ruleIdentifier + : lhs.violation.location < rhs.violation.location + } +} + +/// A set of violations that can be used to filter newly detected violations. +public struct Baseline: Equatable { + private let baseline: ViolationsPerFile + private var sortedBaselineViolations: BaselineViolations { + baseline.flatMap(\.value).sorted() + } + + /// The stored violations. + public var violations: [StyleViolation] { + sortedBaselineViolations.violationsWithAbsolutePaths + } + + /// Creates a `Baseline` from a saved file. + /// + /// - parameter fromPath: The path to read from. + public init(fromPath path: String) throws { + let data = try Data(contentsOf: URL(fileURLWithPath: path)) + baseline = try JSONDecoder().decode(BaselineViolations.self, from: data).groupedByFile() + } + + /// Creates a `Baseline` from a list of violations. + /// + /// - parameter violations: The violations for the baseline. + public init(violations: [StyleViolation]) { + baseline = BaselineViolations(violations).groupedByFile() + } + + /// Writes a `Baseline` to disk in JSON format. + /// + /// - parameter toPath: The path to write to. + public func write(toPath path: String) throws { + let data = try JSONEncoder().encode(sortedBaselineViolations) + try data.write(to: URL(fileURLWithPath: path)) + } + + /// Filters out violations that are present in the `Baseline`. + /// + /// Assumes that all violations are from the same file. + /// + /// - parameter violations: The violations to filter. + /// - Returns: The new violations. + public func filter(_ violations: [StyleViolation]) -> [StyleViolation] { + guard let firstViolation = violations.first, + let baselineViolations = baseline[firstViolation.location.relativeFile ?? ""], + baselineViolations.isNotEmpty else { + return violations + } + + let relativePathViolations = BaselineViolations(violations) + let violationsWithAbsolutePaths = filter( + relativePathViolations: relativePathViolations, + baselineViolations: baselineViolations + ) + return violations.filter { violationsWithAbsolutePaths.contains($0) } + } + + private func filter( + relativePathViolations: BaselineViolations, baselineViolations: BaselineViolations + ) -> Set { + if relativePathViolations == baselineViolations { + return [] + } + + let violationsByRuleIdentifier = relativePathViolations.groupedByRuleIdentifier( + filteredBy: baselineViolations + ) + let baselineViolationsByRuleIdentifier = baselineViolations.groupedByRuleIdentifier( + filteredBy: relativePathViolations + ) + + var filteredViolations: Set = [] + + for (ruleIdentifier, ruleViolations) in violationsByRuleIdentifier { + guard + let baselineViolations = baselineViolationsByRuleIdentifier[ruleIdentifier], + baselineViolations.isNotEmpty else { + filteredViolations.formUnion(ruleViolations) + continue + } + + let groupedRuleViolations = Dictionary(grouping: ruleViolations, by: \.key) + let groupedBaselineViolations = Dictionary(grouping: baselineViolations, by: \.key) + + for (key, ruleViolations) in groupedRuleViolations { + guard let baselineViolations = groupedBaselineViolations[key] else { + filteredViolations.formUnion(ruleViolations) + continue + } + if ruleViolations.count > baselineViolations.count { + filteredViolations.formUnion(ruleViolations) + } + } + } + + return Set(filteredViolations.violationsWithAbsolutePaths) + } + + /// Returns the violations that are present in another `Baseline`, but not in this one. + /// + /// The violations are filtered using the same algorithm as the `filter` method above. + /// + /// - parameter otherBaseline: The other `Baseline`. + public func compare(_ otherBaseline: Self) -> [StyleViolation] { + otherBaseline.baseline.flatMap { relativePath, otherBaselineViolations -> Set in + if let baselineViolations = baseline[relativePath] { + return filter(relativePathViolations: otherBaselineViolations, baselineViolations: baselineViolations) + } + return Set(otherBaselineViolations.violationsWithAbsolutePaths) + }.sorted { + $0.location == $1.location ? $0.ruleIdentifier < $1.ruleIdentifier : $0.location < $1.location + } + } +} + +private struct LineCache { + private var lines: [String: [String]] = [:] + + mutating func text(at location: Location) -> String { + let line = (location.line ?? 0) - 1 + if line > 0, let file = location.file, let content = cached(file: file), line < content.count { + return content[line] + } + return "" + } + + private mutating func cached(file: String) -> [String]? { + if let fileLines = lines[file] { + return fileLines + } + if let fileLines = SwiftLintFile(path: file)?.lines.map(\.content) { + lines[file] = fileLines + return fileLines + } + return nil + } +} + +private extension Sequence where Element == BaselineViolation { + init(_ violations: [StyleViolation]) where Self == BaselineViolations { + var lineCache = LineCache() + self = violations.map { $0.baselineViolation(text: lineCache.text(at: $0.location)) } + } + + var violationsWithAbsolutePaths: [StyleViolation] { + map(\.violation.withAbsolutePath) + } + + func groupedByFile() -> ViolationsPerFile { + Dictionary(grouping: self) { $0.violation.location.relativeFile ?? "" } + } + + func groupedByRuleIdentifier(filteredBy existingViolations: [BaselineViolation] = []) -> ViolationsPerRule { + Dictionary(grouping: Set(self).subtracting(existingViolations), by: \.violation.ruleIdentifier) + } +} + +private extension StyleViolation { + var withAbsolutePath: StyleViolation { + let absolutePath: String? = + if let relativePath = location.file { + FileManager.default.currentDirectoryPath + "/" + relativePath + } else { + nil + } + let newLocation = Location(file: absolutePath, line: location.line, character: location.character) + return with(location: newLocation) + } + + func baselineViolation(text: String = "") -> BaselineViolation { + BaselineViolation(violation: self, text: text) + } +} diff --git a/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift b/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift index 3c867c6bb7..4f63881029 100644 --- a/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift +++ b/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift @@ -1,6 +1,6 @@ /// A rule configuration that allows to disable (`off`) an option of a rule or specify its severity level in which /// case it's active. -public struct ChildOptionSeverityConfiguration: RuleConfiguration { +public struct ChildOptionSeverityConfiguration: RuleConfiguration, AcceptableByConfigurationElement { /// Configuration with a warning severity. public static var error: Self { Self(optionSeverity: .error) } /// Configuration with an error severity. @@ -23,7 +23,7 @@ public struct ChildOptionSeverityConfiguration: RuleConfiguration public mutating func apply(configuration: Any) throws { guard let configString = configuration as? String, let optionSeverity = ChildOptionSeverity(rawValue: configString.lowercased()) else { - throw Issue.unknownConfiguration(ruleID: Parent.description.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } self.optionSeverity = optionSeverity } diff --git a/Source/SwiftLintCore/Models/Command.swift b/Source/SwiftLintCore/Models/Command.swift index d9efb2ac61..375cdf00a6 100644 --- a/Source/SwiftLintCore/Models/Command.swift +++ b/Source/SwiftLintCore/Models/Command.swift @@ -13,7 +13,7 @@ public struct Command: Equatable { /// - returns: The inverse action that can cancel out the current action, restoring the SwifttLint engine's /// state prior to the current action. - internal func inverse() -> Action { + internal func inverse() -> Self { switch self { case .enable: return .disable case .disable: return .enable @@ -67,8 +67,12 @@ public struct Command: Equatable { /// defined. /// - parameter modifier: This command's modifier, if any. /// - parameter trailingComment: The comment following this command's `-` delimiter, if any. - public init(action: Action, ruleIdentifiers: Set = [], line: Int = 0, - character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) { + public init(action: Action, + ruleIdentifiers: Set = [], + line: Int = 0, + character: Int? = nil, + modifier: Modifier? = nil, + trailingComment: String? = nil) { self.action = action self.ruleIdentifiers = ruleIdentifiers self.line = line @@ -143,7 +147,7 @@ public struct Command: Equatable { /// If the command doesn't have a modifier, it is returned as-is. /// /// - returns: The expanded commands. - internal func expand() -> [Command] { + internal func expand() -> [Self] { guard let modifier else { return [self] } @@ -151,17 +155,17 @@ public struct Command: Equatable { case .previous: return [ Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line - 1), - Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line - 1, character: Int.max) + Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line - 1, character: Int.max), ] case .this: return [ Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line), - Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line, character: Int.max) + Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line, character: Int.max), ] case .next: return [ Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line + 1), - Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, character: Int.max) + Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, character: Int.max), ] case .invalid: return [] diff --git a/Source/SwiftLintCore/Models/Configuration.swift b/Source/SwiftLintCore/Models/Configuration.swift index 2a06b66813..bf5d6d9bc1 100644 --- a/Source/SwiftLintCore/Models/Configuration.swift +++ b/Source/SwiftLintCore/Models/Configuration.swift @@ -5,7 +5,7 @@ import SourceKittenFramework public struct Configuration { // MARK: - Properties: Static /// The default Configuration resulting from an empty configuration file. - public static var `default`: Configuration { + public static var `default`: Self { // This is realized via a getter to account for differences of the current working directory Self() } @@ -27,7 +27,7 @@ public struct Configuration { public let warningThreshold: Int? /// The identifier for the `Reporter` to use to report style violations. - public let reporter: String + public let reporter: String? /// The location of the persisted cache to use with this configuration. public let cachePath: String? @@ -35,9 +35,21 @@ public struct Configuration { /// Allow or disallow SwiftLint to exit successfully when passed only ignored or unlintable files. public let allowZeroLintableFiles: Bool - /// Treat warnings as errors + /// Treat warnings as errors. public let strict: Bool + /// Treat errors as warnings. + public let lenient: Bool + + /// The path to read a baseline from. + public let baseline: String? + + /// The path to write a baseline to. + public let writeBaseline: String? + + /// Check for updates. + public let checkForUpdates: Bool + /// This value is `true` iff the `--config` parameter was used to specify (a) configuration file(s) /// In particular, this means that the value is also `true` if the `--config` parameter /// was used to explicitly specify the default `.swiftlint.yml` as the configuration file @@ -70,10 +82,14 @@ public struct Configuration { excludedPaths: [String], indentation: IndentationStyle, warningThreshold: Int?, - reporter: String, + reporter: String?, cachePath: String?, allowZeroLintableFiles: Bool, - strict: Bool + strict: Bool, + lenient: Bool, + baseline: String?, + writeBaseline: String?, + checkForUpdates: Bool ) { self.rulesWrapper = rulesWrapper self.fileGraph = fileGraph @@ -85,12 +101,16 @@ public struct Configuration { self.cachePath = cachePath self.allowZeroLintableFiles = allowZeroLintableFiles self.strict = strict + self.lenient = lenient + self.baseline = baseline + self.writeBaseline = writeBaseline + self.checkForUpdates = checkForUpdates } /// Creates a Configuration by copying an existing configuration. /// /// - parameter copying: The existing configuration to copy. - internal init(copying configuration: Configuration) { + internal init(copying configuration: Self) { rulesWrapper = configuration.rulesWrapper fileGraph = configuration.fileGraph includedPaths = configuration.includedPaths @@ -102,6 +122,10 @@ public struct Configuration { cachePath = configuration.cachePath allowZeroLintableFiles = configuration.allowZeroLintableFiles strict = configuration.strict + lenient = configuration.lenient + baseline = configuration.baseline + writeBaseline = configuration.writeBaseline + checkForUpdates = configuration.checkForUpdates } /// Creates a `Configuration` by specifying its properties directly, @@ -112,20 +136,25 @@ public struct Configuration { /// - parameter allRulesWrapped: The rules with their own configurations already applied. /// - parameter ruleList: The list of all rules. Used for alias resolving and as a fallback /// if `allRulesWrapped` is nil. - /// - parameter filePath The underlaying file graph. If `nil` is specified, a empty file graph - /// with the current working directory as the `rootDirectory` will be used + /// - parameter filePath The underlying file graph. If `nil` is specified, a empty file graph + /// with the current working directory as the `rootDirectory` will be used. /// - parameter includedPaths: Included paths to lint. /// - parameter excludedPaths: Excluded paths to not lint. /// - parameter indentation: The style to use when indenting Swift source code. /// - parameter warningThreshold: The threshold for the number of warnings to tolerate before treating the /// lint as having failed. /// - parameter reporter: The identifier for the `Reporter` to use to report style violations. - /// - parameter cachePath: The location of the persisted cache to use whith this configuration. + /// - parameter cachePath: The location of the persisted cache to use with this configuration. /// - parameter pinnedVersion: The SwiftLint version defined in this configuration. - /// - parameter allowZeroLintableFiles: Allow SwiftLint to exit successfully when passed ignored or unlintable files - /// - parameter strict: Treat warnings as errors + /// - parameter allowZeroLintableFiles: Allow SwiftLint to exit successfully when passed ignored or unlintable + /// files. + /// - parameter strict: Treat warnings as errors. + /// - parameter lenient: Treat errors as warnings. + /// - parameter baseline: The path to read a baseline from. + /// - parameter writeBaseline: The path to write a baseline to. + /// - parameter checkForUpdates: Check for updates to SwiftLint. package init( - rulesMode: RulesMode = .default(disabled: [], optIn: []), + rulesMode: RulesMode = .defaultConfiguration(disabled: [], optIn: []), allRulesWrapped: [ConfigurationRuleWrapper]? = nil, ruleList: RuleList = RuleRegistry.shared.list, fileGraph: FileGraph? = nil, @@ -137,7 +166,11 @@ public struct Configuration { cachePath: String? = nil, pinnedVersion: String? = nil, allowZeroLintableFiles: Bool = false, - strict: Bool = false + strict: Bool = false, + lenient: Bool = false, + baseline: String? = nil, + writeBaseline: String? = nil, + checkForUpdates: Bool = false ) { if let pinnedVersion, pinnedVersion != Version.current.value { queuedPrintError( @@ -160,10 +193,14 @@ public struct Configuration { excludedPaths: excludedPaths, indentation: indentation, warningThreshold: warningThreshold, - reporter: reporter ?? XcodeReporter.identifier, + reporter: reporter, cachePath: cachePath, allowZeroLintableFiles: allowZeroLintableFiles, - strict: strict + strict: strict, + lenient: lenient, + baseline: baseline, + writeBaseline: writeBaseline, + checkForUpdates: checkForUpdates ) } @@ -182,6 +219,7 @@ public struct Configuration { public init( configurationFiles: [String], // No default value here to avoid ambiguous Configuration() initializer enableAllRules: Bool = false, + onlyRule: [String] = [], cachePath: String? = nil, ignoreParentAndChildConfigs: Bool = false, mockedNetworkResults: [String: String] = [:], @@ -201,7 +239,13 @@ public struct Configuration { defer { basedOnCustomConfigurationFiles = hasCustomConfigurationFiles } let currentWorkingDirectory = FileManager.default.currentDirectoryPath.bridge().absolutePathStandardized() - let rulesMode: RulesMode = enableAllRules ? .allEnabled : .default(disabled: [], optIn: []) + let rulesMode: RulesMode = if enableAllRules { + .allCommandLine + } else if onlyRule.isNotEmpty { + .onlyCommandLine(Set(onlyRule)) + } else { + .defaultConfiguration(disabled: [], optIn: []) + } // Try obtaining cached config let cacheIdentifier = "\(currentWorkingDirectory) - \(configurationFiles)" @@ -219,6 +263,7 @@ public struct Configuration { ) let resultingConfiguration = try fileGraph.resultingConfiguration( enableAllRules: enableAllRules, + onlyRule: onlyRule, cachePath: cachePath ) @@ -268,14 +313,18 @@ extension Configuration: Hashable { hasher.combine(reporter) hasher.combine(allowZeroLintableFiles) hasher.combine(strict) + hasher.combine(lenient) + hasher.combine(baseline) + hasher.combine(writeBaseline) + hasher.combine(checkForUpdates) hasher.combine(basedOnCustomConfigurationFiles) hasher.combine(cachePath) - hasher.combine(rules.map { type(of: $0).description.identifier }) + hasher.combine(rules.map { type(of: $0).identifier }) hasher.combine(fileGraph) } public static func == (lhs: Configuration, rhs: Configuration) -> Bool { - return lhs.includedPaths == rhs.includedPaths && + lhs.includedPaths == rhs.includedPaths && lhs.excludedPaths == rhs.excludedPaths && lhs.indentation == rhs.indentation && lhs.warningThreshold == rhs.warningThreshold && @@ -285,22 +334,27 @@ extension Configuration: Hashable { lhs.rules == rhs.rules && lhs.fileGraph == rhs.fileGraph && lhs.allowZeroLintableFiles == rhs.allowZeroLintableFiles && - lhs.strict == rhs.strict + lhs.strict == rhs.strict && + lhs.lenient == rhs.lenient && + lhs.baseline == rhs.baseline && + lhs.writeBaseline == rhs.writeBaseline && + lhs.checkForUpdates == rhs.checkForUpdates && + lhs.rulesMode == rhs.rulesMode } } // MARK: - CustomStringConvertible extension Configuration: CustomStringConvertible { public var description: String { - return "Configuration: \n" + "Configuration: \n" + "- Indentation Style: \(indentation)\n" + "- Included Paths: \(includedPaths)\n" + "- Excluded Paths: \(excludedPaths)\n" + "- Warning Threshold: \(warningThreshold as Optional)\n" + "- Root Directory: \(rootDirectory as Optional)\n" - + "- Reporter: \(reporter)\n" + + "- Reporter: \(reporter ?? "default")\n" + "- Cache Path: \(cachePath as Optional)\n" + "- Computed Cache Description: \(computedCacheDescription as Optional)\n" - + "- Rules: \(rules.map { type(of: $0).description.identifier })" + + "- Rules: \(rules.map { type(of: $0).identifier })" } } diff --git a/Source/SwiftLintCore/Models/Correction.swift b/Source/SwiftLintCore/Models/Correction.swift index 1dcfa0842f..92d7fa1419 100644 --- a/Source/SwiftLintCore/Models/Correction.swift +++ b/Source/SwiftLintCore/Models/Correction.swift @@ -7,7 +7,7 @@ public struct Correction: Equatable, Sendable { /// The console-printable description for this correction. public var consoleDescription: String { - return "\(location) Corrected \(ruleDescription.name)" + "\(location.file ?? ""): Corrected \(ruleDescription.name)" } /// Memberwise initializer. diff --git a/Source/SwiftLintCore/Models/Example.swift b/Source/SwiftLintCore/Models/Example.swift index 5a8a84eed4..1c49edbb15 100644 --- a/Source/SwiftLintCore/Models/Example.swift +++ b/Source/SwiftLintCore/Models/Example.swift @@ -59,9 +59,15 @@ public extension Example { /// Defaults to the file where this initializer is called. /// - line: The line in the file where the example is located. /// Defaults to the line where this initializer is called. - init(_ code: String, configuration: [String: any Sendable]? = nil, testMultiByteOffsets: Bool = true, - testWrappingInComment: Bool = true, testWrappingInString: Bool = true, testDisableCommand: Bool = true, - testOnLinux: Bool = true, file: StaticString = #file, line: UInt = #line, + init(_ code: String, + configuration: [String: any Sendable]? = nil, + testMultiByteOffsets: Bool = true, + testWrappingInComment: Bool = true, + testWrappingInString: Bool = true, + testDisableCommand: Bool = true, + testOnLinux: Bool = true, + file: StaticString = #filePath, + line: UInt = #line, excludeFromDocumentation: Bool = false) { self.code = code self.configuration = configuration @@ -86,7 +92,7 @@ public extension Example { /// Returns a copy of the Example with all instances of the "↓" character removed. func removingViolationMarkers() -> Example { - return with(code: code.replacingOccurrences(of: "↓", with: "")) + with(code: code.replacingOccurrences(of: "↓", with: "")) } } @@ -127,7 +133,7 @@ extension Example: Hashable { public static func == (lhs: Example, rhs: Example) -> Bool { // Ignoring file/line metadata because two Examples could represent // the same idea, but captured at two different points in the code - return lhs.code == rhs.code + lhs.code == rhs.code } public func hash(into hasher: inout Hasher) { @@ -139,7 +145,7 @@ extension Example: Hashable { extension Example: Comparable { public static func < (lhs: Example, rhs: Example) -> Bool { - return lhs.code < rhs.code + lhs.code < rhs.code } } diff --git a/Source/SwiftLintCore/Models/HashableConfigurationRuleWrapperWrapper.swift b/Source/SwiftLintCore/Models/HashableConfigurationRuleWrapperWrapper.swift index af245f6d77..1abe3deae2 100644 --- a/Source/SwiftLintCore/Models/HashableConfigurationRuleWrapperWrapper.swift +++ b/Source/SwiftLintCore/Models/HashableConfigurationRuleWrapperWrapper.swift @@ -2,14 +2,14 @@ internal struct HashableConfigurationRuleWrapperWrapper: Hashable { let configurationRuleWrapper: ConfigurationRuleWrapper static func == ( - lhs: HashableConfigurationRuleWrapperWrapper, rhs: HashableConfigurationRuleWrapperWrapper + lhs: Self, rhs: Self ) -> Bool { // Only use identifier for equality check (not taking config into account) - return type(of: lhs.configurationRuleWrapper.rule).description.identifier - == type(of: rhs.configurationRuleWrapper.rule).description.identifier + type(of: lhs.configurationRuleWrapper.rule).identifier + == type(of: rhs.configurationRuleWrapper.rule).identifier } func hash(into hasher: inout Hasher) { - hasher.combine(type(of: configurationRuleWrapper.rule).description.identifier) + hasher.combine(type(of: configurationRuleWrapper.rule).identifier) } } diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index b26ea6bc75..2a35b36863 100644 --- a/Source/SwiftLintCore/Models/Issue.swift +++ b/Source/SwiftLintCore/Models/Issue.swift @@ -3,7 +3,13 @@ import Foundation /// All possible SwiftLint issues which are printed as warnings by default. public enum Issue: LocalizedError, Equatable { /// The configuration didn't match internal expectations. - case unknownConfiguration(ruleID: String) + case invalidConfiguration(ruleID: String, message: String? = nil) + + /// Issued when an option is deprecated. Suggests an alternative optionally. + case deprecatedConfigurationOption(ruleID: String, key: String, alternative: String? = nil) + + /// Used in configuration parsing when no changes have been applied. Use only internally! + case nothingApplied(ruleID: String) /// Rule is listed multiple times in the configuration. case listedMultipleTime(ruleID: String, times: Int) @@ -11,15 +17,31 @@ public enum Issue: LocalizedError, Equatable { /// An identifier `old` has been renamed to `new`. case renamedIdentifier(old: String, new: String) - /// Configuration for a rule is invalid. - case invalidConfiguration(ruleID: String) - /// Some configuration keys are invalid. case invalidConfigurationKeys(ruleID: String, keys: Set) + /// The configuration is inconsistent, that is options are mutually exclusive or one drives other values + /// irrelevant. + case inconsistentConfiguration(ruleID: String, message: String) + /// Used rule IDs are invalid. case invalidRuleIDs(Set) + /// Found a rule configuration for a rule that is not present in `only_rules`. + case ruleNotPresentInOnlyRules(ruleID: String) + + /// Found a rule configuration for a rule that is disabled. + case ruleDisabledInDisabledRules(ruleID: String) + + /// Found a rule configuration for a rule that is disabled in the parent configuration. + case ruleDisabledInParentConfiguration(ruleID: String) + + /// Found a rule configuration for a rule that is not enabled in `opt_in_rules`. + case ruleNotEnabledInOptInRules(ruleID: String) + + /// Found a rule configuration for a rule that is not enabled in parent `only_rules`. + case ruleNotEnabledInParentOnlyRules(ruleID: String) + /// A generic warning specified by a string. case genericWarning(String) @@ -32,6 +54,9 @@ public enum Issue: LocalizedError, Equatable { /// The initial configuration file was not found. case initialFileNotFound(path: String) + /// A file at specified path was not found. + case fileNotFound(path: String) + /// The file at `path` is not readable or cannot be opened. case fileNotReadable(path: String?, ruleID: String) @@ -50,16 +75,40 @@ public enum Issue: LocalizedError, Equatable { /// An error that occurred when parsing YAML. case yamlParsing(String) + /// The baseline file at `path` is not readable or cannot be opened. + case baselineNotReadable(path: String) + /// Flag to enable warnings for deprecations being printed to the console. Printing is enabled by default. - public static var printDeprecationWarnings = true + #if compiler(>=6.0) + package nonisolated(unsafe) static var printDeprecationWarnings = true + #else + package static var printDeprecationWarnings = true + #endif + + /// Hook used to capture all messages normally printed to stdout and return them back to the caller. + /// + /// > Warning: Shall only be used in tests to verify console output. + /// + /// - parameter runner: The code to run. Messages printed during the execution are collected. + /// + /// - returns: The collected messages produced while running the code in the runner. + static func captureConsole(runner: () throws -> Void) rethrows -> String { + var console = "" + messageConsumer = { console += (console.isEmpty ? "" : "\n") + $0 } + defer { messageConsumer = nil } + try runner() + return console + } + + private static var messageConsumer: ((String) -> Void)? /// Wraps any `Error` into a `SwiftLintError.genericWarning` if it is not already a `SwiftLintError`. /// /// - parameter error: Any `Error`. /// - /// - returns: A `SwiftLintError.genericWarning` containig the message of the `error` argument. + /// - returns: A `SwiftLintError.genericWarning` containing the message of the `error` argument. static func wrap(error: some Error) -> Self { - error as? Issue ?? Self.genericWarning(error.localizedDescription) + error as? Self ?? Self.genericWarning(error.localizedDescription) } /// Make this issue an error. @@ -84,23 +133,50 @@ public enum Issue: LocalizedError, Equatable { if case .ruleDeprecated = self, !Self.printDeprecationWarnings { return } - queuedPrintError(errorDescription) + if let consumer = Self.messageConsumer { + consumer(errorDescription) + } else { + queuedPrintError(errorDescription) + } } private var message: String { switch self { - case let .unknownConfiguration(id): - return "Invalid configuration for '\(id)' rule. Falling back to default." + case let .invalidConfiguration(id, message): + let message = if let message { ": \(message)" } else { "." } + return "Invalid configuration for '\(id)' rule\(message) Falling back to default." + case let .deprecatedConfigurationOption(id, key, alternative): + let baseMessage = "Configuration option '\(key)' in '\(id)' rule is deprecated." + if let alternative { + return baseMessage + " Use the option '\(alternative)' instead." + } + return baseMessage + case let .nothingApplied(ruleID: id): + return Self.invalidConfiguration(ruleID: id).message case let .listedMultipleTime(id, times): return "'\(id)' is listed \(times) times in the configuration." case let .renamedIdentifier(old, new): return "'\(old)' has been renamed to '\(new)' and will be completely removed in a future release." - case let .invalidConfiguration(id): - return "Invalid configuration for '\(id)'. Falling back to default." case let .invalidConfigurationKeys(id, keys): return "Configuration for '\(id)' rule contains the invalid key(s) \(keys.formatted)." + case let .inconsistentConfiguration(id, message): + return "Inconsistent configuration for '\(id)' rule: \(message)" case let .invalidRuleIDs(ruleIDs): return "The key(s) \(ruleIDs.formatted) used as rule identifier(s) is/are invalid." + case let .ruleNotPresentInOnlyRules(id): + return "Found a configuration for '\(id)' rule, but it is not present in " + + "'\(Configuration.Key.onlyRules.rawValue)'." + case let .ruleDisabledInDisabledRules(id): + return "Found a configuration for '\(id)' rule, but it is disabled in " + + "'\(Configuration.Key.disabledRules.rawValue)'." + case let .ruleDisabledInParentConfiguration(id): + return "Found a configuration for '\(id)' rule, but it is disabled in a parent configuration." + case let .ruleNotEnabledInOptInRules(id): + return "Found a configuration for '\(id)' rule, but it is not enabled in " + + "'\(Configuration.Key.optInRules.rawValue)'." + case let .ruleNotEnabledInParentOnlyRules(id): + return "Found a configuration for '\(id)' rule, but it is not present in the parent's " + + "'\(Configuration.Key.onlyRules.rawValue)'." case let .genericWarning(message), let .genericError(message): return message case let .ruleDeprecated(id): @@ -110,6 +186,8 @@ public enum Issue: LocalizedError, Equatable { """ case let .initialFileNotFound(path): return "Could not read file at path '\(path)'." + case let .fileNotFound(path): + return "File at path '\(path)' not found." case let .fileNotReadable(path, id): return "Cannot open or read file at path '\(path ?? "...")' within '\(id)' rule." case let .fileNotWritable(path): @@ -125,6 +203,8 @@ public enum Issue: LocalizedError, Equatable { return "Cannot get cursor info from file at path '\(path ?? "...")' within '\(id)' rule." case let .yamlParsing(message): return "Cannot parse YAML file: \(message)" + case let .baselineNotReadable(path): + return "Cannot open or read the baseline file at path '\(path)'." } } } diff --git a/Source/SwiftLintCore/Models/Linter.swift b/Source/SwiftLintCore/Models/Linter.swift index 5fee18ad6c..68f111fdc9 100644 --- a/Source/SwiftLintCore/Models/Linter.swift +++ b/Source/SwiftLintCore/Models/Linter.swift @@ -1,6 +1,8 @@ import Foundation import SourceKittenFramework +// swiftlint:disable file_length + private let warnSourceKitFailedOnceImpl: Void = { Issue.genericWarning("SourceKit-based rules will be skipped because sourcekitd has failed.").print() }() @@ -16,50 +18,60 @@ private struct LintResult { } private extension Rule { - static func superfluousDisableCommandViolations(regions: [Region], - superfluousDisableCommandRule: SuperfluousDisableCommandRule?, - allViolations: [StyleViolation]) -> [StyleViolation] { + func superfluousDisableCommandViolations(regions: [Region], + superfluousDisableCommandRule: SuperfluousDisableCommandRule?, + allViolations: [StyleViolation]) -> [StyleViolation] { guard regions.isNotEmpty, let superfluousDisableCommandRule else { return [] } - let regionsDisablingCurrentRule = regions.filter { region in - return region.isRuleDisabled(self.init()) - } + let regions = regions.perIdentifierRegions + let regionsDisablingSuperfluousDisableRule = regions.filter { region in - return region.isRuleDisabled(superfluousDisableCommandRule) + region.isRuleDisabled(superfluousDisableCommandRule) } - return regionsDisablingCurrentRule.compactMap { region -> StyleViolation? in - let isSuperfluousRuleDisabled = regionsDisablingSuperfluousDisableRule.contains { - $0.contains(region.start) + var superfluousDisableCommandViolations = [StyleViolation]() + for region in regions { + if regionsDisablingSuperfluousDisableRule.contains(where: { $0.contains(region.start) }) { + continue } - - guard !isSuperfluousRuleDisabled else { - return nil + guard let disabledRuleIdentifier = region.disabledRuleIdentifiers.first else { + continue } - - let noViolationsInDisabledRegion = !allViolations.contains { violation in - return region.contains(violation.location) + guard !isEnabled(in: region, for: disabledRuleIdentifier.stringRepresentation) else { + continue } - guard noViolationsInDisabledRegion else { - return nil + var disableCommandValid = false + for violation in allViolations where region.contains(violation.location) { + if canBeDisabled(violation: violation, by: disabledRuleIdentifier) { + disableCommandValid = true + break + } + } + if !disableCommandValid { + let reason = superfluousDisableCommandRule.reason( + forRuleIdentifier: disabledRuleIdentifier.stringRepresentation + ) + superfluousDisableCommandViolations.append( + StyleViolation( + ruleDescription: type(of: superfluousDisableCommandRule).description, + severity: superfluousDisableCommandRule.configuration.severity, + location: region.start, + reason: reason + ) + ) } - - return StyleViolation( - ruleDescription: type(of: superfluousDisableCommandRule).description, - severity: superfluousDisableCommandRule.configuration.severity, - location: region.start, - reason: superfluousDisableCommandRule.reason(for: self) - ) } + return superfluousDisableCommandViolations } // As we need the configuration to get custom identifiers. - // swiftlint:disable:next function_parameter_count - func lint(file: SwiftLintFile, regions: [Region], benchmark: Bool, + // swiftlint:disable:next function_parameter_count function_body_length + func lint(file: SwiftLintFile, + regions: [Region], + benchmark: Bool, storage: RuleStorage, - configuration: Configuration, superfluousDisableCommandRule: SuperfluousDisableCommandRule?, compilerArguments: [String]) -> LintResult? { // We shouldn't lint if the current Swift version is not supported by the rule @@ -77,7 +89,7 @@ private extension Rule { return nil } - let ruleID = Self.description.identifier + let ruleID = Self.identifier let violations: [StyleViolation] let ruleTime: (String, Double)? @@ -91,17 +103,27 @@ private extension Rule { } let (disabledViolationsAndRegions, enabledViolationsAndRegions) = violations.map { violation in - return (violation, regions.first { $0.contains(violation.location) }) - }.partitioned { _, region in - return region?.isRuleEnabled(self) ?? true + (violation, regions.first { $0.contains(violation.location) }) + }.partitioned { violation, region in + if let region { + return isEnabled(in: region, for: violation.ruleIdentifier) + } + return true } + let customRulesIDs: [String] = { + guard let customRules = self as? CustomRules else { + return [] + } + return customRules.customRuleIdentifiers + }() let ruleIDs = Self.description.allIdentifiers + + customRulesIDs + (superfluousDisableCommandRule.map({ type(of: $0) })?.description.allIdentifiers ?? []) + [RuleIdentifier.all.stringRepresentation] let ruleIdentifiers = Set(ruleIDs.map { RuleIdentifier($0) }) - let superfluousDisableCommandViolations = Self.superfluousDisableCommandViolations( + let superfluousDisableCommandViolations = superfluousDisableCommandViolations( regions: regions.count > 1 ? file.regions(restrictingRuleIdentifiers: ruleIdentifiers) : regions, superfluousDisableCommandRule: superfluousDisableCommandRule, allViolations: violations @@ -114,7 +136,7 @@ private extension Rule { return violation } } else { - enabledViolations = enabledViolationsAndRegions.map { $0.0 } + enabledViolations = enabledViolationsAndRegions.map(\.0) } let deprecatedToValidIDPairs = disabledViolationsAndRegions.flatMap { _, region -> [(String, String)] in let identifiers = region?.deprecatedAliasesDisabling(rule: self) ?? [] @@ -127,6 +149,57 @@ private extension Rule { } } +private extension [Region] { + // Normally regions correspond to changes in the set of enabled rules. To detect superfluous disable command + // rule violations effectively, we need individual regions for each disabled rule identifier. + var perIdentifierRegions: [Region] { + guard isNotEmpty else { + return [] + } + + var convertedRegions = [Region]() + var startMap: [RuleIdentifier: Location] = [:] + var lastRegionEnd: Location? + + for region in self { + let ruleIdentifiers = startMap.keys.sorted() + for ruleIdentifier in ruleIdentifiers where !region.disabledRuleIdentifiers.contains(ruleIdentifier) { + if let lastRegionEnd, let start = startMap[ruleIdentifier] { + let newRegion = Region(start: start, end: lastRegionEnd, disabledRuleIdentifiers: [ruleIdentifier]) + convertedRegions.append(newRegion) + startMap[ruleIdentifier] = nil + } + } + for ruleIdentifier in region.disabledRuleIdentifiers where startMap[ruleIdentifier] == nil { + startMap[ruleIdentifier] = region.start + } + if region.disabledRuleIdentifiers.isEmpty { + convertedRegions.append(region) + } + lastRegionEnd = region.end + } + + let end = Location(file: first?.start.file, line: .max, character: .max) + for ruleIdentifier in startMap.keys.sorted() { + if let start = startMap[ruleIdentifier] { + let newRegion = Region(start: start, end: end, disabledRuleIdentifiers: [ruleIdentifier]) + convertedRegions.append(newRegion) + startMap[ruleIdentifier] = nil + } + } + + return convertedRegions.sorted { + if $0.start == $1.start { + if let lhsDisabledRuleIdentifier = $0.disabledRuleIdentifiers.first, + let rhsDisabledRuleIdentifier = $1.disabledRuleIdentifiers.first { + return lhsDisabledRuleIdentifier < rhsDisabledRuleIdentifier + } + } + return $0.start < $1.start + } + } +} + /// Represents a file that can be linted for style violations and corrections after being collected. public struct Linter { /// The file to lint with this linter. @@ -144,7 +217,9 @@ public struct Linter { /// - parameter configuration: The SwiftLint configuration to apply to this linter. /// - parameter cache: The persisted cache to use for this linter. /// - parameter compilerArguments: The compiler arguments to use for this linter if it is to execute analyzer rules. - public init(file: SwiftLintFile, configuration: Configuration = Configuration.default, cache: LinterCache? = nil, + public init(file: SwiftLintFile, + configuration: Configuration = Configuration.default, + cache: LinterCache? = nil, compilerArguments: [String] = []) { self.file = file self.cache = cache @@ -160,7 +235,7 @@ public struct Linter { if compilerArguments.isEmpty { return !(rule is any AnalyzerRule) } - return rule is any AnalyzerRule + return rule is any AnalyzerRule || rule is SuperfluousDisableCommandRule } self.rules = rules self.isCollecting = rules.contains(where: { $0 is any AnyCollectingRule }) @@ -204,7 +279,7 @@ public struct CollectedLinter { /// /// - returns: All style violations found by this linter. public func styleViolations(using storage: RuleStorage) -> [StyleViolation] { - return getStyleViolations(using: storage).0 + getStyleViolations(using: storage).0 } /// Computes or retrieves style violations and the time spent executing each rule. @@ -214,7 +289,7 @@ public struct CollectedLinter { /// - returns: All style violations found by this linter, and the time spent executing each rule. public func styleViolationsAndRuleTimes(using storage: RuleStorage) -> ([StyleViolation], [(id: String, time: Double)]) { - return getStyleViolations(using: storage, benchmark: true) + getStyleViolations(using: storage, benchmark: true) } private func getStyleViolations(using storage: RuleStorage, @@ -233,20 +308,19 @@ public struct CollectedLinter { $0 is SuperfluousDisableCommandRule }) as? SuperfluousDisableCommandRule let validationResults = rules.parallelCompactMap { - $0.lint(file: self.file, regions: regions, benchmark: benchmark, + $0.lint(file: file, regions: regions, benchmark: benchmark, storage: storage, - configuration: self.configuration, superfluousDisableCommandRule: superfluousDisableCommandRule, - compilerArguments: self.compilerArguments) + compilerArguments: compilerArguments) } let undefinedSuperfluousCommandViolations = self.undefinedSuperfluousCommandViolations( regions: regions, configuration: configuration, superfluousDisableCommandRule: superfluousDisableCommandRule) - let violations = validationResults.flatMap { $0.violations } + undefinedSuperfluousCommandViolations - let ruleTimes = validationResults.compactMap { $0.ruleTime } + let violations = validationResults.flatMap(\.violations) + undefinedSuperfluousCommandViolations + let ruleTimes = validationResults.compactMap(\.ruleTime) var deprecatedToValidIdentifier = [String: String]() - for (key, value) in validationResults.flatMap({ $0.deprecatedToValidIDPairs }) { + for (key, value) in validationResults.flatMap(\.deprecatedToValidIDPairs) { deprecatedToValidIdentifier[key] = value } @@ -265,7 +339,7 @@ public struct CollectedLinter { } private func cachedStyleViolations(benchmark: Bool = false) -> ([StyleViolation], [(id: String, time: Double)])? { - let start: Date! = benchmark ? Date() : nil + let start = Date() guard let cache, let file = file.path, let cachedViolations = cache.violations(forFile: file, configuration: configuration) else { return nil @@ -277,7 +351,7 @@ public struct CollectedLinter { let totalTime = -start.timeIntervalSinceNow let fractionedTime = totalTime / TimeInterval(rules.count) ruleTimes = rules.compactMap { rule in - let id = type(of: rule).description.identifier + let id = type(of: rule).identifier return (id, fractionedTime) } } @@ -340,7 +414,7 @@ public struct CollectedLinter { .configuration.customRuleConfigurations.map { RuleIdentifier($0.identifier) } ?? [] let allRuleIdentifiers = RuleRegistry.shared.list.allValidIdentifiers().map { RuleIdentifier($0) } let allValidIdentifiers = Set(allCustomIdentifiers + allRuleIdentifiers + [.all]) - let superfluousRuleIdentifier = RuleIdentifier(SuperfluousDisableCommandRule.description.identifier) + let superfluousRuleIdentifier = RuleIdentifier(SuperfluousDisableCommandRule.identifier) return regions.flatMap { region in region.disabledRuleIdentifiers.filter({ @@ -348,7 +422,7 @@ public struct CollectedLinter { !region.disabledRuleIdentifiers.contains(.all) && !region.disabledRuleIdentifiers.contains(superfluousRuleIdentifier) }).map { id in - return StyleViolation( + StyleViolation( ruleDescription: type(of: superfluousDisableCommandRule).description, severity: superfluousDisableCommandRule.configuration.severity, location: region.start, diff --git a/Source/SwiftLintCore/Models/LinterCache.swift b/Source/SwiftLintCore/Models/LinterCache.swift index 4902185cc0..75a4097216 100644 --- a/Source/SwiftLintCore/Models/LinterCache.swift +++ b/Source/SwiftLintCore/Models/LinterCache.swift @@ -13,7 +13,7 @@ private struct FileCacheEntry: Codable { private struct FileCache: Codable { var entries: [String: FileCacheEntry] - static var empty: FileCache { return Self(entries: [:]) } + static var empty: Self { Self(entries: [:]) } } /// A persisted cache for storing and retrieving linter results. @@ -115,7 +115,7 @@ public final class LinterCache { } internal func flushed() -> LinterCache { - return Self(cache: mergeCaches(), location: location, fileManager: fileManager, swiftVersion: swiftVersion) + Self(cache: mergeCaches(), location: location, fileManager: fileManager, swiftVersion: swiftVersion) } private func fileCache(cacheDescription: String) -> FileCache { diff --git a/Source/SwiftLintCore/Models/Location.swift b/Source/SwiftLintCore/Models/Location.swift index 5e4202467a..4bb174207c 100644 --- a/Source/SwiftLintCore/Models/Location.swift +++ b/Source/SwiftLintCore/Models/Location.swift @@ -15,15 +15,15 @@ public struct Location: CustomStringConvertible, Comparable, Codable, Sendable { public var description: String { // Xcode likes warnings and errors in the following format: // {full_path_to_file}{:line}{:character}: {error,warning}: {content} - let fileString: String = file ?? "" - let lineString: String = ":\(line ?? 1)" - let charString: String = ":\(character ?? 1)" + let fileString = file ?? "" + let lineString = ":\(line ?? 1)" + let charString = ":\(character ?? 1)" return [fileString, lineString, charString].joined() } /// The file path for this location relative to the current working directory. public var relativeFile: String? { - return file?.replacingOccurrences(of: FileManager.default.currentDirectoryPath + "/", with: "") + file?.replacingOccurrences(of: FileManager.default.currentDirectoryPath + "/", with: "") } /// Creates a `Location` by specifying its properties directly. @@ -80,7 +80,7 @@ public struct Location: CustomStringConvertible, Comparable, Codable, Sendable { // MARK: Comparable - public static func < (lhs: Location, rhs: Location) -> Bool { + public static func < (lhs: Self, rhs: Self) -> Bool { if lhs.file != rhs.file { return lhs.file < rhs.file } diff --git a/Source/SwiftLintCore/Models/Region.swift b/Source/SwiftLintCore/Models/Region.swift index 1553c31ecb..9c6483fbd9 100644 --- a/Source/SwiftLintCore/Models/Region.swift +++ b/Source/SwiftLintCore/Models/Region.swift @@ -28,7 +28,7 @@ public struct Region: Equatable { /// /// - returns: True if the specific location is contained in this region. public func contains(_ location: Location) -> Bool { - return start <= location && end >= location + start <= location && end >= location } /// Whether the specified rule is enabled in this region. @@ -37,16 +37,20 @@ public struct Region: Equatable { /// /// - returns: True if the specified rule is enabled in this region. public func isRuleEnabled(_ rule: some Rule) -> Bool { - return !isRuleDisabled(rule) + !isRuleDisabled(rule) } /// Whether the specified rule is disabled in this region. /// /// - parameter rule: The rule whose status should be determined. /// + /// - note: For CustomRules, this will only return true if the `custom_rules` identifier + /// is used with the `swiftlint` disable command, but this method is never + /// called for CustomRules. + /// /// - returns: True if the specified rule is disabled in this region. public func isRuleDisabled(_ rule: some Rule) -> Bool { - return areRulesDisabled(ruleIDs: type(of: rule).description.allIdentifiers) + areRulesDisabled(ruleIDs: type(of: rule).description.allIdentifiers) } /// Whether the given rules are disabled in this region. @@ -58,7 +62,7 @@ public struct Region: Equatable { if disabledRuleIdentifiers.contains(.all) { return true } - let regionIdentifiers = Set(disabledRuleIdentifiers.map { $0.stringRepresentation }) + let regionIdentifiers = Set(disabledRuleIdentifiers.map(\.stringRepresentation)) return !regionIdentifiers.isDisjoint(with: ruleIDs) } @@ -70,7 +74,7 @@ public struct Region: Equatable { /// - returns: Deprecated rule aliases. public func deprecatedAliasesDisabling(rule: some Rule) -> Set { let identifiers = type(of: rule).description.deprecatedAliases - return Set(disabledRuleIdentifiers.map { $0.stringRepresentation }).intersection(identifiers) + return Set(disabledRuleIdentifiers.map(\.stringRepresentation)).intersection(identifiers) } /// Converts this `Region` to a SwiftSyntax `SourceRange`. diff --git a/Source/SwiftLintCore/Models/ReportersList.swift b/Source/SwiftLintCore/Models/ReportersList.swift index 53c89305f6..e8773225e4 100644 --- a/Source/SwiftLintCore/Models/ReportersList.swift +++ b/Source/SwiftLintCore/Models/ReportersList.swift @@ -14,7 +14,8 @@ public let reportersList: [any Reporter.Type] = [ JUnitReporter.self, MarkdownReporter.self, RelativePathReporter.self, + SARIFReporter.self, SonarQubeReporter.self, SummaryReporter.self, - XcodeReporter.self + XcodeReporter.self, ] diff --git a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift index afc3ea5330..b0e5750897 100644 --- a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift +++ b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift @@ -27,7 +27,7 @@ public protocol Documentable { public struct RuleConfigurationDescription: Equatable { fileprivate let options: [RuleConfigurationOption] - fileprivate init(options: [RuleConfigurationOption]) { + fileprivate init(options: [RuleConfigurationOption], exclusiveOptions: Set = []) { if options.contains(.noOptions) { if options.count > 1 { queuedFatalError( @@ -39,18 +39,21 @@ public struct RuleConfigurationDescription: Equatable { ) } self.options = [] - } else { - self.options = options.filter { $0.value != .empty } + return } + let nonEmptyOptions = options.filter { $0.value != .empty } + self.options = exclusiveOptions.isEmpty + ? nonEmptyOptions + : nonEmptyOptions.filter { exclusiveOptions.contains($0.key) } } - static func from(configuration: some RuleConfiguration) -> Self { + static func from(configuration: some RuleConfiguration, exclusiveOptions: Set = []) -> Self { // Prefer custom descriptions. if let customDescription = configuration.parameterDescription { - return customDescription + return Self(options: customDescription.options, exclusiveOptions: exclusiveOptions) } let options: [RuleConfigurationOption] = Mirror(reflecting: configuration).children - .compactMap { child -> RuleConfigurationDescription? in + .compactMap { child -> Self? in // Property wrappers have names prefixed by an underscore. guard let codingKey = child.label, codingKey.starts(with: "_") else { return nil @@ -69,7 +72,7 @@ public struct RuleConfigurationDescription: Equatable { """ ) } - return Self(options: options) + return Self(options: options, exclusiveOptions: exclusiveOptions) } func allowedKeys() -> [String] { @@ -179,7 +182,7 @@ public enum OptionType: Equatable { /// Special option for a ``ViolationSeverity``. case severity(ViolationSeverity) /// A list of options. - case list([OptionType]) + case list([Self]) /// An option which is another set of configuration options to be nested in the serialized output. case nested(RuleConfigurationDescription) } @@ -273,7 +276,7 @@ public struct RuleConfigurationDescriptionBuilder { /// :nodoc: public static func buildArray(_ components: [Description]) -> Description { - Description(options: components.flatMap { $0.options }) + Description(options: components.flatMap(\.options)) } } @@ -332,9 +335,11 @@ public protocol AcceptableByConfigurationElement { func asDescription(with key: String) -> RuleConfigurationDescription /// Update the object. - /// - /// - Parameter value: New underlying data for the object. - mutating func apply(_ value: Any?, ruleID: String) throws + /// + /// - Parameters: + /// - value: New underlying data for the object. + /// - ruleID: The rule's identifier in which context the configuration parsing runs. + mutating func apply(_ value: Any, ruleID: String) throws } /// Default implementations which are shortcuts applicable for most of the types conforming to the protocol. @@ -343,110 +348,178 @@ public extension AcceptableByConfigurationElement { RuleConfigurationDescription(options: [key => asOption()]) } - mutating func apply(_ value: Any?, ruleID: String) throws { - if let value { - self = try Self(fromAny: value, context: ruleID) - } + mutating func apply(_ value: Any, ruleID: String) throws { + self = try Self(fromAny: value, context: ruleID) } } -/// An option type that does not need a key when used in a ``ConfigurationElement``. Its value will be inlined. +/// An option type that can appear inlined into its using configuration. +/// +/// The ``ConfigurationElement`` must opt into this behavior. In this case, the option does not have a key. This is +/// almost exclusively useful for common ``RuleConfiguration``s that are used in many other rules as child +/// configurations. +/// +/// > Warning: A type conforming to this protocol is assumed to throw an issue in its `apply` method only when it's +/// absolutely clear that there is an error in the YAML configuration passed in. Since it may be used in a nested +/// context and doesn't know about the outer configuration, it's not always clear if a certain key-value is really +/// unacceptable. public protocol InlinableOptionType: AcceptableByConfigurationElement {} /// A single parameter of a rule configuration. /// /// Apply it to a simple (e.g. boolean) property like /// ```swift -/// @ConfigurationElement(key: "name") +/// @ConfigurationElement /// var property = true /// ``` -/// If the wrapped element is an ``InlinableOptionType``, there are two options for its representation -/// in the documentation: +/// to add a (boolean) option to a configuration. The name of the option will be inferred from the name of the property. +/// In this case, it's just `property`. CamelCase names will translated into snake_case, i.e. `myOption` is going to be +/// translated into `my_option` in the `.swiftlint.yml` configuration file. +/// +/// This mechanism may be overwritten with an explicitly set key: +/// ```swift +/// @ConfigurationElement(key: "foo_bar") +/// var property = true +/// ``` +/// +/// If the wrapped element is an ``InlinableOptionType``, there are three ways to represent it in the documentation: /// -/// 1. It can be inlined into the parent configuration. For that, do not provide a name as an argument. E.g. +/// 1. It can be inlined into the parent configuration. For that, add the parameter `inline: true`. E.g. /// ```swift -/// @ConfigurationElement(key: "name") -/// var property = true -/// @ConfigurationElement +/// @ConfigurationElement(inline: true) /// var levels = SeverityLevelsConfiguration(warning: 1, error: 2) /// ``` /// will be documented as a linear list: /// ``` -/// name: true /// warning: 1 /// error: 2 /// ``` -/// 2. It can be represented as a separate nested configuration. In this case, it must have a name. E.g. +/// 2. It can be represented as a separate nested configuration. In this case, it must not have set the `inline` flag to +/// `true`. E.g. /// ```swift -/// @ConfigurationElement(key: "name") -/// var property = true -/// @ConfigurationElement(key: "levels") +/// @ConfigurationElement /// var levels = SeverityLevelsConfiguration(warning: 1, error: 2) /// ``` /// will have a nested configuration section: /// ``` -/// name: true /// levels: warning: 1 /// error: 2 /// ``` +/// 3. As mentioned in the beginning, the implicit key inference mechanism can be overruled by specifying a `key` as in: +/// ```swift +/// @ConfigurationElement(key: "foo") +/// var levels = SeverityLevelsConfiguration(warning: 1, error: 2) +/// ``` +/// It will appear in the documentation as: +/// ``` +/// foo: warning: 1 +/// error: 2 +/// ``` +/// @propertyWrapper public struct ConfigurationElement: Equatable { + /// A deprecation notice. + public enum DeprecationNotice { + /// Warning suggesting an alternative option. + case suggestAlternative(ruleID: String, name: String) + } + /// Wrapped option value. - public var wrappedValue: T + public var wrappedValue: T { + didSet { + if case let .suggestAlternative(id, name) = deprecationNotice { + Issue.deprecatedConfigurationOption(ruleID: id, key: key, alternative: name).print() + } + if wrappedValue != oldValue { + postprocessor(&wrappedValue) + } + } + } /// The wrapper itself providing access to all its data. This field can only be accessed by the /// element's name prefixed with a `$`. - public var projectedValue: ConfigurationElement { + public var projectedValue: Self { get { self } _modify { yield &self } } /// Name of this configuration entry. - public let key: String + public var key: String + + /// Whether this configuration element will be inlined into its description. + public let inline: Bool - private let postprocessor: (inout T) throws -> Void + private let deprecationNotice: DeprecationNotice? + private let postprocessor: (inout T) -> Void /// Default constructor. /// /// - Parameters: /// - value: Value to be wrapped. - /// - key: Name of the option. + /// - key: Optional name of the option. If not specified, it will be inferred from the attributed property. + /// - deprecationNotice: An optional deprecation notice in case an option is outdated and/or has been replaced by + /// an alternative. /// - postprocessor: Function to be applied to the wrapped value after parsing to validate and modify it. - public init(wrappedValue value: T, key: String, postprocessor: @escaping (inout T) throws -> Void = { _ in }) { - self.wrappedValue = value - self.key = key - self.postprocessor = postprocessor - - // Validate and modify the set value immediately. An exception means invalid defaults. - try! performAfterParseOperations() // swiftlint:disable:this force_try + public init(wrappedValue value: T, + key: String, + deprecationNotice: DeprecationNotice? = nil, + postprocessor: @escaping (inout T) -> Void = { _ in }) { // swiftlint:disable:this no_empty_block + self.init( + wrappedValue: value, + key: key, + inline: false, + deprecationNotice: deprecationNotice, + postprocessor: postprocessor + ) + + // Modify the set value immediately. + postprocessor(&wrappedValue) } /// Constructor for optional values. /// - /// It allows to skip explicit initialization with `nil` of the property. + /// It allows to skip explicit initialization of the property with `nil`. /// - /// - Parameter value: Value to be wrapped. + /// - Parameters: + /// - key: Optional name of the option. If not specified, it will be inferred from the attributed property. public init(key: String) where T == Wrapped? { - self.init(wrappedValue: nil, key: key) + self.init(wrappedValue: nil, key: key, inline: false) } - /// Constructor for a ``ConfigurationElement`` without a key. + /// Constructor for an ``InlinableOptionType`` without a key. /// - /// ``InlinableOptionType``s are allowed to have an empty key. The configuration will be inlined into its - /// parent configuration in this specific case. + /// - Parameters: + /// - value: Value to be wrapped. + /// - inline: If `true`, the option will be handled as it would be part of its parent. All of its options + /// will be inlined. Otherwise, it will be treated as a normal nested configuration with its name + /// inferred from the name of the attributed property. + public init(wrappedValue value: T, inline: Bool) where T: InlinableOptionType { + assert(inline, "Only 'inline: true' is allowed at the moment.") + self.init(wrappedValue: value, key: "", inline: inline) + } + + /// Constructor for an ``InlinableOptionType`` with a name. The configuration will explicitly not be inlined. /// /// - Parameters: /// - value: Value to be wrapped. - public init(wrappedValue value: T) where T: InlinableOptionType { - self.init(wrappedValue: value, key: "") + /// - key: Name of the option. + public init(wrappedValue value: T, key: String) where T: InlinableOptionType { + self.init(wrappedValue: value, key: key, inline: false) } - /// Run operations to validate and modify the parsed value. - public mutating func performAfterParseOperations() throws { - try postprocessor(&wrappedValue) + private init(wrappedValue: T, + key: String, + inline: Bool, + deprecationNotice: DeprecationNotice? = nil, + postprocessor: @escaping (inout T) -> Void = { _ in }) { // swiftlint:disable:this no_empty_block + self.wrappedValue = wrappedValue + self.key = key + self.inline = inline + self.deprecationNotice = deprecationNotice + self.postprocessor = postprocessor } - public static func == (lhs: ConfigurationElement, rhs: ConfigurationElement) -> Bool { + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.wrappedValue == rhs.wrappedValue && lhs.key == rhs.key } } @@ -461,10 +534,7 @@ extension ConfigurationElement: AnyConfigurationElement { extension Optional: AcceptableByConfigurationElement where Wrapped: AcceptableByConfigurationElement { public func asOption() -> OptionType { - if let value = self { - return value.asOption() - } - return .empty + self?.asOption() ?? .empty } public init(fromAny value: Any, context ruleID: String) throws { @@ -575,7 +645,7 @@ extension RegularExpression: AcceptableByConfigurationElement { // MARK: RuleConfiguration conformances -public extension RuleConfiguration { +public extension AcceptableByConfigurationElement where Self: RuleConfiguration { func asOption() -> OptionType { .nested(.from(configuration: self)) } @@ -587,13 +657,11 @@ public extension RuleConfiguration { return RuleConfigurationDescription(options: [key => asOption()]) } - mutating func apply(_ value: Any?, ruleID: String) throws { - if let value { - try apply(configuration: value) - } + mutating func apply(_ value: Any, ruleID _: String) throws { + try apply(configuration: value) } - init(fromAny value: Any, context ruleID: String) throws { + init(fromAny _: Any, context _: String) throws { throw Issue.genericError("Do not call this initializer") } } diff --git a/Source/SwiftLintCore/Models/RuleDescription.swift b/Source/SwiftLintCore/Models/RuleDescription.swift index 56fbf3d525..4df2960eaf 100644 --- a/Source/SwiftLintCore/Models/RuleDescription.swift +++ b/Source/SwiftLintCore/Models/RuleDescription.swift @@ -52,11 +52,11 @@ public struct RuleDescription: Equatable, Sendable { public let requiresFileOnDisk: Bool /// The console-printable string for this description. - public var consoleDescription: String { return "\(name) (\(identifier)): \(description)" } + public var consoleDescription: String { "\(name) (\(identifier)): \(description)" } /// All identifiers that have been used to uniquely identify this rule in past and current SwiftLint versions. public var allIdentifiers: [String] { - return Array(deprecatedAliases) + [identifier] + Array(deprecatedAliases) + [identifier] } /// Creates a `RuleDescription` by specifying all its properties directly. @@ -71,9 +71,13 @@ public struct RuleDescription: Equatable, Sendable { /// - parameter corrections: Sets the description's `corrections` property. /// - parameter deprecatedAliases: Sets the description's `deprecatedAliases` property. /// - parameter requiresFileOnDisk: Sets the description's `requiresFileOnDisk` property. - public init(identifier: String, name: String, description: String, kind: RuleKind, + public init(identifier: String, + name: String, + description: String, + kind: RuleKind, minSwiftVersion: SwiftVersion = .five, - nonTriggeringExamples: [Example] = [], triggeringExamples: [Example] = [], + nonTriggeringExamples: [Example] = [], + triggeringExamples: [Example] = [], corrections: [Example: Example] = [:], deprecatedAliases: Set = [], requiresFileOnDisk: Bool = false) { @@ -91,7 +95,7 @@ public struct RuleDescription: Equatable, Sendable { // MARK: Equatable - public static func == (lhs: RuleDescription, rhs: RuleDescription) -> Bool { - return lhs.identifier == rhs.identifier + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.identifier == rhs.identifier } } diff --git a/Source/SwiftLintCore/Models/RuleIdentifier.swift b/Source/SwiftLintCore/Models/RuleIdentifier.swift index 9908adf668..8955c8badf 100644 --- a/Source/SwiftLintCore/Models/RuleIdentifier.swift +++ b/Source/SwiftLintCore/Models/RuleIdentifier.swift @@ -1,5 +1,5 @@ /// An identifier representing a SwiftLint rule, or all rules. -public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral { +public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral, Comparable { // MARK: - Values /// Special identifier that should be treated as referring to 'all' SwiftLint rules. One helpful usecase is in @@ -14,7 +14,7 @@ public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral { private static let allStringRepresentation = "all" - /// The spelling of the string for this idenfitier. + /// The spelling of the string for this identifier. public var stringRepresentation: String { switch self { case .all: @@ -39,4 +39,10 @@ public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral { public init(stringLiteral value: String) { self = Self(value) } + + // MARK: - Comparable Conformance + + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.stringRepresentation < rhs.stringRepresentation + } } diff --git a/Source/SwiftLintCore/Models/RuleList.swift b/Source/SwiftLintCore/Models/RuleList.swift index afe6865b14..79dab75ac6 100644 --- a/Source/SwiftLintCore/Models/RuleList.swift +++ b/Source/SwiftLintCore/Models/RuleList.swift @@ -27,7 +27,7 @@ public struct RuleList { var tmpAliases = [String: String]() for rule in rules { - let identifier = rule.description.identifier + let identifier = rule.identifier tmpList[identifier] = rule for alias in rule.description.deprecatedAliases { tmpAliases[alias] = identifier @@ -73,11 +73,11 @@ public struct RuleList { } internal func identifier(for alias: String) -> String? { - return aliases[alias] + aliases[alias] } internal func allValidIdentifiers() -> [String] { - return list.flatMap { _, rule -> [String] in + list.flatMap { _, rule -> [String] in rule.description.allIdentifiers } } @@ -85,7 +85,8 @@ public struct RuleList { extension RuleList: Equatable { public static func == (lhs: RuleList, rhs: RuleList) -> Bool { - return lhs.list.map { $0.0 } == rhs.list.map { $0.0 } + lhs.list.map(\.0) == rhs.list.map(\.0) + // swiftlint:disable:next prefer_key_path && lhs.list.map { $0.1.description } == rhs.list.map { $0.1.description } && lhs.aliases == rhs.aliases } diff --git a/Source/SwiftLintCore/Models/RuleRegistry.swift b/Source/SwiftLintCore/Models/RuleRegistry.swift index 74c56e73a9..3a823ea094 100644 --- a/Source/SwiftLintCore/Models/RuleRegistry.swift +++ b/Source/SwiftLintCore/Models/RuleRegistry.swift @@ -1,5 +1,5 @@ /// Container to register and look up SwiftLint rules. -public final class RuleRegistry { +public final class RuleRegistry: @unchecked Sendable { private var registeredRules = [any Rule.Type]() /// Shared rule registry instance. @@ -12,7 +12,7 @@ public final class RuleRegistry { /// accessed will not work. public private(set) lazy var list = RuleList(rules: registeredRules) - private init() {} + private init() { /* To guarantee that this is singleton. */ } /// Register rules. /// @@ -27,6 +27,6 @@ public final class RuleRegistry { /// /// - returns: The rule matching the specified ID, if one was found. public func rule(forID id: String) -> (any Rule.Type)? { - return list.list[id] + list.list[id] } } diff --git a/Source/SwiftLintCore/Models/RuleStorage.swift b/Source/SwiftLintCore/Models/RuleStorage.swift index caaa3db5e8..ad9aa5f974 100644 --- a/Source/SwiftLintCore/Models/RuleStorage.swift +++ b/Source/SwiftLintCore/Models/RuleStorage.swift @@ -19,7 +19,7 @@ public class RuleStorage: CustomStringConvertible { /// - parameter info: The file information to store. /// - parameter file: The file for which this information pertains to. /// - parameter rule: The SwiftLint rule that generated this info. - func collect(info: R.FileInfo, for file: SwiftLintFile, in rule: R) { + func collect(info: R.FileInfo, for file: SwiftLintFile, in _: R) { let key = ObjectIdentifier(R.self) access.sync(flags: .barrier) { storage[key, default: [:]][file] = info @@ -31,8 +31,8 @@ public class RuleStorage: CustomStringConvertible { /// - parameter rule: The rule whose collected information should be retrieved. /// /// - returns: All file information for a given rule that was collected via `collect(...)`. - func collectedInfo(for rule: R) -> [SwiftLintFile: R.FileInfo]? { - return access.sync { + func collectedInfo(for _: R) -> [SwiftLintFile: R.FileInfo]? { + access.sync { storage[ObjectIdentifier(R.self)] as? [SwiftLintFile: R.FileInfo] } } diff --git a/Source/SwiftLintCore/Models/SeverityConfiguration.swift b/Source/SwiftLintCore/Models/SeverityConfiguration.swift index 7e44821f68..83336a37e7 100644 --- a/Source/SwiftLintCore/Models/SeverityConfiguration.swift +++ b/Source/SwiftLintCore/Models/SeverityConfiguration.swift @@ -1,5 +1,5 @@ /// A rule configuration that allows specifying the desired severity level for violations. -public struct SeverityConfiguration: SeverityBasedRuleConfiguration { +public struct SeverityConfiguration: SeverityBasedRuleConfiguration, InlinableOptionType { /// Configuration with a warning severity. public static var error: Self { Self(.error) } /// Configuration with an error severity. @@ -8,7 +8,7 @@ public struct SeverityConfiguration: SeverityBasedRuleConfiguratio @ConfigurationElement(key: "severity") var severity = ViolationSeverity.warning - public var severityConfiguration: SeverityConfiguration { + public var severityConfiguration: Self { self } @@ -22,10 +22,14 @@ public struct SeverityConfiguration: SeverityBasedRuleConfiguratio public mutating func apply(configuration: Any) throws { let configString = configuration as? String let configDict = configuration as? [String: Any] - guard let severityString: String = configString ?? configDict?[$severity.key] as? String, - let severity = ViolationSeverity(rawValue: severityString.lowercased()) else { - throw Issue.unknownConfiguration(ruleID: Parent.description.identifier) + if let severityString: String = configString ?? configDict?[$severity.key] as? String { + if let severity = ViolationSeverity(rawValue: severityString.lowercased()) { + self.severity = severity + } else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + } else { + throw Issue.nothingApplied(ruleID: Parent.identifier) } - self.severity = severity } } diff --git a/Source/SwiftLintCore/Models/StyleViolation.swift b/Source/SwiftLintCore/Models/StyleViolation.swift index c243e08d14..96a0aecabc 100644 --- a/Source/SwiftLintCore/Models/StyleViolation.swift +++ b/Source/SwiftLintCore/Models/StyleViolation.swift @@ -1,5 +1,5 @@ /// A value describing an instance of Swift source code that is considered invalid by a SwiftLint rule. -public struct StyleViolation: CustomStringConvertible, Equatable, Codable { +public struct StyleViolation: CustomStringConvertible, Codable, Hashable { /// The identifier of the rule that generated this violation. public let ruleIdentifier: String @@ -20,7 +20,7 @@ public struct StyleViolation: CustomStringConvertible, Equatable, Codable { /// A printable description for this violation. public var description: String { - return XcodeReporter.generateForSingleViolation(self) + XcodeReporter.generateForSingleViolation(self) } /// Creates a `StyleViolation` by specifying its properties directly. @@ -53,7 +53,7 @@ public struct StyleViolation: CustomStringConvertible, Equatable, Codable { /// Returns the same violation, but with the `severity` that is passed in /// - Parameter severity: the new severity to use in the modified violation - public func with(severity: ViolationSeverity) -> StyleViolation { + public func with(severity: ViolationSeverity) -> Self { var new = self new.severity = severity return new @@ -61,9 +61,13 @@ public struct StyleViolation: CustomStringConvertible, Equatable, Codable { /// Returns the same violation, but with the `location` that is passed in /// - Parameter location: the new location to use in the modified violation - public func with(location: Location) -> StyleViolation { + public func with(location: Location) -> Self { var new = self new.location = location return new } + + public func hash(into hasher: inout Hasher) { + hasher.combine(description) + } } diff --git a/Source/SwiftLintCore/Models/SwiftLintFile.swift b/Source/SwiftLintCore/Models/SwiftLintFile.swift index 6d029d2ab6..7a42fb0936 100644 --- a/Source/SwiftLintCore/Models/SwiftLintFile.swift +++ b/Source/SwiftLintCore/Models/SwiftLintFile.swift @@ -47,22 +47,22 @@ public final class SwiftLintFile { /// The path on disk for this file. public var path: String? { - return file.path + file.path } /// The file's contents. public var contents: String { - return file.contents + file.contents } /// A string view into the contents of this file optimized for string manipulation operations. public var stringView: StringView { - return file.stringView + file.stringView } /// The parsed lines for this file's contents. public var lines: [Line] { - return file.lines + file.lines } /// Mark this file as used for testing purposes. diff --git a/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift b/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift index 13f122ea3d..96d5f5e1fc 100644 --- a/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift +++ b/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift @@ -23,11 +23,11 @@ public struct SwiftLintSyntaxMap { /// - returns: The array of syntax tokens intersecting with byte range. public func tokens(inByteRange byteRange: ByteRange) -> [SwiftLintSyntaxToken] { func intersect(_ token: SwiftLintSyntaxToken) -> Bool { - return token.range.intersects(byteRange) + token.range.intersects(byteRange) } func intersectsOrAfter(_ token: SwiftLintSyntaxToken) -> Bool { - return token.offset + token.length > byteRange.location + token.offset + token.length > byteRange.location } guard let startIndex = tokens.firstIndexAssumingSorted(where: intersectsOrAfter) else { @@ -49,6 +49,6 @@ public struct SwiftLintSyntaxMap { /// /// - returns: The syntax kinds in the specified byte range. public func kinds(inByteRange byteRange: ByteRange) -> [SyntaxKind] { - return tokens(inByteRange: byteRange).compactMap { $0.kind } + tokens(inByteRange: byteRange).compactMap(\.kind) } } diff --git a/Source/SwiftLintCore/Models/SwiftLintSyntaxToken.swift b/Source/SwiftLintCore/Models/SwiftLintSyntaxToken.swift index 9266e39f92..d0032b03db 100644 --- a/Source/SwiftLintCore/Models/SwiftLintSyntaxToken.swift +++ b/Source/SwiftLintCore/Models/SwiftLintSyntaxToken.swift @@ -18,23 +18,23 @@ public struct SwiftLintSyntaxToken { /// The byte range in a source file for this token. public var range: ByteRange { - return value.range + value.range } /// The starting byte offset in a source file for this token. public var offset: ByteCount { - return value.offset + value.offset } /// The length in bytes for this token. public var length: ByteCount { - return value.length + value.length } } public extension Array where Element == SwiftLintSyntaxToken { /// The kinds for these tokens. var kinds: [SyntaxKind] { - return compactMap { $0.kind } + compactMap(\.kind) } } diff --git a/Source/SwiftLintCore/Models/SwiftVersion.swift b/Source/SwiftLintCore/Models/SwiftVersion.swift index 62ccdee305..4918793ed1 100644 --- a/Source/SwiftLintCore/Models/SwiftVersion.swift +++ b/Source/SwiftLintCore/Models/SwiftVersion.swift @@ -2,7 +2,7 @@ import Foundation import SourceKittenFramework /// A value describing the version of the Swift compiler. -public struct SwiftVersion: RawRepresentable, Codable, Comparable, Sendable { +public struct SwiftVersion: RawRepresentable, Codable, VersionComparable, Sendable { public typealias RawValue = String public let rawValue: String @@ -10,48 +10,72 @@ public struct SwiftVersion: RawRepresentable, Codable, Comparable, Sendable { public init(rawValue: String) { self.rawValue = rawValue } +} + +/// A comparable `major.minor.patch` version number. +public protocol VersionComparable: Comparable { + /// The version string. + var rawValue: String { get } +} - public static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool { +extension VersionComparable { + public static func == (lhs: Self, rhs: Self) -> Bool { + if let lhsComparators = lhs.comparators, let rhsComparators = rhs.comparators { + return lhsComparators == rhsComparators + } + return lhs.rawValue == rhs.rawValue + } + + public static func < (lhs: Self, rhs: Self) -> Bool { + if let lhsComparators = lhs.comparators, let rhsComparators = rhs.comparators { + return lhsComparators.lexicographicallyPrecedes(rhsComparators) + } return lhs.rawValue < rhs.rawValue } + + private var comparators: [Int]? { + let components = rawValue.split(separator: ".").compactMap { Int($0) } + guard let major = components.first else { + return nil + } + let minor = components.dropFirst(1).first ?? 0 + let patch = components.dropFirst(2).first ?? 0 + return [major, minor, patch] + } } public extension SwiftVersion { - /// Swift 5.0.x - https://swift.org/download/#swift-50 + /// Swift 5 static let five = SwiftVersion(rawValue: "5.0.0") - /// Swift 5.1.x - https://swift.org/download/#swift-51 + /// Swift 5.1 static let fiveDotOne = SwiftVersion(rawValue: "5.1.0") - /// Swift 5.2.x - https://swift.org/download/#swift-52 + /// Swift 5.2 static let fiveDotTwo = SwiftVersion(rawValue: "5.2.0") - /// Swift 5.3.x - https://swift.org/download/#swift-53 + /// Swift 5.3 static let fiveDotThree = SwiftVersion(rawValue: "5.3.0") - /// Swift 5.4.x - https://swift.org/download/#swift-54 + /// Swift 5.4 static let fiveDotFour = SwiftVersion(rawValue: "5.4.0") - /// Swift 5.5.x - https://swift.org/download/#swift-55 + /// Swift 5.5 static let fiveDotFive = SwiftVersion(rawValue: "5.5.0") - /// Swift 5.6.x - https://swift.org/download/#swift-56 + /// Swift 5.6 static let fiveDotSix = SwiftVersion(rawValue: "5.6.0") - /// Swift 5.7.x - https://swift.org/download/#swift-57 + /// Swift 5.7 static let fiveDotSeven = SwiftVersion(rawValue: "5.7.0") - /// Swift 5.8.x - https://swift.org/download/#swift-58 + /// Swift 5.8 static let fiveDotEight = SwiftVersion(rawValue: "5.8.0") - /// Swift 5.9.x - https://swift.org/download/#swift-59 + /// Swift 5.9 static let fiveDotNine = SwiftVersion(rawValue: "5.9.0") + /// Swift 6 + static let six = SwiftVersion(rawValue: "6.0.0") /// The current detected Swift compiler version, based on the currently accessible SourceKit version. /// /// - note: Override by setting the `SWIFTLINT_SWIFT_VERSION` environment variable. static let current: SwiftVersion = { - // Allow forcing the Swift version, useful in cases where SourceKit isn't available + // Allow forcing the Swift version, useful in cases where SourceKit isn't available. if let envVersion = ProcessInfo.processInfo.environment["SWIFTLINT_SWIFT_VERSION"] { - switch envVersion { - case "5": - return .five - default: - return .five - } + return SwiftVersion(rawValue: envVersion) } - if !Request.disableSourceKit { // This request was added in Swift 5.1 let params: SourceKitObject = ["key.request": UID("source.request.compiler_version")] @@ -60,21 +84,20 @@ public extension SwiftVersion { return SwiftVersion(rawValue: "\(major).\(minor).\(patch)") } } - return .five }() } private extension Dictionary where Key == String { var versionMajor: Int? { - return (self["key.version_major"] as? Int64).flatMap({ Int($0) }) + (self["key.version_major"] as? Int64).flatMap({ Int($0) }) } var versionMinor: Int? { - return (self["key.version_minor"] as? Int64).flatMap({ Int($0) }) + (self["key.version_minor"] as? Int64).flatMap({ Int($0) }) } var versionPatch: Int? { - return (self["key.version_patch"] as? Int64).flatMap({ Int($0) }) + (self["key.version_patch"] as? Int64).flatMap({ Int($0) }) } } diff --git a/Source/SwiftLintCore/Models/Version.swift b/Source/SwiftLintCore/Models/Version.swift index 318c136b20..9d10c86ea1 100644 --- a/Source/SwiftLintCore/Models/Version.swift +++ b/Source/SwiftLintCore/Models/Version.swift @@ -1,8 +1,20 @@ /// A type describing the SwiftLint version. -public struct Version { +public struct Version: VersionComparable { /// The string value for this version. public let value: String + /// An alias for `value` required for protocol conformance. + public var rawValue: String { + value + } + /// The current SwiftLint version. - public static let current = Version(value: "0.54.0") + public static let current = Self(value: "0.57.1") + + /// Public initializer. + /// + /// - parameter value: The string value for this version. + public init(value: String) { + self.value = value + } } diff --git a/Source/SwiftLintCore/Models/ViolationSeverity.swift b/Source/SwiftLintCore/Models/ViolationSeverity.swift index 3905f78dec..41c75abb03 100644 --- a/Source/SwiftLintCore/Models/ViolationSeverity.swift +++ b/Source/SwiftLintCore/Models/ViolationSeverity.swift @@ -1,5 +1,5 @@ /// The magnitude of a `StyleViolation`. -@MakeAcceptableByConfigurationElement +@AcceptableByConfigurationElement public enum ViolationSeverity: String, Comparable, Codable, InlinableOptionType { /// Non-fatal. If using SwiftLint as an Xcode build phase, Xcode will mark the build as having succeeded. case warning @@ -8,7 +8,7 @@ public enum ViolationSeverity: String, Comparable, Codable, InlinableOptionType // MARK: Comparable - public static func < (lhs: ViolationSeverity, rhs: ViolationSeverity) -> Bool { - return lhs == .warning && rhs == .error + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs == .warning && rhs == .error } } diff --git a/Source/SwiftLintCore/Models/YamlParser.swift b/Source/SwiftLintCore/Models/YamlParser.swift index c4238be19a..018478a927 100644 --- a/Source/SwiftLintCore/Models/YamlParser.swift +++ b/Source/SwiftLintCore/Models/YamlParser.swift @@ -26,45 +26,33 @@ public struct YamlParser { private extension Constructor { static func swiftlintConstructor(env: [String: String]) -> Constructor { - return Constructor(customScalarMap(env: env)) + Constructor(customScalarMap(env: env)) } static func customScalarMap(env: [String: String]) -> ScalarMap { var map = defaultScalarMap - map[.str] = String.constructExpandingEnvVars(env: env) - map[.bool] = Bool.constructUsingOnlyTrueAndFalse - + map[.str] = { $0.string.expandingEnvVars(env: env) } + map[.bool] = { + switch $0.string.expandingEnvVars(env: env).lowercased() { + case "true": true + case "false": false + default: nil + } + } + map[.int] = { Int($0.string.expandingEnvVars(env: env)) } + map[.float] = { Double($0.string.expandingEnvVars(env: env)) } return map } } private extension String { - static func constructExpandingEnvVars(env: [String: String]) -> (_ scalar: Node.Scalar) -> String? { - return { (scalar: Node.Scalar) -> String? in - return scalar.string.expandingEnvVars(env: env) - } - } - func expandingEnvVars(env: [String: String]) -> String { - var result = self - for (key, value) in env { - result = result.replacingOccurrences(of: "${\(key)}", with: value) + guard contains("${") else { + // No environment variables used. + return self } - - return result - } -} - -private extension Bool { - // swiftlint:disable:next discouraged_optional_boolean - static func constructUsingOnlyTrueAndFalse(from scalar: Node.Scalar) -> Bool? { - switch scalar.string.lowercased() { - case "true": - return true - case "false": - return false - default: - return nil + return env.reduce(into: self) { result, envVar in + result = result.replacingOccurrences(of: "${\(envVar.key)}", with: envVar.value) } } } diff --git a/Source/SwiftLintCore/Protocols/ASTRule.swift b/Source/SwiftLintCore/Protocols/ASTRule.swift index 0f64595ff9..ef367cba58 100644 --- a/Source/SwiftLintCore/Protocols/ASTRule.swift +++ b/Source/SwiftLintCore/Protocols/ASTRule.swift @@ -27,7 +27,7 @@ public protocol ASTRule: Rule { public extension ASTRule { func validate(file: SwiftLintFile) -> [StyleViolation] { - return validate(file: file, dictionary: file.structureDictionary) + validate(file: file, dictionary: file.structureDictionary) } /// Executes the rule on a file and a subset of its AST structure, returning any violations to the rule's @@ -38,7 +38,7 @@ public extension ASTRule { /// /// - returns: All style violations to the rule's expectations. func validate(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [StyleViolation] { - return dictionary.traverseDepthFirst { subDict in + dictionary.traverseDepthFirst { subDict in guard let kind = self.kind(from: subDict) else { return nil } return validate(file: file, kind: kind, dictionary: subDict) } @@ -47,18 +47,18 @@ public extension ASTRule { public extension ASTRule where KindType == SwiftDeclarationKind { func kind(from dictionary: SourceKittenDictionary) -> KindType? { - return dictionary.declarationKind + dictionary.declarationKind } } public extension ASTRule where KindType == SwiftExpressionKind { func kind(from dictionary: SourceKittenDictionary) -> KindType? { - return dictionary.expressionKind + dictionary.expressionKind } } public extension ASTRule where KindType == StatementKind { func kind(from dictionary: SourceKittenDictionary) -> KindType? { - return dictionary.statementKind + dictionary.statementKind } } diff --git a/Source/SwiftLintCore/Protocols/CollectingRule.swift b/Source/SwiftLintCore/Protocols/CollectingRule.swift index de9cd6e61f..3bacde8e81 100644 --- a/Source/SwiftLintCore/Protocols/CollectingRule.swift +++ b/Source/SwiftLintCore/Protocols/CollectingRule.swift @@ -29,7 +29,8 @@ public protocol CollectingRule: AnyCollectingRule { /// - parameter compilerArguments: The compiler arguments needed to compile this file. /// /// - returns: All style violations to the rule's expectations. - func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo], + func validate(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: FileInfo], compilerArguments: [String]) -> [StyleViolation] /// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's @@ -53,33 +54,35 @@ public extension CollectingRule { } return validate(file: file, collectedInfo: info, compilerArguments: compilerArguments) } - func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> FileInfo { - return collectInfo(for: file) + func collectInfo(for file: SwiftLintFile, compilerArguments _: [String]) -> FileInfo { + collectInfo(for: file) } - func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo], - compilerArguments: [String]) -> [StyleViolation] { - return validate(file: file, collectedInfo: collectedInfo) + + func validate(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: FileInfo], + compilerArguments _: [String]) -> [StyleViolation] { + validate(file: file, collectedInfo: collectedInfo) } - func validate(file: SwiftLintFile) -> [StyleViolation] { + func validate(file _: SwiftLintFile) -> [StyleViolation] { queuedFatalError("Must call `validate(file:collectedInfo:)` for CollectingRule") } - func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] { + func validate(file _: SwiftLintFile, compilerArguments _: [String]) -> [StyleViolation] { queuedFatalError("Must call `validate(file:collectedInfo:compilerArguments:)` for CollectingRule") } } public extension CollectingRule where Self: AnalyzerRule { - func collectInfo(for file: SwiftLintFile) -> FileInfo { + func collectInfo(for _: SwiftLintFile) -> FileInfo { queuedFatalError( "Must call `collect(infoFor:compilerArguments:)` for AnalyzerRule & CollectingRule" ) } - func validate(file: SwiftLintFile) -> [StyleViolation] { + func validate(file _: SwiftLintFile) -> [StyleViolation] { queuedFatalError( "Must call `validate(file:collectedInfo:compilerArguments:)` for AnalyzerRule & CollectingRule" ) } - func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [StyleViolation] { + func validate(file _: SwiftLintFile, collectedInfo _: [SwiftLintFile: FileInfo]) -> [StyleViolation] { queuedFatalError( "Must call `validate(file:collectedInfo:compilerArguments:)` for AnalyzerRule & CollectingRule" ) @@ -98,7 +101,8 @@ package protocol CollectingCorrectableRule: CollectingRule, CorrectableRule { /// - parameter compilerArguments: The compiler arguments needed to compile this file. /// /// - returns: All corrections that were applied. - func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo], + func correct(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: FileInfo], compilerArguments: [String]) -> [Correction] /// Attempts to correct the violations to this rule in the specified file after collecting file info for all files @@ -114,9 +118,10 @@ package protocol CollectingCorrectableRule: CollectingRule, CorrectableRule { } package extension CollectingCorrectableRule { - func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo], - compilerArguments: [String]) -> [Correction] { - return correct(file: file, collectedInfo: collectedInfo) + func correct(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: FileInfo], + compilerArguments _: [String]) -> [Correction] { + correct(file: file, collectedInfo: collectedInfo) } func correct(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [Correction] { @@ -126,23 +131,23 @@ package extension CollectingCorrectableRule { return correct(file: file, collectedInfo: info, compilerArguments: compilerArguments) } - func correct(file: SwiftLintFile) -> [Correction] { + func correct(file _: SwiftLintFile) -> [Correction] { queuedFatalError("Must call `correct(file:collectedInfo:)` for AnalyzerRule") } - func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] { + func correct(file _: SwiftLintFile, compilerArguments _: [String]) -> [Correction] { queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule") } } package extension CollectingCorrectableRule where Self: AnalyzerRule { - func correct(file: SwiftLintFile) -> [Correction] { + func correct(file _: SwiftLintFile) -> [Correction] { queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule") } - func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] { + func correct(file _: SwiftLintFile, compilerArguments _: [String]) -> [Correction] { queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule") } - func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [Correction] { + func correct(file _: SwiftLintFile, collectedInfo _: [SwiftLintFile: FileInfo]) -> [Correction] { queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule") } } diff --git a/Source/SwiftLintCore/Protocols/Reporter.swift b/Source/SwiftLintCore/Protocols/Reporter.swift index 8244fed412..18667c9d65 100644 --- a/Source/SwiftLintCore/Protocols/Reporter.swift +++ b/Source/SwiftLintCore/Protocols/Reporter.swift @@ -32,7 +32,10 @@ extension Reporter { /// - parameter identifier: The identifier corresponding to the reporter. /// /// - returns: The reporter type. -public func reporterFrom(identifier: String) -> any Reporter.Type { +public func reporterFrom(identifier: String?) -> any Reporter.Type { + guard let identifier else { + return XcodeReporter.self + } guard let reporter = reportersList.first(where: { $0.identifier == identifier }) else { queuedFatalError("No reporter with identifier '\(identifier)' available.") } diff --git a/Source/SwiftLintCore/Protocols/Rule.swift b/Source/SwiftLintCore/Protocols/Rule.swift index 4aef5a78fa..5e0214b678 100644 --- a/Source/SwiftLintCore/Protocols/Rule.swift +++ b/Source/SwiftLintCore/Protocols/Rule.swift @@ -3,19 +3,12 @@ import SourceKittenFramework /// An executable value that can identify issues (violations) in Swift source code. public protocol Rule { - /// The rule's description type. - associatedtype Description: Documentable - /// The type of the configuration used to configure this rule. associatedtype ConfigurationType: RuleConfiguration /// A verbose description of many of this rule's properties. static var description: RuleDescription { get } - /// A description of how this rule has been configured to run. It can be built using the annotated result builder. - @RuleConfigurationDescriptionBuilder - var configurationDescription: Description { get } - /// This rule's configuration. var configuration: ConfigurationType { get set } @@ -32,6 +25,9 @@ public protocol Rule { /// - throws: Throws if the configuration didn't match the expected format. init(configuration: Any) throws + /// Create a description of how this rule has been configured to run. + func createConfigurationDescription(exclusiveOptions: Set) -> RuleConfigurationDescription + /// Executes the rule on a file and returns any violations to the rule's expectations. /// /// - parameter file: The file for which to execute the rule. @@ -74,6 +70,26 @@ public protocol Rule { /// /// - returns: All style violations to the rule's expectations. func validate(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation] + + /// Checks if a style violation can be disabled by a command specifying a rule ID. Only the rule can claim that for + /// sure since it knows all the possible identifiers. + /// + /// - Parameters: + /// - violation: A style violation. + /// - ruleID: The name of a rule as used in a disable command. + /// + /// - Returns: A boolean value indicating whether the violation can be disabled by the given ID. + func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool + + /// Checks if a the rule is enabled in a given region. A specific rule ID can be provided in case a rule supports + /// more than one identifier. + /// + /// - Parameters: + /// - region: The region to check. + /// - ruleID: Rule identifier deviating from the default rule's name. + /// + /// - Returns: A boolean value indicating whether the rule is enabled in the given region. + func isEnabled(in region: Region, for ruleID: String) -> Bool } public extension Rule { @@ -86,13 +102,12 @@ public extension Rule { try self.configuration.apply(configuration: configuration) } - func validate(file: SwiftLintFile, using storage: RuleStorage, - compilerArguments: [String]) -> [StyleViolation] { - return validate(file: file, compilerArguments: compilerArguments) + func validate(file: SwiftLintFile, using _: RuleStorage, compilerArguments: [String]) -> [StyleViolation] { + validate(file: file, compilerArguments: compilerArguments) } - func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] { - return validate(file: file) + func validate(file: SwiftLintFile, compilerArguments _: [String]) -> [StyleViolation] { + validate(file: file) } func isEqualTo(_ rule: any Rule) -> Bool { @@ -102,18 +117,32 @@ public extension Rule { return false } - func collectInfo(for file: SwiftLintFile, into storage: RuleStorage, compilerArguments: [String]) { + func collectInfo(for _: SwiftLintFile, into _: RuleStorage, compilerArguments _: [String]) { // no-op: only CollectingRules mutate their storage } /// The cache description which will be used to determine if a previous /// cached value is still valid given the new cache value. var cacheDescription: String { - (self as? any CacheDescriptionProvider)?.cacheDescription ?? configurationDescription.oneLiner() + (self as? any CacheDescriptionProvider)?.cacheDescription ?? createConfigurationDescription().oneLiner() + } + + func createConfigurationDescription(exclusiveOptions: Set = []) -> RuleConfigurationDescription { + RuleConfigurationDescription.from(configuration: configuration, exclusiveOptions: exclusiveOptions) } - var configurationDescription: some Documentable { - RuleConfigurationDescription.from(configuration: configuration) + func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool { + switch ruleID { + case .all: + true + case let .single(identifier: id): + Self.description.allIdentifiers.contains(id) + && Self.description.allIdentifiers.contains(violation.ruleIdentifier) + } + } + + func isEnabled(in region: Region, for ruleID: String) -> Bool { + !Self.description.allIdentifiers.contains(ruleID) || region.isRuleEnabled(self) } } @@ -156,11 +185,11 @@ public protocol CorrectableRule: Rule { } public extension CorrectableRule { - func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] { - return correct(file: file) + func correct(file: SwiftLintFile, compilerArguments _: [String]) -> [Correction] { + correct(file: file) } - func correct(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [Correction] { - return correct(file: file, compilerArguments: compilerArguments) + func correct(file: SwiftLintFile, using _: RuleStorage, compilerArguments: [String]) -> [Correction] { + correct(file: file, compilerArguments: compilerArguments) } } @@ -213,14 +242,14 @@ public protocol SourceKitFreeRule: Rule {} public protocol AnalyzerRule: OptInRule {} public extension AnalyzerRule { - func validate(file: SwiftLintFile) -> [StyleViolation] { + func validate(file _: SwiftLintFile) -> [StyleViolation] { queuedFatalError("Must call `validate(file:compilerArguments:)` for AnalyzerRule") } } /// :nodoc: public extension AnalyzerRule where Self: CorrectableRule { - func correct(file: SwiftLintFile) -> [Correction] { + func correct(file _: SwiftLintFile) -> [Correction] { queuedFatalError("Must call `correct(file:compilerArguments:)` for AnalyzerRule") } } diff --git a/Source/SwiftLintCore/Protocols/RuleConfiguration.swift b/Source/SwiftLintCore/Protocols/RuleConfiguration.swift index 8f5a32f516..c750417df4 100644 --- a/Source/SwiftLintCore/Protocols/RuleConfiguration.swift +++ b/Source/SwiftLintCore/Protocols/RuleConfiguration.swift @@ -1,5 +1,5 @@ /// A configuration value for a rule to allow users to modify its behavior. -public protocol RuleConfiguration: InlinableOptionType, Equatable { +public protocol RuleConfiguration: Equatable { /// The type of the rule that's using this configuration. associatedtype Parent: Rule @@ -13,6 +13,10 @@ public protocol RuleConfiguration: InlinableOptionType, Equatable { /// /// - throws: Throws if the configuration is not in the expected format. mutating func apply(configuration: Any) throws + + /// Run a sanity check on the configuration, perform optional postprocessing steps and/or warn about potential + /// issues. + mutating func validate() throws } /// A configuration for a rule that allows to configure at least the severity. @@ -30,6 +34,10 @@ public extension SeverityBasedRuleConfiguration { public extension RuleConfiguration { var parameterDescription: RuleConfigurationDescription? { nil } + + func validate() throws { + // Do nothing by default. + } } public extension RuleConfiguration { diff --git a/Source/SwiftLintCore/Protocols/SwiftSyntaxCorrectableRule.swift b/Source/SwiftLintCore/Protocols/SwiftSyntaxCorrectableRule.swift index 2192ab0665..f977d9b050 100644 --- a/Source/SwiftLintCore/Protocols/SwiftSyntaxCorrectableRule.swift +++ b/Source/SwiftLintCore/Protocols/SwiftSyntaxCorrectableRule.swift @@ -1,3 +1,4 @@ +import Foundation import SwiftSyntax /// A SwiftLint CorrectableRule that performs its corrections using a SwiftSyntax `SyntaxRewriter`. @@ -12,7 +13,7 @@ public protocol SwiftSyntaxCorrectableRule: SwiftSyntaxRule, CorrectableRule { } public extension SwiftSyntaxCorrectableRule { - func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? { + func makeRewriter(file _: SwiftLintFile) -> ViolationsSyntaxRewriter? { nil } @@ -23,7 +24,7 @@ public extension SwiftSyntaxCorrectableRule { if let rewriter = makeRewriter(file: file) { let newTree = rewriter.visit(syntaxTree) let positions = rewriter.correctionPositions - if positions.isEmpty { + guard positions.isNotEmpty else { return [] } let corrections = positions @@ -39,18 +40,33 @@ public extension SwiftSyntaxCorrectableRule { } // There is no rewriter. Falling back to the correction ranges collected by the visitor (if any). - let violationCorrections = makeVisitor(file: file).walk(tree: syntaxTree, handler: \.violationCorrections) - if violationCorrections.isEmpty { + let violations = makeVisitor(file: file) + .walk(tree: syntaxTree, handler: \.violations) + guard violations.isNotEmpty else { return [] } - let correctionRanges = violationCorrections + + let locationConverter = file.locationConverter + let disabledRegions = file.regions() + .filter { $0.areRulesDisabled(ruleIDs: Self.description.allIdentifiers) } + .compactMap { $0.toSourceRange(locationConverter: locationConverter) } + + typealias CorrectionRange = (range: NSRange, correction: String) + let correctionRanges = violations + .filter { !$0.position.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) } + .compactMap(\.correction) .compactMap { correction in file.stringView.NSRange(start: correction.start, end: correction.end).map { range in - (range: range, correction: correction.replacement) + CorrectionRange(range: range, correction: correction.replacement) } } - .filter { file.ruleEnabled(violatingRange: $0.range, for: self) != nil } - .reversed() + .sorted { (lhs: CorrectionRange, rhs: CorrectionRange) -> Bool in + lhs.range.location > rhs.range.location + } + guard correctionRanges.isNotEmpty else { + return [] + } + var corrections = [Correction]() var contents = file.contents for range in correctionRanges { @@ -83,7 +99,7 @@ open class ViolationsSyntaxRewriter: SyntaxRew /// Positions in a source file where corrections were applied. public var correctionPositions = [AbsolutePosition]() - /// Initilizer for a ``ViolationsSyntaxRewriter``. + /// Initializer for a ``ViolationsSyntaxRewriter``. /// /// - Parameters: /// - configuration: Configuration of a rule. diff --git a/Source/SwiftLintCore/Protocols/SwiftSyntaxRule.swift b/Source/SwiftLintCore/Protocols/SwiftSyntaxRule.swift index 1a47105905..32b691aa61 100644 --- a/Source/SwiftLintCore/Protocols/SwiftSyntaxRule.swift +++ b/Source/SwiftLintCore/Protocols/SwiftSyntaxRule.swift @@ -46,8 +46,13 @@ public extension SwiftSyntaxRule { return [] } - return makeVisitor(file: file) + let violations = makeVisitor(file: file) .walk(tree: syntaxTree, handler: \.violations) + assert( + violations.allSatisfy { $0.correction == nil || self is any SwiftSyntaxCorrectableRule }, + "\(Self.self) produced corrections without being correctable." + ) + return violations .sorted() .map { makeViolation(file: file, violation: $0) } } @@ -75,22 +80,53 @@ public extension SwiftSyntaxRule { /// A violation produced by `ViolationsSyntaxVisitor`s. public struct ReasonedRuleViolation: Comparable, Hashable { + /// The correction of a violation that is basically the violation's range in the source code and a + /// replacement for this range that would fix the violation. + public struct ViolationCorrection: Hashable { + /// Start position of the violation range. + let start: AbsolutePosition + /// End position of the violation range. + let end: AbsolutePosition + /// Replacement for the violating range. + let replacement: String + + /// Create a ``ViolationCorrection``. + /// - Parameters: + /// - start: Start position of the violation range. + /// - end: End position of the violation range. + /// - replacement: Replacement for the violating range. + public init(start: AbsolutePosition, end: AbsolutePosition, replacement: String) { + self.start = start + self.end = end + self.replacement = replacement + } + } + /// The violation's position. public let position: AbsolutePosition /// A specific reason for the violation. public let reason: String? /// The violation's severity. public let severity: ViolationSeverity? + /// An optional correction of the violation to be used in rewriting (see ``SwiftSyntaxCorrectableRule``). Can be + /// left unset when creating a violation, especially if the rule is not correctable or provides a custom rewriter. + public let correction: ViolationCorrection? /// Creates a `ReasonedRuleViolation`. /// - /// - parameter position: The violations position in the analyzed source file. - /// - parameter reason: The reason for the violation if different from the rule's description. - /// - parameter severity: The severity of the violation if different from the rule's default configured severity. - public init(position: AbsolutePosition, reason: String? = nil, severity: ViolationSeverity? = nil) { + /// - Parameters: + /// - position: The violations position in the analyzed source file. + /// - reason: The reason for the violation if different from the rule's description. + /// - severity: The severity of the violation if different from the rule's default configured severity. + /// - correction: An optional correction of the violation to be used in rewriting. + public init(position: AbsolutePosition, + reason: String? = nil, + severity: ViolationSeverity? = nil, + correction: ViolationCorrection? = nil) { self.position = position self.reason = reason self.severity = severity + self.correction = correction } public static func < (lhs: Self, rhs: Self) -> Bool { @@ -101,14 +137,22 @@ public struct ReasonedRuleViolation: Comparable, Hashable { /// Extension for arrays of `ReasonedRuleViolation`s that provides the automatic conversion of /// `AbsolutePosition`s into `ReasonedRuleViolation`s (without a specific reason). public extension Array where Element == ReasonedRuleViolation { - /// Append a minimal violation for the specified position. + /// Add a violation at the specified position using the default description and severity. /// /// - parameter position: The position for the violation to append. mutating func append(_ position: AbsolutePosition) { append(ReasonedRuleViolation(position: position)) } - /// Append minimal violations for the specified positions. + /// Add a violation and the correction at the specified position using the default description and severity. + /// + /// - parameter position: The position for the violation to append. + /// - parameter correction: An optional correction to be applied when running with `--fix`. + mutating func append(at position: AbsolutePosition, correction: ReasonedRuleViolation.ViolationCorrection? = nil) { + append(ReasonedRuleViolation(position: position, correction: correction)) + } + + /// Add violations for the specified positions using the default description and severity. /// /// - parameter positions: The positions for the violations to append. mutating func append(contentsOf positions: [AbsolutePosition]) { diff --git a/Source/SwiftLintCore/Reporters/CSVReporter.swift b/Source/SwiftLintCore/Reporters/CSVReporter.swift index 3c0a7ff26e..023f32b68f 100644 --- a/Source/SwiftLintCore/Reporters/CSVReporter.swift +++ b/Source/SwiftLintCore/Reporters/CSVReporter.swift @@ -16,7 +16,7 @@ struct CSVReporter: Reporter { "severity", "type", "reason", - "rule_id" + "rule_id", ].joined(separator: ",") let rows = [keys] + violations.map(csvRow(for:)) @@ -26,14 +26,14 @@ struct CSVReporter: Reporter { // MARK: - Private private static func csvRow(for violation: StyleViolation) -> String { - return [ + [ violation.location.file?.escapedForCSV() ?? "", violation.location.line?.description ?? "", violation.location.character?.description ?? "", violation.severity.rawValue.capitalized, violation.ruleName.escapedForCSV(), violation.reason.escapedForCSV(), - violation.ruleIdentifier + violation.ruleIdentifier, ].joined(separator: ",") } } diff --git a/Source/SwiftLintCore/Reporters/CheckstyleReporter.swift b/Source/SwiftLintCore/Reporters/CheckstyleReporter.swift index 4c039e2b2b..47327648c6 100644 --- a/Source/SwiftLintCore/Reporters/CheckstyleReporter.swift +++ b/Source/SwiftLintCore/Reporters/CheckstyleReporter.swift @@ -8,23 +8,23 @@ struct CheckstyleReporter: Reporter { static let description = "Reports violations as Checkstyle XML." static func generateReport(_ violations: [StyleViolation]) -> String { - return [ + [ "\n", violations .group(by: { ($0.location.file ?? "").escapedForXML() }) .sorted(by: { $0.key < $1.key }) .map(generateForViolationFile).joined(), - "\n" + "\n", ].joined() } // MARK: - Private private static func generateForViolationFile(_ file: String, violations: [StyleViolation]) -> String { - return [ + [ "\n\t\n", violations.map(generateForSingleViolation).joined(), - "\t" + "\t", ].joined() } @@ -40,7 +40,7 @@ struct CheckstyleReporter: Reporter { "column=\"\(col)\" ", "severity=\"", severity, "\" ", "message=\"", reason, "\" ", - "source=\"\(source)\"/>\n" + "source=\"\(source)\"/>\n", ].joined() } } diff --git a/Source/SwiftLintCore/Reporters/CodeClimateReporter.swift b/Source/SwiftLintCore/Reporters/CodeClimateReporter.swift index 829e3ae234..90a622d2c1 100644 --- a/Source/SwiftLintCore/Reporters/CodeClimateReporter.swift +++ b/Source/SwiftLintCore/Reporters/CodeClimateReporter.swift @@ -13,14 +13,14 @@ struct CodeClimateReporter: Reporter { static let description = "Reports violations as a JSON array in Code Climate format." static func generateReport(_ violations: [StyleViolation]) -> String { - return toJSON(violations.map(dictionary(for:))) + toJSON(violations.map(dictionary(for:))) .replacingOccurrences(of: "\\/", with: "/") } // MARK: - Private private static func dictionary(for violation: StyleViolation) -> [String: Any] { - return [ + [ "check_name": violation.ruleName, "description": violation.reason, "engine_name": "SwiftLint", @@ -29,11 +29,11 @@ struct CodeClimateReporter: Reporter { "path": violation.location.relativeFile ?? NSNull() as Any, "lines": [ "begin": violation.location.line ?? NSNull() as Any, - "end": violation.location.line ?? NSNull() as Any - ] + "end": violation.location.line ?? NSNull() as Any, + ], ], - "severity": violation.severity == .error ? "MAJOR" : "MINOR", - "type": "issue" + "severity": violation.severity == .error ? "major" : "minor", + "type": "issue", ] } @@ -46,7 +46,7 @@ struct CodeClimateReporter: Reporter { return [ "\(fingerprintLocation)", - "\(violation.ruleIdentifier)" + "\(violation.ruleIdentifier)", ].joined().sha256() } } diff --git a/Source/SwiftLintCore/Reporters/GitHubActionsLoggingReporter.swift b/Source/SwiftLintCore/Reporters/GitHubActionsLoggingReporter.swift index f4d388f710..cbe31039fd 100644 --- a/Source/SwiftLintCore/Reporters/GitHubActionsLoggingReporter.swift +++ b/Source/SwiftLintCore/Reporters/GitHubActionsLoggingReporter.swift @@ -8,7 +8,7 @@ struct GitHubActionsLoggingReporter: Reporter { "machine for Actions can recognize as messages." static func generateReport(_ violations: [StyleViolation]) -> String { - return violations.map(generateForSingleViolation).joined(separator: "\n") + violations.map(generateForSingleViolation).joined(separator: "\n") } // MARK: - Private @@ -17,13 +17,13 @@ struct GitHubActionsLoggingReporter: Reporter { // swiftlint:disable:next line_length // https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands // ::(warning|error) file={relative_path_to_file},line={:line},col={:character}::{content} - return [ + [ "::\(violation.severity.rawValue) ", "file=\(violation.location.relativeFile ?? ""),", "line=\(violation.location.line ?? 1),", "col=\(violation.location.character ?? 1)::", violation.reason, - " (\(violation.ruleIdentifier))" + " (\(violation.ruleIdentifier))", ].joined() } } diff --git a/Source/SwiftLintCore/Reporters/GitLabJUnitReporter.swift b/Source/SwiftLintCore/Reporters/GitLabJUnitReporter.swift index 57381e3b94..df02194405 100644 --- a/Source/SwiftLintCore/Reporters/GitLabJUnitReporter.swift +++ b/Source/SwiftLintCore/Reporters/GitLabJUnitReporter.swift @@ -7,7 +7,7 @@ struct GitLabJUnitReporter: Reporter { static let description = "Reports violations as JUnit XML supported by GitLab." static func generateReport(_ violations: [StyleViolation]) -> String { - return "\n" + + "\n" + violations.map({ violation -> String in let fileName = (violation.location.relativeFile ?? "").escapedForXML() let line = violation.location.line.map(String.init) @@ -31,7 +31,7 @@ struct GitLabJUnitReporter: Reporter { return [ "\n\t\n", "\t\t\(body)\n\t\t\n", - "\t" + "\t", ].joined() }).joined() + "\n\n" } diff --git a/Source/SwiftLintCore/Reporters/HTMLReporter.swift b/Source/SwiftLintCore/Reporters/HTMLReporter.swift index fcc1d65486..37916a3d4f 100644 --- a/Source/SwiftLintCore/Reporters/HTMLReporter.swift +++ b/Source/SwiftLintCore/Reporters/HTMLReporter.swift @@ -15,20 +15,20 @@ struct HTMLReporter: Reporter { static let description = "Reports violations as HTML." static func generateReport(_ violations: [StyleViolation]) -> String { - return generateReport(violations, swiftlintVersion: Version.current.value, - dateString: formatter.string(from: Date())) + generateReport(violations, swiftlintVersion: Version.current.value, dateString: formatter.string(from: Date())) } // MARK: - Internal // swiftlint:disable:next function_body_length - internal static func generateReport(_ violations: [StyleViolation], swiftlintVersion: String, + internal static func generateReport(_ violations: [StyleViolation], + swiftlintVersion: String, dateString: String) -> String { let rows = violations.enumerated() .map { generateSingleRow(for: $1, at: $0 + 1) } .joined(separator: "\n") - let fileCount = Set(violations.compactMap({ $0.location.file })).count + let fileCount = Set(violations.compactMap(\.location.file)).count let warningCount = violations.filter({ $0.severity == .warning }).count let errorCount = violations.filter({ $0.severity == .error }).count diff --git a/Source/SwiftLintCore/Reporters/JSONReporter.swift b/Source/SwiftLintCore/Reporters/JSONReporter.swift index 78834307e2..334152c6c7 100644 --- a/Source/SwiftLintCore/Reporters/JSONReporter.swift +++ b/Source/SwiftLintCore/Reporters/JSONReporter.swift @@ -7,23 +7,23 @@ struct JSONReporter: Reporter { static let identifier = "json" static let isRealtime = false - static let description: String = "Reports violations as a JSON array." + static let description = "Reports violations as a JSON array." static func generateReport(_ violations: [StyleViolation]) -> String { - return toJSON(violations.map(dictionary(for:))) + toJSON(violations.map(dictionary(for:)), options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]) } // MARK: - Private private static func dictionary(for violation: StyleViolation) -> [String: Any] { - return [ + [ "file": violation.location.file ?? NSNull() as Any, "line": violation.location.line ?? NSNull() as Any, "character": violation.location.character ?? NSNull() as Any, "severity": violation.severity.rawValue.capitalized, "type": violation.ruleName, "rule_id": violation.ruleIdentifier, - "reason": violation.reason + "reason": violation.reason, ] } } diff --git a/Source/SwiftLintCore/Reporters/MarkdownReporter.swift b/Source/SwiftLintCore/Reporters/MarkdownReporter.swift index b121be845a..642f30faab 100644 --- a/Source/SwiftLintCore/Reporters/MarkdownReporter.swift +++ b/Source/SwiftLintCore/Reporters/MarkdownReporter.swift @@ -14,7 +14,7 @@ struct MarkdownReporter: Reporter { "line", "severity", "reason", - "rule_id" + "rule_id", ].joined(separator: " | ") let rows = [keys, "--- | --- | --- | --- | ---"] + violations.map(markdownRow(for:)) @@ -24,12 +24,12 @@ struct MarkdownReporter: Reporter { // MARK: - Private private static func markdownRow(for violation: StyleViolation) -> String { - return [ + [ violation.location.file?.escapedForMarkdown() ?? "", violation.location.line?.description ?? "", severity(for: violation.severity), violation.ruleName.escapedForMarkdown() + ": " + violation.reason.escapedForMarkdown(), - violation.ruleIdentifier + violation.ruleIdentifier, ].joined(separator: " | ") } diff --git a/Source/SwiftLintCore/Reporters/RelativePathReporter.swift b/Source/SwiftLintCore/Reporters/RelativePathReporter.swift index ae22120e9c..c61586e450 100644 --- a/Source/SwiftLintCore/Reporters/RelativePathReporter.swift +++ b/Source/SwiftLintCore/Reporters/RelativePathReporter.swift @@ -7,7 +7,7 @@ struct RelativePathReporter: Reporter { static let description = "Reports violations with relative paths." static func generateReport(_ violations: [StyleViolation]) -> String { - return violations.map(generateForSingleViolation).joined(separator: "\n") + violations.map(generateForSingleViolation).joined(separator: "\n") } /// Generates a report for a single violation. @@ -18,14 +18,14 @@ struct RelativePathReporter: Reporter { internal static func generateForSingleViolation(_ violation: StyleViolation) -> String { // {relative_path_to_file}{:line}{:character}: {error,warning}: {content} - return [ + [ "\(violation.location.relativeFile ?? "")", ":\(violation.location.line ?? 1)", ":\(violation.location.character ?? 1): ", "\(violation.severity.rawValue): ", "\(violation.ruleName) Violation: ", violation.reason, - " (\(violation.ruleIdentifier))" + " (\(violation.ruleIdentifier))", ].joined() } } diff --git a/Source/SwiftLintCore/Reporters/SARIFReporter.swift b/Source/SwiftLintCore/Reporters/SARIFReporter.swift new file mode 100644 index 0000000000..108e4fb2d6 --- /dev/null +++ b/Source/SwiftLintCore/Reporters/SARIFReporter.swift @@ -0,0 +1,75 @@ +import Foundation +import SourceKittenFramework + +/// To some tools (i.e. Datadog), code quality findings are reported in the SARIF format: +/// - Full Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html +/// - Samples: https://github.com/microsoft/sarif-tutorials/blob/main/samples/ +/// - JSON Validator: https://sarifweb.azurewebsites.net/Validation +struct SARIFReporter: Reporter { + // MARK: - Reporter Conformance + static let identifier = "sarif" + static let isRealtime = false + static let description = "Reports violations in the Static Analysis Results Interchange Format (SARIF)" + static let swiftlintVersion = "https://github.com/realm/SwiftLint/blob/\(Version.current.value)/README.md" + + static func generateReport(_ violations: [StyleViolation]) -> String { + let SARIFJson = [ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json", + "runs": [ + [ + "tool": [ + "driver": [ + "name": "SwiftLint", + "semanticVersion": Version.current.value, + "informationUri": swiftlintVersion, + ], + ], + "results": violations.map(dictionary(for:)), + ], + ], + ] as [String: Any] + + return toJSON(SARIFJson, options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]) + } + + // MARK: - Private + + private static func dictionary(for violation: StyleViolation) -> [String: Any] { + [ + "level": violation.severity.rawValue, + "ruleId": violation.ruleIdentifier, + "message": [ + "text": violation.reason + ], + "locations": [ + dictionary(for: violation.location) + ], + ] + } + + private static func dictionary(for location: Location) -> [String: Any] { + // According to SARIF specification JSON1008, minimum value for line is 1 + if let line = location.line, line > 0 { + return [ + "physicalLocation": [ + "artifactLocation": [ + "uri": location.relativeFile ?? "" + ], + "region": [ + "startLine": line, + "startColumn": location.character ?? 1, + ], + ], + ] + } + + return [ + "physicalLocation": [ + "artifactLocation": [ + "uri": location.file ?? "" + ], + ], + ] + } +} diff --git a/Source/SwiftLintCore/Reporters/SonarQubeReporter.swift b/Source/SwiftLintCore/Reporters/SonarQubeReporter.swift index 81483ccf45..3e014c9e13 100644 --- a/Source/SwiftLintCore/Reporters/SonarQubeReporter.swift +++ b/Source/SwiftLintCore/Reporters/SonarQubeReporter.swift @@ -9,14 +9,14 @@ struct SonarQubeReporter: Reporter { static let description = "Reports violations in SonarQube import format." static func generateReport(_ violations: [StyleViolation]) -> String { - return toJSON(["issues": violations.map(dictionary(for:))]) + toJSON(["issues": violations.map(dictionary(for:))]) } // MARK: - Private // refer to https://docs.sonarqube.org/display/SONAR/Generic+Issue+Data private static func dictionary(for violation: StyleViolation) -> [String: Any] { - return [ + [ "engineId": "SwiftLint", "ruleId": violation.ruleIdentifier, "primaryLocation": [ @@ -24,10 +24,10 @@ struct SonarQubeReporter: Reporter { "filePath": violation.location.relativeFile ?? "", "textRange": [ "startLine": violation.location.line ?? 1 - ] as Any + ] as Any, ] as Any, "type": "CODE_SMELL", - "severity": violation.severity == .error ? "MAJOR" : "MINOR" + "severity": violation.severity == .error ? "MAJOR" : "MINOR", ] } } diff --git a/Source/SwiftLintCore/Reporters/SummaryReporter.swift b/Source/SwiftLintCore/Reporters/SummaryReporter.swift index ba85a20259..24b8fa3501 100644 --- a/Source/SwiftLintCore/Reporters/SummaryReporter.swift +++ b/Source/SwiftLintCore/Reporters/SummaryReporter.swift @@ -32,22 +32,22 @@ private extension TextTable { TextTableColumn(header: numberOfWarningsHeader), TextTableColumn(header: numberOfErrorsHeader), TextTableColumn(header: numberOfViolationsHeader), - TextTableColumn(header: numberOfFilesHeader) + TextTableColumn(header: numberOfFilesHeader), ] self.init(columns: columns) let ruleIdentifiersToViolationsMap = violations.group { $0.ruleIdentifier } - let sortedRuleIdentifiers = ruleIdentifiersToViolationsMap.keys.sorted { - let count1 = ruleIdentifiersToViolationsMap[$0]?.count ?? 0 - let count2 = ruleIdentifiersToViolationsMap[$1]?.count ?? 0 + let sortedRuleIdentifiers = ruleIdentifiersToViolationsMap.sorted { lhs, rhs in + let count1 = lhs.value.count + let count2 = rhs.value.count if count1 > count2 { return true } if count1 == count2 { - return $0 < $1 + return lhs.key < rhs.key } return false - } + }.map(\.key) var totalNumberOfWarnings = 0 var totalNumberOfErrors = 0 @@ -65,7 +65,7 @@ private extension TextTable { totalNumberOfWarnings += numberOfWarnings totalNumberOfErrors += numberOfErrors let ruleViolations = ruleIdentifiersToViolationsMap[ruleIdentifier] ?? [] - let numberOfFiles = Set(ruleViolations.map { $0.location.file }).count + let numberOfFiles = Set(ruleViolations.map(\.location.file)).count addRow(values: [ ruleIdentifier, @@ -75,12 +75,12 @@ private extension TextTable { numberOfWarnings.formattedString.leftPadded(forHeader: numberOfWarningsHeader), numberOfErrors.formattedString.leftPadded(forHeader: numberOfErrorsHeader), numberOfViolations.formattedString.leftPadded(forHeader: numberOfViolationsHeader), - numberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader) + numberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader), ]) } let totalNumberOfViolations = totalNumberOfWarnings + totalNumberOfErrors - let totalNumberOfFiles = Set(violations.map { $0.location.file }).count + let totalNumberOfFiles = Set(violations.map(\.location.file)).count addRow(values: [ "Total", "", @@ -89,7 +89,7 @@ private extension TextTable { totalNumberOfWarnings.formattedString.leftPadded(forHeader: numberOfWarningsHeader), totalNumberOfErrors.formattedString.leftPadded(forHeader: numberOfErrorsHeader), totalNumberOfViolations.formattedString.leftPadded(forHeader: numberOfViolationsHeader), - totalNumberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader) + totalNumberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader), ]) } diff --git a/Source/SwiftLintCore/Reporters/XcodeReporter.swift b/Source/SwiftLintCore/Reporters/XcodeReporter.swift index cd478be29f..a7a04913db 100644 --- a/Source/SwiftLintCore/Reporters/XcodeReporter.swift +++ b/Source/SwiftLintCore/Reporters/XcodeReporter.swift @@ -7,7 +7,7 @@ struct XcodeReporter: Reporter { static let description = "Reports violations in the format Xcode uses to display in the IDE. (default)" static func generateReport(_ violations: [StyleViolation]) -> String { - return violations.map(generateForSingleViolation).joined(separator: "\n") + violations.map(generateForSingleViolation).joined(separator: "\n") } /// Generates a report for a single violation. @@ -17,12 +17,12 @@ struct XcodeReporter: Reporter { /// - returns: The report for a single violation. internal static func generateForSingleViolation(_ violation: StyleViolation) -> String { // {full_path_to_file}{:line}{:character}: {error,warning}: {content} - return [ + [ "\(violation.location): ", "\(violation.severity.rawValue): ", "\(violation.ruleName) Violation: ", violation.reason, - " (\(violation.ruleIdentifier))" + " (\(violation.ruleIdentifier))", ].joined() } } diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index 7f696694ee..c4d3acf106 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -2,7 +2,8 @@ import Foundation import SourceKittenFramework /// A rule configuration used for defining custom rules in yaml. -public struct RegexConfiguration: SeverityBasedRuleConfiguration, Hashable, CacheDescriptionProvider { +public struct RegexConfiguration: SeverityBasedRuleConfiguration, Hashable, + CacheDescriptionProvider, InlinableOptionType { /// The identifier for this custom rule. public let identifier: String /// The name for this custom rule. @@ -11,7 +12,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, public var message = "Regex matched" /// The regular expression to apply to trigger violations for this custom rule. @ConfigurationElement(key: "regex") - var regex: RegularExpression! + var regex: RegularExpression! // swiftlint:disable:this implicitly_unwrapped_optional /// Regular expressions to include when matching the file path. public var included: [NSRegularExpression] = [] /// Regular expressions to exclude when matching the file path. @@ -22,7 +23,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, @ConfigurationElement(key: "severity") public var severityConfiguration = SeverityConfiguration(.warning) /// The index of the regex capture group to match. - public var captureGroup: Int = 0 + public var captureGroup = 0 public var cacheDescription: String { let jsonObject: [String] = [ @@ -33,19 +34,20 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, included.map(\.pattern).joined(separator: ","), excluded.map(\.pattern).joined(separator: ","), SyntaxKind.allKinds.subtracting(excludedMatchKinds) - .map({ $0.rawValue }).sorted(by: <).joined(separator: ","), - severity.rawValue + .map(\.rawValue).sorted(by: <).joined(separator: ","), + severity.rawValue, ] - if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject) { - return String(decoding: jsonData, as: UTF8.self) + if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString } queuedFatalError("Could not serialize regex configuration for cache") } /// The `RuleDescription` for the custom rule defined here. public var description: RuleDescription { - return RuleDescription(identifier: identifier, name: name ?? identifier, - description: "", kind: .style) + RuleDescription(identifier: identifier, name: name ?? identifier, + description: "", kind: .style) } /// Create a `RegexConfiguration` with the specified identifier, with other properties to be set later. @@ -58,7 +60,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, public mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any], let regexString = configurationDict[$regex.key] as? String else { - throw Issue.unknownConfiguration(ruleID: Parent.description.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } regex = try RegularExpression(pattern: regexString) @@ -90,7 +92,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, } if let captureGroup = configurationDict["capture_group"] as? Int { guard (0 ... regex.numberOfCaptureGroups).contains(captureGroup) else { - throw Issue.unknownConfiguration(ruleID: Parent.description.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } self.captureGroup = captureGroup } @@ -140,7 +142,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, if let kind = SyntaxKind(shortName: $0) { return kind } - throw Issue.unknownConfiguration(ruleID: Parent.description.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } return Set(kinds) } diff --git a/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift index 8bfcdc8639..4761a0f011 100644 --- a/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift @@ -1,13 +1,13 @@ /// A rule configuration that allows specifying thresholds for `warning` and `error` severities. -public struct SeverityLevelsConfiguration: RuleConfiguration { +public struct SeverityLevelsConfiguration: RuleConfiguration, InlinableOptionType { /// The threshold for a violation to be a warning. @ConfigurationElement(key: "warning") - public var warning: Int = 12 + public var warning = 12 /// The threshold for a violation to be an error. @ConfigurationElement(key: "error") public var error: Int? - /// Create a `SeverityLevelsConfiguration` based on the sepecified `warning` and `error` thresholds. + /// Create a `SeverityLevelsConfiguration` based on the specified `warning` and `error` thresholds. /// /// - parameter warning: The threshold for a violation to be a warning. /// - parameter error: The threshold for a violation to be an error. @@ -19,8 +19,10 @@ public struct SeverityLevelsConfiguration: RuleConfiguration { /// The rule parameters that define the thresholds that should map to each severity. public var params: [RuleParameter] { if let error { - return [RuleParameter(severity: .error, value: error), - RuleParameter(severity: .warning, value: warning)] + return [ + RuleParameter(severity: .error, value: error), + RuleParameter(severity: .warning, value: warning), + ] } return [RuleParameter(severity: .warning, value: warning)] } @@ -34,7 +36,7 @@ public struct SeverityLevelsConfiguration: RuleConfiguration { if let warning = warningValue as? Int { self.warning = warning } else { - throw Issue.invalidConfiguration(ruleID: Parent.description.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } } if let errorValue = configDict[$error.key] { @@ -43,13 +45,13 @@ public struct SeverityLevelsConfiguration: RuleConfiguration { } else if let error = errorValue as? Int { self.error = error } else { - throw Issue.invalidConfiguration(ruleID: Parent.description.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } } else { self.error = nil } } else { - throw Issue.invalidConfiguration(ruleID: Parent.description.identifier) + throw Issue.nothingApplied(ruleID: Parent.identifier) } } } diff --git a/Source/SwiftLintCore/Rules/CoreRules.swift b/Source/SwiftLintCore/Rules/CoreRules.swift index a1dc3895ee..fc959d2333 100644 --- a/Source/SwiftLintCore/Rules/CoreRules.swift +++ b/Source/SwiftLintCore/Rules/CoreRules.swift @@ -1,5 +1,5 @@ /// The rule list containing all available rules built into SwiftLintCore. public let coreRules: [any Rule.Type] = [ CustomRules.self, - SuperfluousDisableCommandRule.self + SuperfluousDisableCommandRule.self, ] diff --git a/Source/SwiftLintCore/Rules/CustomRules.swift b/Source/SwiftLintCore/Rules/CustomRules.swift index d7831979d8..ec6a4f9000 100644 --- a/Source/SwiftLintCore/Rules/CustomRules.swift +++ b/Source/SwiftLintCore/Rules/CustomRules.swift @@ -9,14 +9,14 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { var cacheDescription: String { customRuleConfigurations .sorted { $0.identifier < $1.identifier } - .map { $0.cacheDescription } + .map(\.cacheDescription) .joined(separator: "\n") } var customRuleConfigurations = [RegexConfiguration]() mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any] else { - throw Issue.unknownConfiguration(ruleID: Parent.identifier) + throw Issue.invalidConfiguration(ruleID: Parent.identifier) } for (key, value) in configurationDict { @@ -38,7 +38,11 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { struct CustomRules: Rule, CacheDescriptionProvider { var cacheDescription: String { - return configuration.cacheDescription + configuration.cacheDescription + } + + var customRuleIdentifiers: [String] { + configuration.customRuleConfigurations.map(\.identifier) } static let description = RuleDescription( @@ -79,19 +83,29 @@ struct CustomRules: Rule, CacheDescriptionProvider { severity: configuration.severity, location: Location(file: file, characterOffset: $0.location), reason: configuration.message) - }).filter { violation in - guard let region = file.regions().first(where: { $0.contains(violation.location) }) else { - return true - } + }) + } + } - return !region.isRuleDisabled(customRuleIdentifier: configuration.identifier) - } + func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool { + switch ruleID { + case let .single(identifier: id): + id == Self.identifier + ? customRuleIdentifiers.contains(violation.ruleIdentifier) + : customRuleIdentifiers.contains(id) && violation.ruleIdentifier == id + default: + (self as any Rule).canBeDisabled(violation: violation, by: ruleID) } } -} -private extension Region { - func isRuleDisabled(customRuleIdentifier: String) -> Bool { - return disabledRuleIdentifiers.contains(RuleIdentifier(customRuleIdentifier)) + func isEnabled(in region: Region, for ruleID: String) -> Bool { + if !Self.description.allIdentifiers.contains(ruleID), + !customRuleIdentifiers.contains(ruleID), + Self.identifier != ruleID { + return true + } + return !region.disabledRuleIdentifiers.contains(RuleIdentifier(Self.identifier)) + && !region.disabledRuleIdentifiers.contains(RuleIdentifier(ruleID)) + && !region.disabledRuleIdentifiers.contains(.all) } } diff --git a/Source/SwiftLintCore/Rules/SuperfluousDisableCommandRule.swift b/Source/SwiftLintCore/Rules/SuperfluousDisableCommandRule.swift index f9c0c09e0d..fab3ef0b5a 100644 --- a/Source/SwiftLintCore/Rules/SuperfluousDisableCommandRule.swift +++ b/Source/SwiftLintCore/Rules/SuperfluousDisableCommandRule.swift @@ -1,7 +1,7 @@ package struct SuperfluousDisableCommandRule: SourceKitFreeRule { package var configuration = SeverityConfiguration(.warning) - package init() {} + package init() { /* Make initializer as accessible as its type. */ } package static let description = RuleDescription( identifier: "superfluous_disable_command", @@ -17,7 +17,7 @@ package struct SuperfluousDisableCommandRule: SourceKitFreeRule { // swiftlint:disable colon let abc:Void // swiftlint:enable colon - """) + """), ], triggeringExamples: [ Example("let abc: Void // swiftlint:disable:this colon"), @@ -25,23 +25,23 @@ package struct SuperfluousDisableCommandRule: SourceKitFreeRule { // swiftlint:disable colon let abc: Void // swiftlint:enable colon - """) + """), ] ) - package func validate(file: SwiftLintFile) -> [StyleViolation] { + package func validate(file _: SwiftLintFile) -> [StyleViolation] { // This rule is implemented in Linter.swift - return [] + [] } - func reason(for rule: (some Rule).Type) -> String { + func reason(forRuleIdentifier ruleIdentifier: String) -> String { """ - SwiftLint rule '\(rule.description.identifier)' did not trigger a violation in the disabled region; \ + SwiftLint rule '\(ruleIdentifier)' did not trigger a violation in the disabled region; \ remove the disable command """ } func reason(forNonExistentRule rule: String) -> String { - return "'\(rule)' is not a valid SwiftLint rule; remove it from the disable command" + "'\(rule)' is not a valid SwiftLint rule; remove it from the disable command" } } diff --git a/Source/SwiftLintCore/Visitors/DeclaredIdentifiersTrackingVisitor.swift b/Source/SwiftLintCore/Visitors/DeclaredIdentifiersTrackingVisitor.swift index fe8caeb582..c7576d7bc8 100644 --- a/Source/SwiftLintCore/Visitors/DeclaredIdentifiersTrackingVisitor.swift +++ b/Source/SwiftLintCore/Visitors/DeclaredIdentifiersTrackingVisitor.swift @@ -1,10 +1,54 @@ +import Foundation import SwiftSyntax +/// An identifier declaration. +public enum IdentifierDeclaration: Hashable { + /// Parameter declaration with a name token. + case parameter(name: TokenSyntax) + /// Local variable declaration with a name token. + case localVariable(name: TokenSyntax) + /// A variable that is implicitly added by the compiler (e.g. `error` in `catch` clauses). + case implicitVariable(name: String) + /// A variable hidden from scope because its name is a wildcard `_`. + case wildcard + /// Special case that marks a type boundary at which name lookup stops. + case lookupBoundary + + /// The name of the declared identifier (e.g. in `let a = 1` this is `a`). + fileprivate var name: String { + switch self { + case let .parameter(name): name.text + case let .localVariable(name): name.text + case let .implicitVariable(name): name + case .wildcard: "_" + case .lookupBoundary: "" + } + } + + /// Check whether self declares a variable given by name. + /// + /// - Parameters: + /// - id: Name of the variable. + /// - disregardBackticks: If `true`, normalize all names before comparison by removing all backticks. This is the + /// default since backticks only disambiguate, but don't contribute to name resolution. + public func declares(id: String, disregardBackticks: Bool = true) -> Bool { + if self == .wildcard || id == "_" { + // Insignificant names cannot refer to each other. + return false + } + if disregardBackticks { + let backticks = CharacterSet(charactersIn: "`") + return id.trimmingCharacters(in: backticks) == name.trimmingCharacters(in: backticks) + } + return id == name + } +} + /// A specialized `ViolationsSyntaxVisitor` that tracks declared identifiers per scope while traversing the AST. open class DeclaredIdentifiersTrackingVisitor: ViolationsSyntaxVisitor { - /// A type that remembers the declared identifers (in order) up to the current position in the code. - public typealias Scope = Stack> + /// A type that remembers the declared identifiers (in order) up to the current position in the code. + public typealias Scope = Stack<[IdentifierDeclaration]> /// The hierarchical stack of identifiers declared up to the current position in the code. public var scope: Scope @@ -14,7 +58,7 @@ open class DeclaredIdentifiersTrackingVisitor: /// - Parameters: /// - configuration: Configuration of a rule. /// - file: File from which the syntax tree stems from. - /// - scope: A (potentially already pre-filled) scope to collect identifers into. + /// - scope: A (potentially already pre-filled) scope to collect identifiers into. @inlinable public init(configuration: Configuration, file: SwiftLintFile, scope: Scope = Scope()) { self.scope = scope @@ -23,16 +67,18 @@ open class DeclaredIdentifiersTrackingVisitor: /// Indicate whether a given identifier is in scope. /// - /// - parameter identifier: An identifier. + /// - Parameters: + /// - identifier: An identifier. + /// - Returns: `true` if the identifier was declared previously. public func hasSeenDeclaration(for identifier: String) -> Bool { - scope.contains { $0.contains(identifier) } + scope.contains { $0.contains { $0.name == identifier } } } override open func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind { + scope.openChildScope() guard let parent = node.parent, !parent.is(SourceFileSyntax.self), let grandParent = parent.parent else { return .visitChildren } - scope.openChildScope() if let ifStmt = grandParent.as(IfExprSyntax.self), parent.keyPathInParent != \IfExprSyntax.elseBody { collectIdentifiers(from: ifStmt.conditions) } else if let whileStmt = grandParent.as(WhileStmtSyntax.self) { @@ -41,6 +87,10 @@ open class DeclaredIdentifiersTrackingVisitor: collectIdentifiers(from: pattern) } else if let parameters = grandParent.as(FunctionDeclSyntax.self)?.signature.parameterClause.parameters { collectIdentifiers(from: parameters) + } else if let parameters = grandParent.as(InitializerDeclSyntax.self)?.signature.parameterClause.parameters { + collectIdentifiers(from: parameters) + } else if let parameters = grandParent.as(SubscriptDeclSyntax.self)?.parameterClause.parameters { + collectIdentifiers(from: parameters) } else if let closureParameters = parent.as(ClosureExprSyntax.self)?.signature?.parameterClause { collectIdentifiers(from: closureParameters) } else if let switchCase = parent.as(SwitchCaseSyntax.self)?.label.as(SwitchCaseLabelSyntax.self) { @@ -51,7 +101,7 @@ open class DeclaredIdentifiersTrackingVisitor: return .visitChildren } - override open func visitPost(_ node: CodeBlockItemListSyntax) { + override open func visitPost(_: CodeBlockItemListSyntax) { scope.pop() } @@ -67,40 +117,75 @@ open class DeclaredIdentifiersTrackingVisitor: collectIdentifiers(from: node.conditions) } + // MARK: Type declaration boundaries + + override open func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind { + if node.belongsToTypeDefinableInFunction { + scope.push([.lookupBoundary]) + } + return .visitChildren + } + + override open func visitPost(_ node: MemberBlockSyntax) { + if node.belongsToTypeDefinableInFunction { + scope.pop() + } + } + + // MARK: Private methods + private func collectIdentifiers(from parameters: FunctionParameterListSyntax) { - parameters.forEach { scope.addToCurrentScope(($0.secondName ?? $0.firstName).text) } + for param in parameters { + let name = param.secondName ?? param.firstName + scope.addToCurrentScope(.parameter(name: name)) + } } private func collectIdentifiers(from closureParameters: ClosureSignatureSyntax.ParameterClause) { switch closureParameters { case let .parameterClause(parameters): - parameters.parameters.forEach { scope.addToCurrentScope(($0.secondName ?? $0.firstName).text) } + for param in parameters.parameters { + let name = param.secondName ?? param.firstName + scope.addToCurrentScope(.parameter(name: name)) + } case let .simpleInput(parameters): - parameters.forEach { scope.addToCurrentScope($0.name.text) } + for param in parameters { + let name = param.name + scope.addToCurrentScope(.parameter(name: name)) + } } } private func collectIdentifiers(from switchCase: SwitchCaseLabelSyntax) { switchCase.caseItems - .compactMap { $0.pattern.as(ValueBindingPatternSyntax.self)?.pattern ?? $0.pattern } - .compactMap { $0.as(ExpressionPatternSyntax.self)?.expression.asFunctionCall } - .compactMap { $0.arguments.as(LabeledExprListSyntax.self) } + .map { item -> PatternSyntax in + item.pattern.as(ValueBindingPatternSyntax.self)?.pattern ?? item.pattern + } + .compactMap { pattern -> FunctionCallExprSyntax? in + pattern.as(ExpressionPatternSyntax.self)?.expression.asFunctionCall + } + .map(\.arguments) .flatMap { $0 } - .compactMap { $0.expression.as(PatternExprSyntax.self) } - .compactMap { $0.pattern.as(ValueBindingPatternSyntax.self)?.pattern ?? $0.pattern } - .compactMap { $0.as(IdentifierPatternSyntax.self) } - .forEach { scope.addToCurrentScope($0.identifier.text) } + .compactMap { labeledExpr -> PatternExprSyntax? in + labeledExpr.expression.as(PatternExprSyntax.self) + } + .map { patternExpr -> any PatternSyntaxProtocol in + patternExpr.pattern.as(ValueBindingPatternSyntax.self)?.pattern ?? patternExpr.pattern + } + .forEach { + collectIdentifiers(from: PatternSyntax(fromProtocol: $0)) + } } private func collectIdentifiers(from catchClause: CatchClauseSyntax) { let items = catchClause.catchItems - if items.isNotEmpty { + if items.isEmpty { + // A catch clause without explicit catch items has an implicit `error` variable in scope. + scope.addToCurrentScope(.implicitVariable(name: "error")) + } else { items .compactMap { $0.pattern?.as(ValueBindingPatternSyntax.self)?.pattern } .forEach(collectIdentifiers(from:)) - } else { - // A catch clause without explicit catch items has an implicit `error` variable in scope. - scope.addToCurrentScope("error") } } @@ -111,18 +196,27 @@ open class DeclaredIdentifiersTrackingVisitor: } private func collectIdentifiers(from pattern: PatternSyntax) { - if let name = pattern.as(IdentifierPatternSyntax.self)?.identifier.text { - scope.addToCurrentScope(name) + if let id = pattern.as(IdentifierPatternSyntax.self)?.identifier { + scope.addToCurrentScope(.localVariable(name: id)) } } } private extension DeclaredIdentifiersTrackingVisitor.Scope { - mutating func addToCurrentScope(_ identifier: String) { - modifyLast { $0.insert(identifier) } + mutating func addToCurrentScope(_ decl: IdentifierDeclaration) { + modifyLast { $0.append(decl.name == "_" ? .wildcard : decl) } } mutating func openChildScope() { push([]) } } + +private extension MemberBlockSyntax { + var belongsToTypeDefinableInFunction: Bool { + if let parent { + return [.actorDecl, .classDecl, .enumDecl, .structDecl].contains(parent.kind) + } + return false + } +} diff --git a/Source/SwiftLintCore/Visitors/ViolationsSyntaxVisitor.swift b/Source/SwiftLintCore/Visitors/ViolationsSyntaxVisitor.swift index 9a9780590c..9551411ce2 100644 --- a/Source/SwiftLintCore/Visitors/ViolationsSyntaxVisitor.swift +++ b/Source/SwiftLintCore/Visitors/ViolationsSyntaxVisitor.swift @@ -11,7 +11,7 @@ open class ViolationsSyntaxVisitor: SyntaxVisi public lazy var locationConverter = file.locationConverter /// Initializer for a ``ViolationsSyntaxVisitor``. - /// + /// /// - Parameters: /// - configuration: Configuration of a rule. /// - file: File from which the syntax tree stems from. @@ -24,9 +24,6 @@ open class ViolationsSyntaxVisitor: SyntaxVisi /// Positions in a source file where violations should be reported. public var violations: [ReasonedRuleViolation] = [] - /// Ranges of violations to be used in rewriting (see ``SwiftSyntaxCorrectableRule``). It is not mandatory to fill - /// this list while traversing the AST, especially not if the rule is not correctable or provides a custom rewriter. - public var violationCorrections = [ViolationCorrection]() /// List of declaration types that shall be skipped while traversing the AST. open var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [] } @@ -56,28 +53,6 @@ open class ViolationsSyntaxVisitor: SyntaxVisi } } -/// The correction of a violation that is basically the violation's range in the source code and a -/// replacement for this range that would fix the violation. -public struct ViolationCorrection { - /// Start position of the violation range. - public let start: AbsolutePosition - /// End position of the violation range. - let end: AbsolutePosition - /// Replacement for the violating range. - let replacement: String - - /// Create a ``ViolationCorrection``. - /// - Parameters: - /// - start: Start position of the violation range. - /// - end: End position of the violation range. - /// - replacement: Replacement for the violating range. - public init(start: AbsolutePosition, end: AbsolutePosition, replacement: String) { - self.start = start - self.end = end - self.replacement = replacement - } -} - public extension Array where Element == any DeclSyntaxProtocol.Type { /// All visitable declaration syntax types. static let all: Self = [ @@ -90,7 +65,7 @@ public extension Array where Element == any DeclSyntaxProtocol.Type { ProtocolDeclSyntax.self, StructDeclSyntax.self, SubscriptDeclSyntax.self, - VariableDeclSyntax.self + VariableDeclSyntax.self, ] /// All declarations except for the specified ones. diff --git a/Source/SwiftLintCoreMacros/RuleConfigurationMacros.swift b/Source/SwiftLintCoreMacros/RuleConfigurationMacros.swift index f9df158831..51ba4427d7 100644 --- a/Source/SwiftLintCoreMacros/RuleConfigurationMacros.swift +++ b/Source/SwiftLintCoreMacros/RuleConfigurationMacros.swift @@ -1,9 +1,12 @@ +import Foundation import SwiftSyntax +import SwiftSyntaxBuilder import SwiftSyntaxMacros -enum AutoApply: MemberMacro { +enum AutoConfigParser: MemberMacro { + // swiftlint:disable:next function_body_length static func expansion( - of node: AttributeSyntax, + of _: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { @@ -22,53 +25,80 @@ enum AutoApply: MemberMacro { let firstIndexWithoutKey = annotatedVarDecls .partition { _, annotation in if case let .argumentList(arguments) = annotation.arguments { - return arguments.contains { $0.label?.text == "key" } == true + return arguments.contains { + $0.label?.text == "inline" + && $0.expression.as(BooleanLiteralExprSyntax.self)?.literal.text == "true" + } } return false } let elementNames = annotatedVarDecls.compactMap { $0.0.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } - let elementsWithoutKeyUpdate = elementNames[.. [ExtensionDeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { @@ -88,11 +118,11 @@ enum MakeAcceptableByConfigurationElement: ExtensionMacro { if let value = value as? String, let newSelf = Self(rawValue: value) { self = newSelf } else { - throw Issue.unknownConfiguration(ruleID: ruleID) + throw Issue.invalidConfiguration(ruleID: ruleID) } } } - """) + """), ] } } @@ -130,3 +160,16 @@ private extension EnumDeclSyntax { }.first ?? "" } } + +private extension String { + // swiftlint:disable:next force_try + static let regex = try! NSRegularExpression(pattern: "(? [ExtensionDeclSyntax] { [ @@ -34,7 +34,7 @@ enum SwiftSyntaxRule: ExtensionMacro { } } """ - ) + ), ].compactMap { $0 } } diff --git a/Source/swiftlint/Helpers/Benchmark.swift b/Source/SwiftLintFramework/Benchmark.swift similarity index 93% rename from Source/swiftlint/Helpers/Benchmark.swift rename to Source/SwiftLintFramework/Benchmark.swift index 18e111a0c5..76b113155c 100644 --- a/Source/swiftlint/Helpers/Benchmark.swift +++ b/Source/SwiftLintFramework/Benchmark.swift @@ -1,5 +1,4 @@ import Foundation -import SwiftLintFramework struct BenchmarkEntry { let id: String @@ -31,7 +30,7 @@ struct Benchmark { let entriesKeyValues: [(String, Double)] = entriesDict.sorted { $0.1 < $1.1 } let lines: [String] = entriesKeyValues.map { id, time -> String in // swiftlint:disable:next legacy_objc_type - return "\(numberFormatter.string(from: NSNumber(value: time))!): \(id)" + "\(numberFormatter.string(from: NSNumber(value: time))!): \(id)" } let string: String = lines.joined(separator: "\n") + "\n" let url = URL(fileURLWithPath: "benchmark_\(name)_\(timestamp).txt", isDirectory: false) diff --git a/Source/swiftlint/Helpers/CompilerArgumentsExtractor.swift b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift similarity index 93% rename from Source/swiftlint/Helpers/CompilerArgumentsExtractor.swift rename to Source/SwiftLintFramework/CompilerArgumentsExtractor.swift index 71a60e7409..d9b1c05545 100644 --- a/Source/swiftlint/Helpers/CompilerArgumentsExtractor.swift +++ b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift @@ -59,7 +59,7 @@ private func partiallyFilter(arguments args: [String]) -> ([String], Bool) { extension Array where Element == String { /// Return the full list of compiler arguments, replacing any response files with their contents. fileprivate var expandingResponseFiles: [String] { - return flatMap { arg -> [String] in + flatMap { arg -> [String] in guard arg.starts(with: "@") else { return [arg] } @@ -88,22 +88,22 @@ extension Array where Element == String { (args, shouldContinueToFilterArguments) = partiallyFilter(arguments: args) } - return args.filter { + return args.filter { arg in ![ "-parseable-output", "-incremental", "-serialize-diagnostics", "-emit-dependencies", - "-use-frontend-parseable-output" - ].contains($0) - }.map { - if $0 == "-O" { + "-use-frontend-parseable-output", + ].contains(arg) + }.map { arg in + if arg == "-O" { return "-Onone" } - if $0 == "-DNDEBUG=1" { + if arg == "-DNDEBUG=1" { return "-DDEBUG=1" } - return $0 + return arg } } } diff --git a/Source/swiftlint/Extensions/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift similarity index 74% rename from Source/swiftlint/Extensions/Configuration+CommandLine.swift rename to Source/SwiftLintFramework/Configuration+CommandLine.swift index d9422a4d42..c24f5c9c55 100644 --- a/Source/swiftlint/Extensions/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -1,7 +1,6 @@ import CollectionConcurrencyKit import Foundation import SourceKittenFramework -import SwiftLintFramework private actor CounterActor { private var count = 0 @@ -12,16 +11,8 @@ private actor CounterActor { } } -private func scriptInputFiles() throws -> [SwiftLintFile] { - let inputFileKey = "SCRIPT_INPUT_FILE_COUNT" - guard let countString = ProcessInfo.processInfo.environment[inputFileKey] else { - throw SwiftLintError.usageError(description: "\(inputFileKey) variable not set") - } - - guard let count = Int(countString) else { - throw SwiftLintError.usageError(description: "\(inputFileKey) did not specify a number") - } - +private func readFilesFromScriptInputFiles() throws -> [SwiftLintFile] { + let count = try fileCount(from: "SCRIPT_INPUT_FILE_COUNT") return (0.. [SwiftLintFile] { } } +private func readFilesFromScriptInputFileLists() throws -> [SwiftLintFile] { + let count = try fileCount(from: "SCRIPT_INPUT_FILE_LIST_COUNT") + return (0.. Int { + guard let countString = ProcessInfo.processInfo.environment[envVar] else { + throw SwiftLintError.usageError(description: "\(envVar) variable not set") + } + guard let count = Int(countString) else { + throw SwiftLintError.usageError(description: "\(envVar) did not specify a number") + } + return count +} + #if os(Linux) -private func autoreleasepool(block: () -> T) -> T { return block() } +private func autoreleasepool(block: () -> T) -> T { block() } #endif extension Configuration { @@ -78,7 +107,7 @@ extension Configuration { -> [Configuration: [SwiftLintFile]] { if files.isEmpty && !visitor.allowZeroLintableFiles { throw SwiftLintError.usageError( - description: "No lintable files found at paths: '\(visitor.paths.joined(separator: ", "))'" + description: "No lintable files found at paths: '\(visitor.options.paths.joined(separator: ", "))'" ) } @@ -142,13 +171,13 @@ extension Configuration { let counter = CounterActor() let total = linters.filter(\.isCollecting).count let progress = ProgressBar(count: total) - if visitor.showProgressBar && total > 0 { + if visitor.options.progress && total > 0 { await progress.initialize() } let collect = { (linter: Linter) -> CollectedLinter? in let skipFile = visitor.shouldSkipFile(atPath: linter.file.path) - if !visitor.quiet && linter.isCollecting { - if visitor.showProgressBar { + if !visitor.options.quiet && linter.isCollecting { + if visitor.options.progress { await progress.printNext() } else if let filePath = linter.file.path { let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames) @@ -186,17 +215,19 @@ extension Configuration { duplicateFileNames: Set) async -> [SwiftLintFile] { let counter = CounterActor() let progress = ProgressBar(count: linters.count) - if visitor.showProgressBar { + if visitor.options.progress { await progress.initialize() } let visit = { (linter: CollectedLinter) -> SwiftLintFile in - if !visitor.quiet { - if visitor.showProgressBar { + if !visitor.options.quiet { + if visitor.options.progress { await progress.printNext() } else if let filePath = linter.file.path { let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames) let visited = await counter.next() - queuedPrintError("\(visitor.action) '\(outputFilename)' (\(visited)/\(linters.count))") + queuedPrintError( + "\(visitor.options.capitalizedVerb) '\(outputFilename)' (\(visited)/\(linters.count))" + ) } } @@ -211,48 +242,55 @@ extension Configuration { } fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] { - if visitor.useSTDIN { + let options = visitor.options + if options.useSTDIN { let stdinData = FileHandle.standardInput.readDataToEndOfFile() - let stdinString = String(decoding: stdinData, as: UTF8.self) - return [SwiftLintFile(contents: stdinString)] + if let stdinString = String(data: stdinData, encoding: .utf8) { + return [SwiftLintFile(contents: stdinString)] + } + throw SwiftLintError.usageError(description: "stdin isn't a UTF8-encoded string") } - if visitor.useScriptInputFiles { - let files = try scriptInputFiles() - guard visitor.forceExclude else { + if options.useScriptInputFiles || options.useScriptInputFileLists { + let files = try options.useScriptInputFiles + ? readFilesFromScriptInputFiles() + : readFilesFromScriptInputFileLists() + guard options.forceExclude else { return files } - let scriptInputPaths = files.compactMap { $0.path } + let scriptInputPaths = files.compactMap(\.path) - if visitor.useExcludingByPrefix { + if options.useExcludingByPrefix { return filterExcludedPathsByPrefix(in: scriptInputPaths) .map(SwiftLintFile.init(pathDeferringReading:)) } return filterExcludedPaths(excludedPaths(), in: scriptInputPaths) .map(SwiftLintFile.init(pathDeferringReading:)) } - if !visitor.quiet { + if !options.quiet { let filesInfo: String - if visitor.paths.isEmpty || visitor.paths == [""] { + if options.paths.isEmpty || options.paths == [""] { filesInfo = "in current working directory" } else { - filesInfo = "at paths \(visitor.paths.joined(separator: ", "))" + filesInfo = "at paths \(options.paths.joined(separator: ", "))" } - queuedPrintError("\(visitor.action) Swift files \(filesInfo)") + queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } - let excludeLintableFilesBy = visitor.useExcludingByPrefix + let excludeLintableFilesBy = options.useExcludingByPrefix ? Configuration.ExcludeBy.prefix : .paths(excludedPaths: excludedPaths()) - return visitor.paths.flatMap { + return options.paths.flatMap { self.lintableFiles( inPath: $0, - forceExclude: visitor.forceExclude, + forceExclude: options.forceExclude, excludeBy: excludeLintableFilesBy) } } - func visitLintableFiles(options: LintOrAnalyzeOptions, cache: LinterCache? = nil, storage: RuleStorage, + func visitLintableFiles(options: LintOrAnalyzeOptions, + cache: LinterCache? = nil, + storage: RuleStorage, visitorBlock: @escaping (CollectedLinter) async -> Void) async throws -> [SwiftLintFile] { let visitor = try LintableFilesVisitor.create(options, cache: cache, allowZeroLintableFiles: allowZeroLintableFiles, @@ -266,6 +304,7 @@ extension Configuration { self.init( configurationFiles: options.configurationFiles, enableAllRules: options.enableAllRules, + onlyRule: options.onlyRule, cachePath: options.cachePath ) } diff --git a/Source/swiftlint/Helpers/ExitHelper.swift b/Source/SwiftLintFramework/ExitHelper.swift similarity index 68% rename from Source/swiftlint/Helpers/ExitHelper.swift rename to Source/SwiftLintFramework/ExitHelper.swift index cd20c6a7de..f2bf1281ea 100644 --- a/Source/swiftlint/Helpers/ExitHelper.swift +++ b/Source/SwiftLintFramework/ExitHelper.swift @@ -2,8 +2,8 @@ import Glibc #endif -enum ExitHelper { - static func successfullyExit() { +package enum ExitHelper { + package static func successfullyExit() { #if os(Linux) // Workaround for https://github.com/apple/swift/issues/59961 Glibc.exit(0) diff --git a/Source/SwiftLintFramework/Exports.swift b/Source/SwiftLintFramework/Exports.swift index b61dabc36c..a352d954cd 100644 --- a/Source/SwiftLintFramework/Exports.swift +++ b/Source/SwiftLintFramework/Exports.swift @@ -6,7 +6,7 @@ private let _registerAllRulesOnceImpl: Void = { RuleRegistry.shared.register(rules: builtInRules + coreRules + extraRules()) }() -public extension RuleRegistry { +package extension RuleRegistry { /// Register all rules. Should only be called once before any SwiftLint code is executed. static func registerAllRulesOnce() { _ = _registerAllRulesOnceImpl diff --git a/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift similarity index 66% rename from Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift rename to Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index da717523c7..746342c301 100644 --- a/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -1,11 +1,15 @@ +#if os(macOS) +@preconcurrency import Darwin +#endif import Dispatch import Foundation -import SwiftLintFramework -enum LintOrAnalyzeMode { +// swiftlint:disable file_length + +package enum LintOrAnalyzeMode { case lint, analyze - var imperative: String { + package var imperative: String { switch self { case .lint: return "lint" @@ -14,7 +18,7 @@ enum LintOrAnalyzeMode { } } - var verb: String { + package var verb: String { switch self { case .lint: return "linting" @@ -24,16 +28,111 @@ enum LintOrAnalyzeMode { } } -struct LintOrAnalyzeCommand { - static func run(_ options: LintOrAnalyzeOptions) async throws { - if options.inProcessSourcekit { - // TODO: [08/11/2024] Remove deprecation warning after ~2 years. - queuedPrintError( - """ - warning: The --in-process-sourcekit option is deprecated. \ - SwiftLint now always uses an in-process SourceKit. - """ - ) +package struct LintOrAnalyzeOptions { + let mode: LintOrAnalyzeMode + let paths: [String] + let useSTDIN: Bool + let configurationFiles: [String] + let strict: Bool + let lenient: Bool + let forceExclude: Bool + let useExcludingByPrefix: Bool + let useScriptInputFiles: Bool + let useScriptInputFileLists: Bool + let benchmark: Bool + let reporter: String? + let baseline: String? + let writeBaseline: String? + let workingDirectory: String? + let quiet: Bool + let output: String? + let progress: Bool + let cachePath: String? + let ignoreCache: Bool + let enableAllRules: Bool + let onlyRule: [String] + let autocorrect: Bool + let format: Bool + let compilerLogPath: String? + let compileCommands: String? + let checkForUpdates: Bool + + package init(mode: LintOrAnalyzeMode, + paths: [String], + useSTDIN: Bool, + configurationFiles: [String], + strict: Bool, + lenient: Bool, + forceExclude: Bool, + useExcludingByPrefix: Bool, + useScriptInputFiles: Bool, + useScriptInputFileLists: Bool, + benchmark: Bool, + reporter: String?, + baseline: String?, + writeBaseline: String?, + workingDirectory: String?, + quiet: Bool, + output: String?, + progress: Bool, + cachePath: String?, + ignoreCache: Bool, + enableAllRules: Bool, + onlyRule: [String], + autocorrect: Bool, + format: Bool, + compilerLogPath: String?, + compileCommands: String?, + checkForUpdates: Bool) { + self.mode = mode + self.paths = paths + self.useSTDIN = useSTDIN + self.configurationFiles = configurationFiles + self.strict = strict + self.lenient = lenient + self.forceExclude = forceExclude + self.useExcludingByPrefix = useExcludingByPrefix + self.useScriptInputFiles = useScriptInputFiles + self.useScriptInputFileLists = useScriptInputFileLists + self.benchmark = benchmark + self.reporter = reporter + self.baseline = baseline + self.writeBaseline = writeBaseline + self.workingDirectory = workingDirectory + self.quiet = quiet + self.output = output + self.progress = progress + self.cachePath = cachePath + self.ignoreCache = ignoreCache + self.enableAllRules = enableAllRules + self.onlyRule = onlyRule + self.autocorrect = autocorrect + self.format = format + self.compilerLogPath = compilerLogPath + self.compileCommands = compileCommands + self.checkForUpdates = checkForUpdates + } + + var verb: String { + autocorrect ? "correcting" : mode.verb + } + + var capitalizedVerb: String { + verb.capitalized + } +} + +package struct LintOrAnalyzeCommand { + package static func run(_ options: LintOrAnalyzeOptions) async throws { + if let workingDirectory = options.workingDirectory { + if !FileManager.default.changeCurrentDirectoryPath(workingDirectory) { + throw SwiftLintError.usageError( + description: """ + Could not change working directory to '\(workingDirectory)'. \ + Make sure it exists and is accessible. + """ + ) + } } try await Signposts.record(name: "LintOrAnalyzeCommand.run") { try await options.autocorrect ? autocorrect(options) : lintOrAnalyze(options) @@ -44,14 +143,21 @@ struct LintOrAnalyzeCommand { private static func lintOrAnalyze(_ options: LintOrAnalyzeOptions) async throws { let builder = LintOrAnalyzeResultBuilder(options) let files = try await collectViolations(builder: builder) + if let baselineOutputPath = options.writeBaseline ?? builder.configuration.writeBaseline { + try Baseline(violations: builder.unfilteredViolations).write(toPath: baselineOutputPath) + } try Signposts.record(name: "LintOrAnalyzeCommand.PostProcessViolations") { try postProcessViolations(files: files, builder: builder) } + if options.checkForUpdates || builder.configuration.checkForUpdates { + await UpdateChecker.checkForUpdates() + } } private static func collectViolations(builder: LintOrAnalyzeResultBuilder) async throws -> [SwiftLintFile] { let options = builder.options let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation") + let baseline = try baseline(options, builder.configuration) return try await builder.configuration.visitLintableFiles(options: options, cache: builder.cache, storage: builder.storage) { linter in let currentViolations: [StyleViolation] @@ -63,25 +169,29 @@ struct LintOrAnalyzeCommand { currentViolations = applyLeniency( options: options, strict: builder.configuration.strict, + lenient: builder.configuration.lenient, violations: violationsBeforeLeniency ) visitorMutationQueue.sync { builder.fileBenchmark.record(file: linter.file, from: start) currentRuleTimes.forEach { builder.ruleBenchmark.record(id: $0, time: $1) } - builder.violations += currentViolations } } else { currentViolations = applyLeniency( options: options, strict: builder.configuration.strict, + lenient: builder.configuration.lenient, violations: linter.styleViolations(using: builder.storage) ) - visitorMutationQueue.sync { - builder.violations += currentViolations - } } + let filteredViolations = baseline?.filter(currentViolations) ?? currentViolations + visitorMutationQueue.sync { + builder.unfilteredViolations += currentViolations + builder.violations += filteredViolations + } + linter.file.invalidateCache() - builder.report(violations: currentViolations, realtimeCondition: true) + builder.report(violations: filteredViolations, realtimeCondition: true) } } @@ -115,9 +225,25 @@ struct LintOrAnalyzeCommand { guard numberOfSeriousViolations == 0 else { exit(2) } } + private static func baseline(_ options: LintOrAnalyzeOptions, _ configuration: Configuration) throws -> Baseline? { + if let baselinePath = options.baseline ?? configuration.baseline { + do { + return try Baseline(fromPath: baselinePath) + } catch { + Issue.baselineNotReadable(path: baselinePath).print() + if + (error as? CocoaError)?.code != CocoaError.fileReadNoSuchFile || + options.writeBaseline != options.baseline { + throw error + } + } + } + return nil + } + private static func printStatus(violations: [StyleViolation], files: [SwiftLintFile], serious: Int, verb: String) { let pluralSuffix = { (collection: [Any]) -> String in - return collection.count != 1 ? "s" : "" + collection.count != 1 ? "s" : "" } queuedPrintError( "Done \(verb)! Found \(violations.count) violation\(pluralSuffix(violations)), " + @@ -149,15 +275,16 @@ struct LintOrAnalyzeCommand { private static func applyLeniency( options: LintOrAnalyzeOptions, strict: Bool, + lenient: Bool, violations: [StyleViolation] ) -> [StyleViolation] { - let strict = (strict && !options.lenient) || options.strict + let leniency = options.leniency(strict: strict, lenient: lenient) - switch (options.lenient, strict) { + switch leniency { case (false, false): return violations - case (true, false): + case (false, true): return violations.map { if $0.severity == .error { return $0.with(severity: .warning) @@ -165,7 +292,7 @@ struct LintOrAnalyzeCommand { return $0 } - case (false, true): + case (true, false): return violations.map { if $0.severity == .warning { return $0.with(severity: .error) @@ -174,7 +301,7 @@ struct LintOrAnalyzeCommand { } case (true, true): - queuedFatalError("Invalid command line options: 'lenient' and 'strict' are mutually exclusive.") + queuedFatalError("Invalid command line or config options: 'strict' and 'lenient' are mutually exclusive.") } } @@ -218,48 +345,19 @@ struct LintOrAnalyzeCommand { } let pluralSuffix = { (collection: [Any]) -> String in - return collection.count != 1 ? "s" : "" + collection.count != 1 ? "s" : "" } queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!") } } } -struct LintOrAnalyzeOptions { - let mode: LintOrAnalyzeMode - let paths: [String] - let useSTDIN: Bool - let configurationFiles: [String] - let strict: Bool - let lenient: Bool - let forceExclude: Bool - let useExcludingByPrefix: Bool - let useScriptInputFiles: Bool - let benchmark: Bool - let reporter: String? - let quiet: Bool - let output: String? - let progress: Bool - let cachePath: String? - let ignoreCache: Bool - let enableAllRules: Bool - let autocorrect: Bool - let format: Bool - let compilerLogPath: String? - let compileCommands: String? - let inProcessSourcekit: Bool - - var verb: String { - if autocorrect { - return "correcting" - } - return mode.verb - } -} - private class LintOrAnalyzeResultBuilder { var fileBenchmark = Benchmark(name: "files") var ruleBenchmark = Benchmark(name: "rules") + /// All detected violations, unfiltered by the baseline, if any. + var unfilteredViolations = [StyleViolation]() + /// The violations to be reported, possibly filtered by a baseline, plus any threshold violations. var violations = [StyleViolation]() let storage = RuleStorage() let configuration: Configuration @@ -299,8 +397,8 @@ private class LintOrAnalyzeResultBuilder { } } -private extension LintOrAnalyzeOptions { - func writeToOutput(_ string: String) { +extension LintOrAnalyzeOptions { + fileprivate func writeToOutput(_ string: String) { guard let outFile = output else { queuedPrint(string) return @@ -316,6 +414,15 @@ private extension LintOrAnalyzeOptions { Issue.fileNotWritable(path: outFile).print() } } + + typealias Leniency = (strict: Bool, lenient: Bool) + + // Config file settings can be overridden by either `--strict` or `--lenient` command line options. + func leniency(strict configurationStrict: Bool, lenient configurationLenient: Bool) -> Leniency { + let strict = self.strict || (configurationStrict && !self.lenient) + let lenient = self.lenient || (configurationLenient && !self.strict) + return Leniency(strict: strict, lenient: lenient) + } } private actor CorrectionsBuilder { diff --git a/Source/swiftlint/Helpers/LintableFilesVisitor.swift b/Source/SwiftLintFramework/LintableFilesVisitor.swift similarity index 75% rename from Source/swiftlint/Helpers/LintableFilesVisitor.swift rename to Source/SwiftLintFramework/LintableFilesVisitor.swift index b13993ddc7..c6c630e335 100644 --- a/Source/swiftlint/Helpers/LintableFilesVisitor.swift +++ b/Source/SwiftLintFramework/LintableFilesVisitor.swift @@ -1,21 +1,20 @@ import Foundation import SourceKittenFramework -import SwiftLintFramework typealias File = String typealias Arguments = [String] class CompilerInvocations { static func buildLog(compilerInvocations: [[String]]) -> CompilerInvocations { - return ArrayCompilerInvocations(invocations: compilerInvocations) + ArrayCompilerInvocations(invocations: compilerInvocations) } static func compilationDatabase(compileCommands: [File: Arguments]) -> CompilerInvocations { - return CompilationDatabaseInvocations(compileCommands: compileCommands) + CompilationDatabaseInvocations(compileCommands: compileCommands) } /// Default implementation - func arguments(forFile path: String?) -> Arguments { [] } + func arguments(forFile _: String?) -> Arguments { [] } // MARK: - Private @@ -31,8 +30,8 @@ class CompilerInvocations { } override func arguments(forFile path: String?) -> Arguments { - return path.flatMap { path in - return invocationsByArgument[path]?.first + path.flatMap { path in + invocationsByArgument[path]?.first } ?? [] } } @@ -45,8 +44,8 @@ class CompilerInvocations { } override func arguments(forFile path: String?) -> Arguments { - return path.flatMap { path in - return compileCommands[path] ?? + path.flatMap { path in + compileCommands[path] ?? compileCommands[path.path(relativeTo: FileManager.default.currentDirectoryPath)] } ?? [] } @@ -59,7 +58,7 @@ enum LintOrAnalyzeModeWithCompilerArguments { } private func resolveParamsFiles(args: [String]) -> [String] { - return args.reduce(into: []) { (allArgs: inout [String], arg: String) in + args.reduce(into: []) { (allArgs: inout [String], arg: String) in if arg.hasPrefix("@"), let contents = try? String(contentsOfFile: String(arg.dropFirst())) { allArgs.append(contentsOf: resolveParamsFiles(args: contents.split(separator: "\n").map(String.init))) } else { @@ -69,70 +68,43 @@ private func resolveParamsFiles(args: [String]) -> [String] { } struct LintableFilesVisitor { - let paths: [String] - let action: String - let useSTDIN: Bool - let quiet: Bool - let showProgressBar: Bool - let useScriptInputFiles: Bool - let forceExclude: Bool - let useExcludingByPrefix: Bool + let options: LintOrAnalyzeOptions let cache: LinterCache? - let parallel: Bool - let allowZeroLintableFiles: Bool let mode: LintOrAnalyzeModeWithCompilerArguments + let parallel: Bool let block: (CollectedLinter) async -> Void + let allowZeroLintableFiles: Bool - private init(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, showProgressBar: Bool, - useScriptInputFiles: Bool, forceExclude: Bool, useExcludingByPrefix: Bool, - cache: LinterCache?, compilerInvocations: CompilerInvocations?, - allowZeroLintableFiles: Bool, block: @escaping (CollectedLinter) async -> Void) { - self.paths = resolveParamsFiles(args: paths) - self.action = action - self.useSTDIN = useSTDIN - self.quiet = quiet - self.showProgressBar = showProgressBar - self.useScriptInputFiles = useScriptInputFiles - self.forceExclude = forceExclude - self.useExcludingByPrefix = useExcludingByPrefix + private init(options: LintOrAnalyzeOptions, + cache: LinterCache?, + allowZeroLintableFiles: Bool, + block: @escaping (CollectedLinter) async -> Void) throws { + self.options = options self.cache = cache - if let compilerInvocations { - self.mode = .analyze(allCompilerInvocations: compilerInvocations) + if options.mode == .lint { + self.mode = .lint + self.parallel = true + } else { + self.mode = .analyze(allCompilerInvocations: try Self.loadCompilerInvocations(options)) // SourceKit had some changes in 5.6 that makes it ~100x more expensive // to process files concurrently. By processing files serially, it's // only 2x slower than before. self.parallel = SwiftVersion.current < .fiveDotSix - } else { - self.mode = .lint - self.parallel = true } - self.block = block self.allowZeroLintableFiles = allowZeroLintableFiles + self.block = block } static func create(_ options: LintOrAnalyzeOptions, cache: LinterCache?, allowZeroLintableFiles: Bool, - block: @escaping (CollectedLinter) async -> Void) - throws -> LintableFilesVisitor { + block: @escaping (CollectedLinter) async -> Void) throws -> Self { try Signposts.record(name: "LintableFilesVisitor.Create") { - let compilerInvocations: CompilerInvocations? - if options.mode == .lint { - compilerInvocations = nil - } else { - compilerInvocations = try loadCompilerInvocations(options) - } - - return Self( - paths: options.paths, action: options.verb.bridge().capitalized, - useSTDIN: options.useSTDIN, quiet: options.quiet, - showProgressBar: options.progress, - useScriptInputFiles: options.useScriptInputFiles, - forceExclude: options.forceExclude, - useExcludingByPrefix: options.useExcludingByPrefix, + try Self( + options: options, cache: cache, - compilerInvocations: compilerInvocations, - allowZeroLintableFiles: allowZeroLintableFiles, block: block + allowZeroLintableFiles: allowZeroLintableFiles, + block: block ) } } @@ -179,8 +151,8 @@ struct LintableFilesVisitor { } private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? { - if let data = FileManager.default.contents(atPath: path) { - let logContents = String(decoding: data, as: UTF8.self) + if let data = FileManager.default.contents(atPath: path), + let logContents = String(data: data, encoding: .utf8) { if logContents.isEmpty { return nil } diff --git a/Source/swiftlint/Extensions/ProcessInfo+XcodeCloud.swift b/Source/SwiftLintFramework/ProcessInfo+XcodeCloud.swift similarity index 95% rename from Source/swiftlint/Extensions/ProcessInfo+XcodeCloud.swift rename to Source/SwiftLintFramework/ProcessInfo+XcodeCloud.swift index c7e25c0686..e94ceeb209 100644 --- a/Source/swiftlint/Extensions/ProcessInfo+XcodeCloud.swift +++ b/Source/SwiftLintFramework/ProcessInfo+XcodeCloud.swift @@ -20,7 +20,7 @@ extension ProcessInfo { "CI_WORKSPACE", "CI_XCODE_PROJECT", "CI_XCODE_SCHEME", - "CI_XCODEBUILD_ACTION" + "CI_XCODEBUILD_ACTION", ] return requiredKeys.isSubset(of: environment.keys) diff --git a/Source/swiftlint/Helpers/ProgressBar.swift b/Source/SwiftLintFramework/ProgressBar.swift similarity index 98% rename from Source/swiftlint/Helpers/ProgressBar.swift rename to Source/SwiftLintFramework/ProgressBar.swift index 0d50555df6..b5a08ab62f 100644 --- a/Source/swiftlint/Helpers/ProgressBar.swift +++ b/Source/SwiftLintFramework/ProgressBar.swift @@ -1,6 +1,5 @@ import Dispatch import Foundation -import SwiftLintFramework // Inspired by https://github.com/jkandzi/Progress.swift actor ProgressBar { diff --git a/Source/swiftlint/Helpers/RulesFilter.swift b/Source/SwiftLintFramework/RulesFilter.swift similarity index 59% rename from Source/swiftlint/Helpers/RulesFilter.swift rename to Source/SwiftLintFramework/RulesFilter.swift index ad56eb29b4..8a5d1d250e 100644 --- a/Source/swiftlint/Helpers/RulesFilter.swift +++ b/Source/SwiftLintFramework/RulesFilter.swift @@ -1,30 +1,32 @@ -import SwiftLintFramework +package final class RulesFilter { + package struct ExcludingOptions: OptionSet { + package let rawValue: Int -final class RulesFilter { - struct ExcludingOptions: OptionSet { - let rawValue: Int + package init(rawValue: Int) { + self.rawValue = rawValue + } - static let enabled = Self(rawValue: 1 << 0) - static let disabled = Self(rawValue: 1 << 1) - static let uncorrectable = Self(rawValue: 1 << 2) + package static let enabled = Self(rawValue: 1 << 0) + package static let disabled = Self(rawValue: 1 << 1) + package static let uncorrectable = Self(rawValue: 1 << 2) } private let allRules: RuleList private let enabledRules: [any Rule] - init(allRules: RuleList = RuleRegistry.shared.list, enabledRules: [any Rule]) { + package init(allRules: RuleList = RuleRegistry.shared.list, enabledRules: [any Rule]) { self.allRules = allRules self.enabledRules = enabledRules } - func getRules(excluding excludingOptions: ExcludingOptions) -> RuleList { + package func getRules(excluding excludingOptions: ExcludingOptions) -> RuleList { if excludingOptions.isEmpty { return allRules } let filtered: [any Rule.Type] = allRules.list.compactMap { ruleID, ruleType in let enabledRule = enabledRules.first { rule in - type(of: rule).description.identifier == ruleID + type(of: rule).identifier == ruleID } let isRuleEnabled = enabledRule != nil diff --git a/Source/swiftlint/Helpers/Signposts.swift b/Source/SwiftLintFramework/Signposts.swift similarity index 100% rename from Source/swiftlint/Helpers/Signposts.swift rename to Source/SwiftLintFramework/Signposts.swift diff --git a/Source/swiftlint/Helpers/SwiftLintError.swift b/Source/SwiftLintFramework/SwiftLintError.swift similarity index 66% rename from Source/swiftlint/Helpers/SwiftLintError.swift rename to Source/SwiftLintFramework/SwiftLintError.swift index cf17a123a4..da6ee8fd49 100644 --- a/Source/swiftlint/Helpers/SwiftLintError.swift +++ b/Source/SwiftLintFramework/SwiftLintError.swift @@ -1,9 +1,9 @@ import Foundation -enum SwiftLintError: LocalizedError { +package enum SwiftLintError: LocalizedError { case usageError(description: String) - var errorDescription: String? { + package var errorDescription: String? { switch self { case .usageError(let description): return description diff --git a/Source/swiftlint/Helpers/SwiftPMCompilationDB.swift b/Source/SwiftLintFramework/SwiftPMCompilationDB.swift similarity index 95% rename from Source/swiftlint/Helpers/SwiftPMCompilationDB.swift rename to Source/SwiftLintFramework/SwiftPMCompilationDB.swift index 6faa7c29be..d651290d48 100644 --- a/Source/swiftlint/Helpers/SwiftPMCompilationDB.swift +++ b/Source/SwiftLintFramework/SwiftPMCompilationDB.swift @@ -28,7 +28,7 @@ struct SwiftPMCompilationDB: Codable { static func parse(yaml: Data) throws -> [File: Arguments] { let decoder = YAMLDecoder() - let compilationDB: SwiftPMCompilationDB + let compilationDB: Self if ProcessInfo.processInfo.environment["TEST_SRCDIR"] != nil { // Running tests @@ -37,7 +37,7 @@ struct SwiftPMCompilationDB: Codable { let pathToReplace = Array(nodes.nodes.keys.filter({ node in node.hasSuffix(suffix) }))[0].dropLast(suffix.count - 1) - let stringFileContents = String(decoding: yaml, as: UTF8.self) + let stringFileContents = String(data: yaml, encoding: .utf8)! .replacingOccurrences(of: pathToReplace, with: "") compilationDB = try decoder.decode(Self.self, from: stringFileContents) } else { diff --git a/Source/SwiftLintFramework/UpdateChecker.swift b/Source/SwiftLintFramework/UpdateChecker.swift new file mode 100644 index 0000000000..ad8db0538c --- /dev/null +++ b/Source/SwiftLintFramework/UpdateChecker.swift @@ -0,0 +1,48 @@ +// swiftlint:disable file_header +// +// Adapted from periphery's UpdateChecker.swift +// +// https://github.com/peripheryapp/periphery +// +// Copyright (c) 2019 Ian Leitch +// Licensed under the MIT License +// +// See https://github.com/peripheryapp/periphery/blob/master/LICENSE.md for license information +// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +package enum UpdateChecker { + package static func checkForUpdates() async { + guard let url = URL(string: "https://api.github.com/repos/realm/SwiftLint/releases/latest"), + let data = try? await sendRequest(to: url), + let latestVersionNumber = parseVersionNumber(data) else { + print("Could not check latest SwiftLint version") + return + } + + let latestVersion = SwiftLintFramework.Version(value: latestVersionNumber) + if latestVersion > SwiftLintFramework.Version.current { + print("A new version of SwiftLint is available: \(latestVersionNumber)") + } else { + print("Your version of SwiftLint is up to date.") + } + } + + private static func parseVersionNumber(_ data: Data) -> String? { + guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable: Any] else { + return nil + } + return jsonObject["tag_name"] as? String + } + + private static func sendRequest(to url: URL) async throws -> Data { + var request = URLRequest(url: url) + request.setValue("SwiftLint", forHTTPHeaderField: "User-Agent") + request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept") + return try await URLSession.shared.data(for: request).0 + } +} diff --git a/Source/swiftlint/Commands/Analyze.swift b/Source/swiftlint/Commands/Analyze.swift index 9e41eea340..10471b1dda 100644 --- a/Source/swiftlint/Commands/Analyze.swift +++ b/Source/swiftlint/Commands/Analyze.swift @@ -7,30 +7,26 @@ extension SwiftLint { @OptionGroup var common: LintOrAnalyzeArguments - @Option(help: pathOptionDescription(for: .analyze)) - var path: String? @Flag(help: quietOptionDescription(for: .analyze)) var quiet = false @Option(help: "The path of the full xcodebuild log to use when running AnalyzerRules.") var compilerLogPath: String? @Option(help: "The path of a compilation database to use when running AnalyzerRules.") var compileCommands: String? + @Option( + parsing: .singleValue, + help: """ + Run only the specified rule, ignoring `only_rules`, `opt_in_rules` and `disabled_rules`. + Can be specified repeatedly to run multiple rules. + """ + ) + var onlyRule: [String] = [] @Argument(help: pathsArgumentDescription(for: .analyze)) var paths = [String]() func run() async throws { - let allPaths: [String] - if let path { - // TODO: [06/14/2024] Remove deprecation warning after ~2 years. - Issue.genericWarning( - "The --path option is deprecated. Pass the path(s) to analyze last to the swiftlint command." - ).print() - allPaths = [path] + paths - } else if !paths.isEmpty { - allPaths = paths - } else { - allPaths = [""] // Analyze files in current working directory if no paths were specified. - } + // Analyze files in current working directory if no paths were specified. + let allPaths = paths.isNotEmpty ? paths : [""] let options = LintOrAnalyzeOptions( mode: .analyze, paths: allPaths, @@ -41,19 +37,24 @@ extension SwiftLint { forceExclude: common.forceExclude, useExcludingByPrefix: common.useAlternativeExcluding, useScriptInputFiles: common.useScriptInputFiles, + useScriptInputFileLists: common.useScriptInputFileLists, benchmark: common.benchmark, reporter: common.reporter, + baseline: common.baseline, + writeBaseline: common.writeBaseline, + workingDirectory: common.workingDirectory, quiet: quiet, output: common.output, progress: common.progress, cachePath: nil, ignoreCache: true, enableAllRules: false, + onlyRule: onlyRule, autocorrect: common.fix, format: common.format, compilerLogPath: compilerLogPath, compileCommands: compileCommands, - inProcessSourcekit: common.inProcessSourcekit + checkForUpdates: common.checkForUpdates ) try await LintOrAnalyzeCommand.run(options) diff --git a/Source/swiftlint/Commands/Baseline.swift b/Source/swiftlint/Commands/Baseline.swift new file mode 100644 index 0000000000..a58b889c87 --- /dev/null +++ b/Source/swiftlint/Commands/Baseline.swift @@ -0,0 +1,88 @@ +import ArgumentParser +import Foundation +import SwiftLintFramework + +extension SwiftLint { + struct Baseline: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Operations on existing baselines", + subcommands: [Report.self, Compare.self], + defaultSubcommand: Report.self + ) + } + + private struct BaselineOptions: ParsableArguments { + @Argument(help: "The path to the baseline file.") + var baseline: String + } + + private struct ReportingOptions: ParsableArguments { + @Option( + help: """ + The reporter used to report violations. The 'summary' reporter can be useful to \ + provide an overview. + """ + ) + var reporter: String? + @Option(help: "The file where violations should be saved. Prints to stdout by default.") + var output: String? + } + + private struct Report: ParsableCommand { + static let configuration = CommandConfiguration(abstract: "Reports the violations in a baseline.") + + @OptionGroup + var options: BaselineOptions + @OptionGroup + var reportingOptions: ReportingOptions + + func run() throws { + let savedBaseline = try SwiftLintCore.Baseline(fromPath: options.baseline) + try report(savedBaseline.violations, using: reportingOptions.reporter, to: reportingOptions.output) + ExitHelper.successfullyExit() + } + } + + private struct Compare: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Reports the violations that are present in another baseline " + + "but not in the original baseline." + ) + + @OptionGroup + var options: BaselineOptions + @Option( + help: """ + The path to a second baseline to compare against the baseline. Violations in \ + the second baseline that are not present in the original baseline will be reported. + """ + ) + var otherBaseline: String + @OptionGroup + var reportingOptions: ReportingOptions + + func run() throws { + let baseline = try SwiftLintCore.Baseline(fromPath: options.baseline) + let otherBaseline = try SwiftLintCore.Baseline(fromPath: otherBaseline) + try report(baseline.compare(otherBaseline), using: reportingOptions.reporter, to: reportingOptions.output) + ExitHelper.successfullyExit() + } + } +} + +private func report(_ violations: [StyleViolation], using reporterIdentifier: String?, to output: String?) throws { + let reporter = reporterFrom(identifier: reporterIdentifier) + let report = reporter.generateReport(violations) + if report.isNotEmpty { + if let output { + let data = Data((report + "\n").utf8) + do { + try data.write(to: URL(fileURLWithPath: output)) + } catch { + Issue.fileNotWritable(path: output).print() + } + } else { + queuedPrint(report) + } + } +} diff --git a/Source/swiftlint/Commands/Common/RulesFilter.ExcludingOptions+RulesFilterOptions.swift b/Source/swiftlint/Commands/Common/RulesFilter.ExcludingOptions+RulesFilterOptions.swift deleted file mode 100644 index 192bf6895a..0000000000 --- a/Source/swiftlint/Commands/Common/RulesFilter.ExcludingOptions+RulesFilterOptions.swift +++ /dev/null @@ -1,20 +0,0 @@ -extension RulesFilter.ExcludingOptions { - static func excludingOptions(byCommandLineOptions rulesFilterOptions: RulesFilterOptions) -> Self { - var excludingOptions: Self = [] - - switch rulesFilterOptions.ruleEnablement { - case .enabled: - excludingOptions.insert(.disabled) - case .disabled: - excludingOptions.insert(.enabled) - case .none: - break - } - - if rulesFilterOptions.correctable { - excludingOptions.insert(.uncorrectable) - } - - return excludingOptions - } -} diff --git a/Source/swiftlint/Commands/Common/RulesFilterOptions.swift b/Source/swiftlint/Commands/Common/RulesFilterOptions.swift deleted file mode 100644 index 0a409fb630..0000000000 --- a/Source/swiftlint/Commands/Common/RulesFilterOptions.swift +++ /dev/null @@ -1,20 +0,0 @@ -import ArgumentParser - -enum RuleEnablementOptions: String, EnumerableFlag { - case enabled, disabled - - static func name(for value: RuleEnablementOptions) -> NameSpecification { - return .shortAndLong - } - - static func help(for value: RuleEnablementOptions) -> ArgumentHelp? { - return "Only show \(value.rawValue) rules" - } -} - -struct RulesFilterOptions: ParsableArguments { - @Flag(exclusivity: .exclusive) - var ruleEnablement: RuleEnablementOptions? - @Flag(name: .shortAndLong, help: "Only display correctable rules") - var correctable = false -} diff --git a/Source/swiftlint/Commands/GenerateDocs.swift b/Source/swiftlint/Commands/GenerateDocs.swift index fc448fc594..b62e6ae8f9 100644 --- a/Source/swiftlint/Commands/GenerateDocs.swift +++ b/Source/swiftlint/Commands/GenerateDocs.swift @@ -18,7 +18,7 @@ extension SwiftLint { func run() throws { let configuration = Configuration(configurationFiles: [config].compactMap({ $0 })) let rulesFilter = RulesFilter(enabledRules: configuration.rules) - let rules = rulesFilter.getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions)) + let rules = rulesFilter.getRules(excluding: rulesFilterOptions.excludingOptions) try RuleListDocumentation(rules) .write(to: URL(fileURLWithPath: path, isDirectory: true)) diff --git a/Source/swiftlint/Commands/Lint.swift b/Source/swiftlint/Commands/Lint.swift index b6f39fd33e..c1edb053b7 100644 --- a/Source/swiftlint/Commands/Lint.swift +++ b/Source/swiftlint/Commands/Lint.swift @@ -7,8 +7,6 @@ extension SwiftLint { @OptionGroup var common: LintOrAnalyzeArguments - @Option(help: pathOptionDescription(for: .lint)) - var path: String? @Flag(help: "Lint standard input.") var useSTDIN = false @Flag(help: quietOptionDescription(for: .lint)) @@ -21,24 +19,26 @@ extension SwiftLint { var noCache = false @Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.") var enableAllRules = false + @Option( + parsing: .singleValue, + help: """ + Run only the specified rule, ignoring `only_rules`, `opt_in_rules` and `disabled_rules`. + Can be specified repeatedly to run multiple rules. + """ + ) + var onlyRule: [String] = [] @Argument(help: pathsArgumentDescription(for: .lint)) var paths = [String]() func run() async throws { Issue.printDeprecationWarnings = !silenceDeprecationWarnings - let allPaths: [String] - if let path { - // TODO: [06/14/2024] Remove deprecation warning after ~2 years. - Issue.genericWarning( - "The --path option is deprecated. Pass the path(s) to lint last to the swiftlint command." - ).print() - allPaths = [path] + paths - } else if !paths.isEmpty { - allPaths = paths - } else { - allPaths = [""] // Lint files in current working directory if no paths were specified. + if common.fix, let leniency = common.leniency { + Issue.genericWarning("The option --\(leniency) has no effect together with --fix.").print() } + + // Lint files in current working directory if no paths were specified. + let allPaths = paths.isNotEmpty ? paths : [""] let options = LintOrAnalyzeOptions( mode: .lint, paths: allPaths, @@ -49,19 +49,24 @@ extension SwiftLint { forceExclude: common.forceExclude, useExcludingByPrefix: common.useAlternativeExcluding, useScriptInputFiles: common.useScriptInputFiles, + useScriptInputFileLists: common.useScriptInputFileLists, benchmark: common.benchmark, reporter: common.reporter, + baseline: common.baseline, + writeBaseline: common.writeBaseline, + workingDirectory: common.workingDirectory, quiet: quiet, output: common.output, progress: common.progress, cachePath: cachePath, ignoreCache: noCache, enableAllRules: enableAllRules, + onlyRule: onlyRule, autocorrect: common.fix, format: common.format, compilerLogPath: nil, compileCommands: nil, - inProcessSourcekit: common.inProcessSourcekit + checkForUpdates: common.checkForUpdates ) try await LintOrAnalyzeCommand.run(options) } diff --git a/Source/swiftlint/Commands/Reporters.swift b/Source/swiftlint/Commands/Reporters.swift index 0eb9aa3b0b..345a93a0a7 100644 --- a/Source/swiftlint/Commands/Reporters.swift +++ b/Source/swiftlint/Commands/Reporters.swift @@ -19,13 +19,13 @@ private extension TextTable { init(reporters: [any Reporter.Type]) { let columns = [ TextTableColumn(header: "identifier"), - TextTableColumn(header: "description") + TextTableColumn(header: "description"), ] self.init(columns: columns) for reporter in reporters { addRow(values: [ reporter.identifier, - reporter.description + reporter.description, ]) } } diff --git a/Source/swiftlint/Commands/Rules.swift b/Source/swiftlint/Commands/Rules.swift index 52cff62992..0ed3a966a7 100644 --- a/Source/swiftlint/Commands/Rules.swift +++ b/Source/swiftlint/Commands/Rules.swift @@ -40,7 +40,7 @@ extension SwiftLint { return } let rules = RulesFilter(enabledRules: configuration.rules) - .getRules(excluding: .excludingOptions(byCommandLineOptions: rulesFilterOptions)) + .getRules(excluding: rulesFilterOptions.excludingOptions) .list .sorted { $0.0 < $1.0 } if configOnly { @@ -70,10 +70,11 @@ extension SwiftLint { } print("\(description.consoleDescription)") - if rule.configurationDescription.hasContent { + let configDescription = rule.createConfigurationDescription() + if configDescription.hasContent { print("\nConfiguration (YAML):\n") print(" \(description.identifier):") - print(rule.configurationDescription.yaml().indent(by: 4)) + print(configDescription.yaml().indent(by: 4)) } guard description.triggeringExamples.isNotEmpty else { return } @@ -85,13 +86,15 @@ extension SwiftLint { } private func printConfig(for rule: any Rule) { - if rule.configurationDescription.hasContent { + let configDescription = rule.createConfigurationDescription() + if configDescription.hasContent { print("\(type(of: rule).identifier):") - print(rule.configurationDescription.yaml().indent(by: 2)) + print(configDescription.yaml().indent(by: 2)) } } - private func createInstance(of ruleType: any Rule.Type, using config: Configuration, + private func createInstance(of ruleType: any Rule.Type, + using config: Configuration, configure: Bool) -> any Rule { configure ? config.configuredRule(forID: ruleType.identifier) ?? ruleType.init() @@ -112,7 +115,7 @@ private extension TextTable { TextTableColumn(header: "kind"), TextTableColumn(header: "analyzer"), TextTableColumn(header: "uses sourcekit"), - TextTableColumn(header: "configuration") + TextTableColumn(header: "configuration"), ] self.init(columns: columns) func truncate(_ string: String) -> String { @@ -141,7 +144,7 @@ private extension TextTable { ruleType.description.kind.rawValue, (rule is any AnalyzerRule) ? "yes" : "no", (rule is any SourceKitFreeRule) ? "no" : "yes", - truncate((defaultConfig ? rule : configuredRule ?? rule).configurationDescription.oneLiner()) + truncate((defaultConfig ? rule : configuredRule ?? rule).createConfigurationDescription().oneLiner()), ]) } } diff --git a/Source/swiftlint/Commands/SwiftLint.swift b/Source/swiftlint/Commands/SwiftLint.swift index dd86138e7e..c0b0772533 100644 --- a/Source/swiftlint/Commands/SwiftLint.swift +++ b/Source/swiftlint/Commands/SwiftLint.swift @@ -10,7 +10,11 @@ import SwiftLintFramework struct SwiftLint: AsyncParsableCommand { static let configuration: CommandConfiguration = { if let directory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] { - FileManager.default.changeCurrentDirectoryPath(directory) + if !FileManager.default.changeCurrentDirectoryPath(directory) { + queuedFatalError(""" + Could not change current directory to \(directory) specified by BUILD_WORKSPACE_DIRECTORY. + """) + } } RuleRegistry.registerAllRulesOnce() @@ -24,9 +28,10 @@ struct SwiftLint: AsyncParsableCommand { Docs.self, GenerateDocs.self, Lint.self, + Baseline.self, Reporters.self, Rules.self, - Version.self + Version.self, ], defaultSubcommand: Lint.self ) diff --git a/Source/swiftlint/Commands/Version.swift b/Source/swiftlint/Commands/Version.swift index 8cbcb082fa..0aa32567d1 100644 --- a/Source/swiftlint/Commands/Version.swift +++ b/Source/swiftlint/Commands/Version.swift @@ -2,21 +2,26 @@ import ArgumentParser import SwiftLintFramework extension SwiftLint { - struct Version: ParsableCommand { - @Flag(help: "Display full version info") + struct Version: AsyncParsableCommand { + @Flag(help: "Display full version info.") var verbose = false + @Flag(help: "Check whether a later version of SwiftLint is available after processing all files.") + var checkForUpdates = false static let configuration = CommandConfiguration(abstract: "Display the current version of SwiftLint") static var value: String { SwiftLintFramework.Version.current.value } - func run() throws { + func run() async { if verbose, let buildID = ExecutableInfo.buildID { print("Version:", Self.value) print("Build ID:", buildID) } else { print(Self.value) } + if checkForUpdates { + await UpdateChecker.checkForUpdates() + } ExitHelper.successfullyExit() } } diff --git a/Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift b/Source/swiftlint/Common/LintOrAnalyzeArguments.swift similarity index 76% rename from Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift rename to Source/swiftlint/Common/LintOrAnalyzeArguments.swift index e8e039d983..2e545aa7ae 100644 --- a/Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift +++ b/Source/swiftlint/Common/LintOrAnalyzeArguments.swift @@ -1,9 +1,10 @@ import ArgumentParser +import SwiftLintFramework enum LeniencyOptions: String, EnumerableFlag { case strict, lenient - static func help(for value: LeniencyOptions) -> ArgumentHelp? { + static func help(for value: Self) -> ArgumentHelp? { switch value { case .strict: return "Upgrades warnings to serious violations (errors)." @@ -30,6 +31,8 @@ struct LintOrAnalyzeArguments: ParsableArguments { var useAlternativeExcluding = false @Flag(help: "Read SCRIPT_INPUT_FILE* environment variables as files.") var useScriptInputFiles = false + @Flag(help: "Read SCRIPT_INPUT_FILE_LIST* environment variables as file lists.") + var useScriptInputFileLists = false @Flag(exclusivity: .exclusive) var leniency: LeniencyOptions? @Flag(help: "Exclude files in config `excluded` even if their paths are explicitly specified.") @@ -38,12 +41,18 @@ struct LintOrAnalyzeArguments: ParsableArguments { var benchmark = false @Option(help: "The reporter used to log errors and warnings.") var reporter: String? - @Flag(help: "Use the in-process version of SourceKit.") - var inProcessSourcekit = false + @Option(help: "The path to a baseline file, which will be used to filter out detected violations.") + var baseline: String? + @Option(help: "The path to save detected violations to as a new baseline.") + var writeBaseline: String? + @Option(help: "The working directory to use when running SwiftLint.") + var workingDirectory: String? @Option(help: "The file where violations should be saved. Prints to stdout by default.") var output: String? @Flag(help: "Show a live-updating progress bar instead of each file being processed.") var progress = false + @Flag(help: "Check whether a later version of SwiftLint is available after processing all files.") + var checkForUpdates = false } // MARK: - Common Argument Help @@ -51,10 +60,6 @@ struct LintOrAnalyzeArguments: ParsableArguments { // It'd be great to be able to parameterize an `@OptionGroup` so we could move these options into // `LintOrAnalyzeArguments`. -func pathOptionDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp { - ArgumentHelp(visibility: .hidden) -} - func pathsArgumentDescription(for mode: LintOrAnalyzeMode) -> ArgumentHelp { "List of paths to the files or directories to \(mode.imperative)." } diff --git a/Source/swiftlint/Common/RulesFilterOptions.swift b/Source/swiftlint/Common/RulesFilterOptions.swift new file mode 100644 index 0000000000..456bfe44d5 --- /dev/null +++ b/Source/swiftlint/Common/RulesFilterOptions.swift @@ -0,0 +1,40 @@ +import ArgumentParser +import SwiftLintFramework + +enum RuleEnablementOptions: String, EnumerableFlag { + case enabled, disabled + + static func name(for _: Self) -> NameSpecification { + .shortAndLong + } + + static func help(for value: Self) -> ArgumentHelp? { + "Only show \(value.rawValue) rules" + } +} + +struct RulesFilterOptions: ParsableArguments { + @Flag(exclusivity: .exclusive) + var ruleEnablement: RuleEnablementOptions? + @Flag(name: .shortAndLong, help: "Only display correctable rules") + var correctable = false + + var excludingOptions: RulesFilter.ExcludingOptions { + var excludingOptions: RulesFilter.ExcludingOptions = [] + + switch ruleEnablement { + case .enabled: + excludingOptions.insert(.disabled) + case .disabled: + excludingOptions.insert(.enabled) + case .none: + break + } + + if correctable { + excludingOptions.insert(.uncorrectable) + } + + return excludingOptions + } +} diff --git a/Tests/BUILD b/Tests/BUILD index 9da3ceebdc..2c846a7625 100644 --- a/Tests/BUILD +++ b/Tests/BUILD @@ -2,7 +2,18 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library", "swift_test") exports_files(["BUILD"]) -copts = ["-enable-upcoming-feature", "ExistentialAny"] +copts = [ + "-enable-upcoming-feature", + "ExistentialAny", + "-enable-upcoming-feature", + "ConciseMagicFile", + "-enable-upcoming-feature", + "ImportObjcForwardDeclarations", + "-enable-upcoming-feature", + "ForwardTrailingClosures", + "-enable-upcoming-feature", + "ImplicitOpenExistentials", +] # CLITests @@ -13,7 +24,7 @@ swift_library( module_name = "CLITests", package_name = "SwiftLint", deps = [ - "//:swiftlint.library", + "//:SwiftLintFramework", ], copts = copts, ) @@ -141,7 +152,10 @@ swift_library( swift_test( name = "IntegrationTests", - data = ["//:LintInputs"], + data = [ + "//:LintInputs", + "IntegrationTests/default_rule_configurations.yml" + ], visibility = ["//visibility:public"], deps = [":IntegrationTests.library"], ) diff --git a/Tests/CLITests/RulesFilterTests.swift b/Tests/CLITests/RulesFilterTests.swift index fb2076601e..8979db66c1 100644 --- a/Tests/CLITests/RulesFilterTests.swift +++ b/Tests/CLITests/RulesFilterTests.swift @@ -1,4 +1,3 @@ -@testable import swiftlint import SwiftLintFramework import XCTest @@ -8,12 +7,12 @@ final class RulesFilterTests: XCTestCase { rules: [ RuleMock1.self, RuleMock2.self, - CorrectableRuleMock.self + CorrectableRuleMock.self, ] ) let enabledRules: [any Rule] = [ RuleMock1(), - CorrectableRuleMock() + CorrectableRuleMock(), ] let rulesFilter = RulesFilter( allRules: allRules, @@ -24,7 +23,7 @@ final class RulesFilterTests: XCTestCase { XCTAssertEqual( Set(filteredRules.list.keys), - Set([RuleMock2.description.identifier]) + Set([RuleMock2.identifier]) ) } @@ -33,12 +32,12 @@ final class RulesFilterTests: XCTestCase { rules: [ RuleMock1.self, RuleMock2.self, - CorrectableRuleMock.self + CorrectableRuleMock.self, ] ) let enabledRules: [any Rule] = [ RuleMock1(), - CorrectableRuleMock() + CorrectableRuleMock(), ] let rulesFilter = RulesFilter( allRules: allRules, @@ -49,7 +48,7 @@ final class RulesFilterTests: XCTestCase { XCTAssertEqual( Set(filteredRules.list.keys), - Set([RuleMock1.description.identifier, CorrectableRuleMock.description.identifier]) + Set([RuleMock1.identifier, CorrectableRuleMock.identifier]) ) } @@ -58,12 +57,12 @@ final class RulesFilterTests: XCTestCase { rules: [ RuleMock1.self, RuleMock2.self, - CorrectableRuleMock.self + CorrectableRuleMock.self, ] ) let enabledRules: [any Rule] = [ RuleMock1(), - CorrectableRuleMock() + CorrectableRuleMock(), ] let rulesFilter = RulesFilter( allRules: allRules, @@ -74,7 +73,7 @@ final class RulesFilterTests: XCTestCase { XCTAssertEqual( Set(filteredRules.list.keys), - Set([CorrectableRuleMock.description.identifier]) + Set([CorrectableRuleMock.identifier]) ) } @@ -83,12 +82,12 @@ final class RulesFilterTests: XCTestCase { rules: [ RuleMock1.self, RuleMock2.self, - CorrectableRuleMock.self + CorrectableRuleMock.self, ] ) let enabledRules: [any Rule] = [ RuleMock1(), - CorrectableRuleMock() + CorrectableRuleMock(), ] let rulesFilter = RulesFilter( allRules: allRules, @@ -99,7 +98,7 @@ final class RulesFilterTests: XCTestCase { XCTAssertEqual( Set(filteredRules.list.keys), - Set([CorrectableRuleMock.description.identifier]) + Set([CorrectableRuleMock.identifier]) ) } @@ -108,7 +107,7 @@ final class RulesFilterTests: XCTestCase { rules: [ RuleMock1.self, RuleMock2.self, - CorrectableRuleMock.self + CorrectableRuleMock.self, ] ) let enabledRules: [any Rule] = [ @@ -123,7 +122,7 @@ final class RulesFilterTests: XCTestCase { XCTAssertEqual( Set(filteredRules.list.keys), - Set([CorrectableRuleMock.description.identifier]) + Set([CorrectableRuleMock.identifier]) ) } } @@ -137,11 +136,11 @@ private struct RuleMock1: Rule { static let description = RuleDescription(identifier: "RuleMock1", name: "", description: "", kind: .style) - init() {} - init(configuration: Any) throws { self.init() } + init() { /* conformance for test */ } + init(configuration _: Any) throws { self.init() } - func validate(file: SwiftLintFile) -> [StyleViolation] { - return [] + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] } } @@ -152,11 +151,11 @@ private struct RuleMock2: Rule { static let description = RuleDescription(identifier: "RuleMock2", name: "", description: "", kind: .style) - init() {} - init(configuration: Any) throws { self.init() } + init() { /* conformance for test */ } + init(configuration _: Any) throws { self.init() } - func validate(file: SwiftLintFile) -> [StyleViolation] { - return [] + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] } } @@ -167,14 +166,14 @@ private struct CorrectableRuleMock: CorrectableRule { static let description = RuleDescription(identifier: "CorrectableRuleMock", name: "", description: "", kind: .style) - init() {} - init(configuration: Any) throws { self.init() } + init() { /* conformance for test */ } + init(configuration _: Any) throws { self.init() } - func validate(file: SwiftLintFile) -> [StyleViolation] { - return [] + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] } - func correct(file: SwiftLintFile) -> [Correction] { + func correct(file _: SwiftLintFile) -> [Correction] { [] } } diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index adb372db2b..5fe0d3a19d 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -7,1447 +7,1495 @@ import SwiftLintTestHelpers // swiftlint:disable:next blanket_disable_command // swiftlint:disable file_length single_test_class type_name -class AccessibilityLabelForImageRuleGeneratedTests: SwiftLintTestCase { +final class AccessibilityLabelForImageRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(AccessibilityLabelForImageRule.description) } } -class AccessibilityTraitForButtonRuleGeneratedTests: SwiftLintTestCase { +final class AccessibilityTraitForButtonRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(AccessibilityTraitForButtonRule.description) } } -class AnonymousArgumentInMultilineClosureRuleGeneratedTests: SwiftLintTestCase { +final class AnonymousArgumentInMultilineClosureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(AnonymousArgumentInMultilineClosureRule.description) } } -class AnyObjectProtocolRuleGeneratedTests: SwiftLintTestCase { +final class ArrayInitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { - verifyRule(AnyObjectProtocolRule.description) + verifyRule(ArrayInitRule.description) } } -class ArrayInitRuleGeneratedTests: SwiftLintTestCase { +final class AsyncWithoutAwaitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { - verifyRule(ArrayInitRule.description) + verifyRule(AsyncWithoutAwaitRule.description) } } -class AttributesRuleGeneratedTests: SwiftLintTestCase { +final class AttributeNameSpacingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AttributeNameSpacingRule.description) + } +} + +final class AttributesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(AttributesRule.description) } } -class BalancedXCTestLifecycleRuleGeneratedTests: SwiftLintTestCase { +final class BalancedXCTestLifecycleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(BalancedXCTestLifecycleRule.description) } } -class BlanketDisableCommandRuleGeneratedTests: SwiftLintTestCase { +final class BlanketDisableCommandRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(BlanketDisableCommandRule.description) } } -class BlockBasedKVORuleGeneratedTests: SwiftLintTestCase { +final class BlockBasedKVORuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(BlockBasedKVORule.description) } } -class CaptureVariableRuleGeneratedTests: SwiftLintTestCase { +final class CaptureVariableRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CaptureVariableRule.description) } } -class ClassDelegateProtocolRuleGeneratedTests: SwiftLintTestCase { +final class ClassDelegateProtocolRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ClassDelegateProtocolRule.description) } } -class ClosingBraceRuleGeneratedTests: SwiftLintTestCase { +final class ClosingBraceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ClosingBraceRule.description) } } -class ClosureBodyLengthRuleGeneratedTests: SwiftLintTestCase { +final class ClosureBodyLengthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ClosureBodyLengthRule.description) } } -class ClosureEndIndentationRuleGeneratedTests: SwiftLintTestCase { +final class ClosureEndIndentationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ClosureEndIndentationRule.description) } } -class ClosureParameterPositionRuleGeneratedTests: SwiftLintTestCase { +final class ClosureParameterPositionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ClosureParameterPositionRule.description) } } -class ClosureSpacingRuleGeneratedTests: SwiftLintTestCase { +final class ClosureSpacingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ClosureSpacingRule.description) } } -class CollectionAlignmentRuleGeneratedTests: SwiftLintTestCase { +final class CollectionAlignmentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CollectionAlignmentRule.description) } } -class ColonRuleGeneratedTests: SwiftLintTestCase { +final class ColonRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ColonRule.description) } } -class CommaInheritanceRuleGeneratedTests: SwiftLintTestCase { +final class CommaInheritanceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CommaInheritanceRule.description) } } -class CommaRuleGeneratedTests: SwiftLintTestCase { +final class CommaRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CommaRule.description) } } -class CommentSpacingRuleGeneratedTests: SwiftLintTestCase { +final class CommentSpacingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CommentSpacingRule.description) } } -class CompilerProtocolInitRuleGeneratedTests: SwiftLintTestCase { +final class CompilerProtocolInitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CompilerProtocolInitRule.description) } } -class ComputedAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { +final class ComputedAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ComputedAccessorsOrderRule.description) } } -class ConditionalReturnsOnNewlineRuleGeneratedTests: SwiftLintTestCase { +final class ConditionalReturnsOnNewlineRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ConditionalReturnsOnNewlineRule.description) } } -class ContainsOverFilterCountRuleGeneratedTests: SwiftLintTestCase { +final class ContainsOverFilterCountRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ContainsOverFilterCountRule.description) } } -class ContainsOverFilterIsEmptyRuleGeneratedTests: SwiftLintTestCase { +final class ContainsOverFilterIsEmptyRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ContainsOverFilterIsEmptyRule.description) } } -class ContainsOverFirstNotNilRuleGeneratedTests: SwiftLintTestCase { +final class ContainsOverFirstNotNilRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ContainsOverFirstNotNilRule.description) } } -class ContainsOverRangeNilComparisonRuleGeneratedTests: SwiftLintTestCase { +final class ContainsOverRangeNilComparisonRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ContainsOverRangeNilComparisonRule.description) } } -class ControlStatementRuleGeneratedTests: SwiftLintTestCase { +final class ContrastedOpeningBraceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ContrastedOpeningBraceRule.description) + } +} + +final class ControlStatementRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ControlStatementRule.description) } } -class ConvenienceTypeRuleGeneratedTests: SwiftLintTestCase { +final class ConvenienceTypeRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ConvenienceTypeRule.description) } } -class CyclomaticComplexityRuleGeneratedTests: SwiftLintTestCase { +final class CyclomaticComplexityRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(CyclomaticComplexityRule.description) } } -class DeploymentTargetRuleGeneratedTests: SwiftLintTestCase { +final class DeploymentTargetRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DeploymentTargetRule.description) } } -class DirectReturnRuleGeneratedTests: SwiftLintTestCase { +final class DirectReturnRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DirectReturnRule.description) } } -class DiscardedNotificationCenterObserverRuleGeneratedTests: SwiftLintTestCase { +final class DiscardedNotificationCenterObserverRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscardedNotificationCenterObserverRule.description) } } -class DiscouragedAssertRuleGeneratedTests: SwiftLintTestCase { +final class DiscouragedAssertRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscouragedAssertRule.description) } } -class DiscouragedDirectInitRuleGeneratedTests: SwiftLintTestCase { +final class DiscouragedDirectInitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscouragedDirectInitRule.description) } } -class DiscouragedNoneNameRuleGeneratedTests: SwiftLintTestCase { +final class DiscouragedNoneNameRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscouragedNoneNameRule.description) } } -class DiscouragedObjectLiteralRuleGeneratedTests: SwiftLintTestCase { +final class DiscouragedObjectLiteralRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscouragedObjectLiteralRule.description) } } -class DiscouragedOptionalBooleanRuleGeneratedTests: SwiftLintTestCase { +final class DiscouragedOptionalBooleanRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscouragedOptionalBooleanRule.description) } } -class DiscouragedOptionalCollectionRuleGeneratedTests: SwiftLintTestCase { +final class DiscouragedOptionalCollectionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DiscouragedOptionalCollectionRule.description) } } -class DuplicateConditionsRuleGeneratedTests: SwiftLintTestCase { +final class DuplicateConditionsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DuplicateConditionsRule.description) } } -class DuplicateEnumCasesRuleGeneratedTests: SwiftLintTestCase { +final class DuplicateEnumCasesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DuplicateEnumCasesRule.description) } } -class DuplicateImportsRuleGeneratedTests: SwiftLintTestCase { +final class DuplicateImportsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DuplicateImportsRule.description) } } -class DuplicatedKeyInDictionaryLiteralRuleGeneratedTests: SwiftLintTestCase { +final class DuplicatedKeyInDictionaryLiteralRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DuplicatedKeyInDictionaryLiteralRule.description) } } -class DynamicInlineRuleGeneratedTests: SwiftLintTestCase { +final class DynamicInlineRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(DynamicInlineRule.description) } } -class EmptyCollectionLiteralRuleGeneratedTests: SwiftLintTestCase { +final class EmptyCollectionLiteralRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyCollectionLiteralRule.description) } } -class EmptyCountRuleGeneratedTests: SwiftLintTestCase { +final class EmptyCountRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyCountRule.description) } } -class EmptyEnumArgumentsRuleGeneratedTests: SwiftLintTestCase { +final class EmptyEnumArgumentsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyEnumArgumentsRule.description) } } -class EmptyParametersRuleGeneratedTests: SwiftLintTestCase { +final class EmptyParametersRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyParametersRule.description) } } -class EmptyParenthesesWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { +final class EmptyParenthesesWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyParenthesesWithTrailingClosureRule.description) } } -class EmptyStringRuleGeneratedTests: SwiftLintTestCase { +final class EmptyStringRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyStringRule.description) } } -class EmptyXCTestMethodRuleGeneratedTests: SwiftLintTestCase { +final class EmptyXCTestMethodRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EmptyXCTestMethodRule.description) } } -class EnumCaseAssociatedValuesLengthRuleGeneratedTests: SwiftLintTestCase { +final class EnumCaseAssociatedValuesLengthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(EnumCaseAssociatedValuesLengthRule.description) } } -class ExpiringTodoRuleGeneratedTests: SwiftLintTestCase { +final class ExpiringTodoRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExpiringTodoRule.description) } } -class ExplicitACLRuleGeneratedTests: SwiftLintTestCase { +final class ExplicitACLRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExplicitACLRule.description) } } -class ExplicitEnumRawValueRuleGeneratedTests: SwiftLintTestCase { +final class ExplicitEnumRawValueRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExplicitEnumRawValueRule.description) } } -class ExplicitInitRuleGeneratedTests: SwiftLintTestCase { +final class ExplicitInitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExplicitInitRule.description) } } -class ExplicitSelfRuleGeneratedTests: SwiftLintTestCase { +final class ExplicitSelfRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExplicitSelfRule.description) } } -class ExplicitTopLevelACLRuleGeneratedTests: SwiftLintTestCase { +final class ExplicitTopLevelACLRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExplicitTopLevelACLRule.description) } } -class ExplicitTypeInterfaceRuleGeneratedTests: SwiftLintTestCase { +final class ExplicitTypeInterfaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExplicitTypeInterfaceRule.description) } } -class ExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { +final class ExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ExtensionAccessModifierRule.description) } } -class FallthroughRuleGeneratedTests: SwiftLintTestCase { +final class FallthroughRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FallthroughRule.description) } } -class FatalErrorMessageRuleGeneratedTests: SwiftLintTestCase { +final class FatalErrorMessageRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FatalErrorMessageRule.description) } } -class FileHeaderRuleGeneratedTests: SwiftLintTestCase { +final class FileHeaderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FileHeaderRule.description) } } -class FileLengthRuleGeneratedTests: SwiftLintTestCase { +final class FileLengthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FileLengthRule.description) } } -class FileNameNoSpaceRuleGeneratedTests: SwiftLintTestCase { +final class FileNameNoSpaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FileNameNoSpaceRule.description) } } -class FileNameRuleGeneratedTests: SwiftLintTestCase { +final class FileNameRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FileNameRule.description) } } -class FileTypesOrderRuleGeneratedTests: SwiftLintTestCase { +final class FileTypesOrderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FileTypesOrderRule.description) } } -class FinalTestCaseRuleGeneratedTests: SwiftLintTestCase { +final class FinalTestCaseRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FinalTestCaseRule.description) } } -class FirstWhereRuleGeneratedTests: SwiftLintTestCase { +final class FirstWhereRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FirstWhereRule.description) } } -class FlatMapOverMapReduceRuleGeneratedTests: SwiftLintTestCase { +final class FlatMapOverMapReduceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FlatMapOverMapReduceRule.description) } } -class ForWhereRuleGeneratedTests: SwiftLintTestCase { +final class ForWhereRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ForWhereRule.description) } } -class ForceCastRuleGeneratedTests: SwiftLintTestCase { +final class ForceCastRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ForceCastRule.description) } } -class ForceTryRuleGeneratedTests: SwiftLintTestCase { +final class ForceTryRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ForceTryRule.description) } } -class ForceUnwrappingRuleGeneratedTests: SwiftLintTestCase { +final class ForceUnwrappingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ForceUnwrappingRule.description) } } -class FunctionBodyLengthRuleGeneratedTests: SwiftLintTestCase { +final class FunctionBodyLengthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FunctionBodyLengthRule.description) } } -class FunctionDefaultParameterAtEndRuleGeneratedTests: SwiftLintTestCase { +final class FunctionDefaultParameterAtEndRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FunctionDefaultParameterAtEndRule.description) } } -class FunctionParameterCountRuleGeneratedTests: SwiftLintTestCase { +final class FunctionParameterCountRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(FunctionParameterCountRule.description) } } -class GenericTypeNameRuleGeneratedTests: SwiftLintTestCase { +final class GenericTypeNameRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(GenericTypeNameRule.description) } } -class IBInspectableInExtensionRuleGeneratedTests: SwiftLintTestCase { +final class IBInspectableInExtensionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(IBInspectableInExtensionRule.description) } } -class IdenticalOperandsRuleGeneratedTests: SwiftLintTestCase { +final class IdenticalOperandsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(IdenticalOperandsRule.description) } } -class IdentifierNameRuleGeneratedTests: SwiftLintTestCase { +final class IdentifierNameRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(IdentifierNameRule.description) } } -class ImplicitGetterRuleGeneratedTests: SwiftLintTestCase { +final class ImplicitGetterRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ImplicitGetterRule.description) } } -class ImplicitReturnRuleGeneratedTests: SwiftLintTestCase { +final class ImplicitReturnRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ImplicitReturnRule.description) } } -class ImplicitlyUnwrappedOptionalRuleGeneratedTests: SwiftLintTestCase { +final class ImplicitlyUnwrappedOptionalRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ImplicitlyUnwrappedOptionalRule.description) } } -class InclusiveLanguageRuleGeneratedTests: SwiftLintTestCase { +final class InclusiveLanguageRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(InclusiveLanguageRule.description) } } -class IndentationWidthRuleGeneratedTests: SwiftLintTestCase { +final class IndentationWidthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(IndentationWidthRule.description) } } -class InertDeferRuleGeneratedTests: SwiftLintTestCase { +final class InertDeferRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(InertDeferRule.description) } } -class InvalidSwiftLintCommandRuleGeneratedTests: SwiftLintTestCase { +final class InvalidSwiftLintCommandRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(InvalidSwiftLintCommandRule.description) } } -class IsDisjointRuleGeneratedTests: SwiftLintTestCase { +final class IsDisjointRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(IsDisjointRule.description) } } -class JSONDecodingRuleGeneratedTests: SwiftLintTestCase { +final class JSONDecodingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(JSONDecodingRule.description) } } -class JoinedDefaultParameterRuleGeneratedTests: SwiftLintTestCase { +final class JoinedDefaultParameterRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(JoinedDefaultParameterRule.description) } } -class LargeTupleRuleGeneratedTests: SwiftLintTestCase { +final class LargeTupleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LargeTupleRule.description) } } -class LastWhereRuleGeneratedTests: SwiftLintTestCase { +final class LastWhereRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LastWhereRule.description) } } -class LeadingWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class LeadingWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LeadingWhitespaceRule.description) } } -class LegacyCGGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { +final class LegacyCGGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyCGGeometryFunctionsRule.description) } } -class LegacyConstantRuleGeneratedTests: SwiftLintTestCase { +final class LegacyConstantRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyConstantRule.description) } } -class LegacyConstructorRuleGeneratedTests: SwiftLintTestCase { +final class LegacyConstructorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyConstructorRule.description) } } -class LegacyHashingRuleGeneratedTests: SwiftLintTestCase { +final class LegacyHashingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyHashingRule.description) } } -class LegacyMultipleRuleGeneratedTests: SwiftLintTestCase { +final class LegacyMultipleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyMultipleRule.description) } } -class LegacyNSGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { +final class LegacyNSGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyNSGeometryFunctionsRule.description) } } -class LegacyObjcTypeRuleGeneratedTests: SwiftLintTestCase { +final class LegacyObjcTypeRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyObjcTypeRule.description) } } -class LegacyRandomRuleGeneratedTests: SwiftLintTestCase { +final class LegacyRandomRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LegacyRandomRule.description) } } -class LetVarWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class LetVarWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LetVarWhitespaceRule.description) } } -class LineLengthRuleGeneratedTests: SwiftLintTestCase { +final class LineLengthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LineLengthRule.description) } } -class LiteralExpressionEndIndentationRuleGeneratedTests: SwiftLintTestCase { +final class LiteralExpressionEndIndentationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LiteralExpressionEndIndentationRule.description) } } -class LocalDocCommentRuleGeneratedTests: SwiftLintTestCase { +final class LocalDocCommentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LocalDocCommentRule.description) } } -class LocaleOverrideRuleGeneratedTests: SwiftLintTestCase { +final class LocaleOverrideRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LocaleOverrideRule.description) } } -class LowerACLThanParentRuleGeneratedTests: SwiftLintTestCase { +final class LowerACLThanParentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(LowerACLThanParentRule.description) } } -class MarkRuleGeneratedTests: SwiftLintTestCase { +final class MarkRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MarkRule.description) } } -class MissingDocsRuleGeneratedTests: SwiftLintTestCase { +final class MissingDocsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MissingDocsRule.description) } } -class ModifierOrderRuleGeneratedTests: SwiftLintTestCase { +final class ModifierOrderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ModifierOrderRule.description) } } -class MultilineArgumentsBracketsRuleGeneratedTests: SwiftLintTestCase { +final class MultilineArgumentsBracketsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultilineArgumentsBracketsRule.description) } } -class MultilineArgumentsRuleGeneratedTests: SwiftLintTestCase { +final class MultilineArgumentsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultilineArgumentsRule.description) } } -class MultilineFunctionChainsRuleGeneratedTests: SwiftLintTestCase { +final class MultilineFunctionChainsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultilineFunctionChainsRule.description) } } -class MultilineLiteralBracketsRuleGeneratedTests: SwiftLintTestCase { +final class MultilineLiteralBracketsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultilineLiteralBracketsRule.description) } } -class MultilineParametersBracketsRuleGeneratedTests: SwiftLintTestCase { +final class MultilineParametersBracketsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultilineParametersBracketsRule.description) } } -class MultilineParametersRuleGeneratedTests: SwiftLintTestCase { +final class MultilineParametersRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultilineParametersRule.description) } } -class MultipleClosuresWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { +final class MultipleClosuresWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(MultipleClosuresWithTrailingClosureRule.description) } } -class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase { +final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NSLocalizedStringKeyRule.description) } } -class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase { +final class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NSLocalizedStringRequireBundleRule.description) } } -class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase { +final class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NSNumberInitAsFunctionReferenceRule.description) } } -class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase { +final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NSObjectPreferIsEqualRule.description) } } -class NavigationTitleRuleGeneratedTests: SwiftLintTestCase { +final class NavigationTitleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NavigationTitleRule.description) } } -class NestingRuleGeneratedTests: SwiftLintTestCase { +final class NestingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NestingRule.description) } } -class NimbleOperatorRuleGeneratedTests: SwiftLintTestCase { +final class NimbleOperatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NimbleOperatorRule.description) } } -class NoExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { +final class NoEmptyBlockRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoEmptyBlockRule.description) + } +} + +final class NoExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NoExtensionAccessModifierRule.description) } } -class NoFallthroughOnlyRuleGeneratedTests: SwiftLintTestCase { +final class NoFallthroughOnlyRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NoFallthroughOnlyRule.description) } } -class NoGroupingExtensionRuleGeneratedTests: SwiftLintTestCase { +final class NoGroupingExtensionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NoGroupingExtensionRule.description) } } -class NoMagicNumbersRuleGeneratedTests: SwiftLintTestCase { +final class NoMagicNumbersRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NoMagicNumbersRule.description) } } -class NoSpaceInMethodCallRuleGeneratedTests: SwiftLintTestCase { +final class NoSpaceInMethodCallRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NoSpaceInMethodCallRule.description) } } -class NonOptionalStringDataConversionRuleGeneratedTests: SwiftLintTestCase { +final class NonOptionalStringDataConversionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NonOptionalStringDataConversionRule.description) } } -class NonOverridableClassDeclarationRuleGeneratedTests: SwiftLintTestCase { +final class NonOverridableClassDeclarationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NonOverridableClassDeclarationRule.description) } } -class NotificationCenterDetachmentRuleGeneratedTests: SwiftLintTestCase { +final class NotificationCenterDetachmentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NotificationCenterDetachmentRule.description) } } -class NumberSeparatorRuleGeneratedTests: SwiftLintTestCase { +final class NumberSeparatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NumberSeparatorRule.description) } } -class ObjectLiteralRuleGeneratedTests: SwiftLintTestCase { +final class ObjectLiteralRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ObjectLiteralRule.description) } } -class OneDelarationPerFileRuleGeneratedTests: SwiftLintTestCase { +final class OneDeclarationPerFileRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { - verifyRule(OneDelarationPerFileRule.description) + verifyRule(OneDeclarationPerFileRule.description) } } -class OpeningBraceRuleGeneratedTests: SwiftLintTestCase { +final class OpeningBraceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OpeningBraceRule.description) } } -class OperatorFunctionWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class OperatorFunctionWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OperatorFunctionWhitespaceRule.description) } } -class OperatorUsageWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class OperatorUsageWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OperatorUsageWhitespaceRule.description) } } -class OptionalEnumCaseMatchingRuleGeneratedTests: SwiftLintTestCase { +final class OptionalDataStringConversionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OptionalDataStringConversionRule.description) + } +} + +final class OptionalEnumCaseMatchingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OptionalEnumCaseMatchingRule.description) } } -class OrphanedDocCommentRuleGeneratedTests: SwiftLintTestCase { +final class OrphanedDocCommentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OrphanedDocCommentRule.description) } } -class OverriddenSuperCallRuleGeneratedTests: SwiftLintTestCase { +final class OverriddenSuperCallRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OverriddenSuperCallRule.description) } } -class OverrideInExtensionRuleGeneratedTests: SwiftLintTestCase { +final class OverrideInExtensionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OverrideInExtensionRule.description) } } -class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase { +final class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PatternMatchingKeywordsRule.description) } } -class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase { +final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PeriodSpacingRule.description) } } -class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { +final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferKeyPathRule.description) + } +} + +final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferNimbleRule.description) } } -class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { +final class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferSelfInStaticReferencesRule.description) } } -class PreferSelfTypeOverTypeOfSelfRuleGeneratedTests: SwiftLintTestCase { +final class PreferSelfTypeOverTypeOfSelfRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferSelfTypeOverTypeOfSelfRule.description) } } -class PreferZeroOverExplicitInitRuleGeneratedTests: SwiftLintTestCase { +final class PreferTypeCheckingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferTypeCheckingRule.description) + } +} + +final class PreferZeroOverExplicitInitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferZeroOverExplicitInitRule.description) } } -class PrefixedTopLevelConstantRuleGeneratedTests: SwiftLintTestCase { +final class PrefixedTopLevelConstantRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrefixedTopLevelConstantRule.description) } } -class PrivateActionRuleGeneratedTests: SwiftLintTestCase { +final class PrivateActionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateActionRule.description) } } -class PrivateOutletRuleGeneratedTests: SwiftLintTestCase { +final class PrivateOutletRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateOutletRule.description) } } -class PrivateOverFilePrivateRuleGeneratedTests: SwiftLintTestCase { +final class PrivateOverFilePrivateRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateOverFilePrivateRule.description) } } -class PrivateSubjectRuleGeneratedTests: SwiftLintTestCase { +final class PrivateSubjectRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateSubjectRule.description) } } -class PrivateSwiftUIStatePropertyRuleGeneratedTests: SwiftLintTestCase { +final class PrivateSwiftUIStatePropertyRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateSwiftUIStatePropertyRule.description) } } -class PrivateUnitTestRuleGeneratedTests: SwiftLintTestCase { +final class PrivateUnitTestRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateUnitTestRule.description) } } -class ProhibitedInterfaceBuilderRuleGeneratedTests: SwiftLintTestCase { +final class ProhibitedInterfaceBuilderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ProhibitedInterfaceBuilderRule.description) } } -class ProhibitedSuperRuleGeneratedTests: SwiftLintTestCase { +final class ProhibitedSuperRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ProhibitedSuperRule.description) } } -class ProtocolPropertyAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { +final class ProtocolPropertyAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ProtocolPropertyAccessorsOrderRule.description) } } -class QuickDiscouragedCallRuleGeneratedTests: SwiftLintTestCase { +final class QuickDiscouragedCallRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(QuickDiscouragedCallRule.description) } } -class QuickDiscouragedFocusedTestRuleGeneratedTests: SwiftLintTestCase { +final class QuickDiscouragedFocusedTestRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(QuickDiscouragedFocusedTestRule.description) } } -class QuickDiscouragedPendingTestRuleGeneratedTests: SwiftLintTestCase { +final class QuickDiscouragedPendingTestRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(QuickDiscouragedPendingTestRule.description) } } -class RawValueForCamelCasedCodableEnumRuleGeneratedTests: SwiftLintTestCase { +final class RawValueForCamelCasedCodableEnumRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RawValueForCamelCasedCodableEnumRule.description) } } -class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase { +final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ReduceBooleanRule.description) } } -class ReduceIntoRuleGeneratedTests: SwiftLintTestCase { +final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ReduceIntoRule.description) } } -class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase { +final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantDiscardableLetRule.description) } } -class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase { +final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantNilCoalescingRule.description) } } -class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase { +final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantObjcAttributeRule.description) } } -class RedundantOptionalInitializationRuleGeneratedTests: SwiftLintTestCase { +final class RedundantOptionalInitializationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantOptionalInitializationRule.description) } } -class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { +final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantSelfInClosureRule.description) } } -class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { +final class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantSetAccessControlRule.description) } } -class RedundantStringEnumValueRuleGeneratedTests: SwiftLintTestCase { +final class RedundantStringEnumValueRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantStringEnumValueRule.description) } } -class RedundantTypeAnnotationRuleGeneratedTests: SwiftLintTestCase { +final class RedundantTypeAnnotationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantTypeAnnotationRule.description) } } -class RedundantVoidReturnRuleGeneratedTests: SwiftLintTestCase { +final class RedundantVoidReturnRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantVoidReturnRule.description) } } -class RequiredDeinitRuleGeneratedTests: SwiftLintTestCase { +final class RequiredDeinitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RequiredDeinitRule.description) } } -class RequiredEnumCaseRuleGeneratedTests: SwiftLintTestCase { +final class RequiredEnumCaseRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RequiredEnumCaseRule.description) } } -class ReturnArrowWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class ReturnArrowWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ReturnArrowWhitespaceRule.description) } } -class ReturnValueFromVoidFunctionRuleGeneratedTests: SwiftLintTestCase { +final class ReturnValueFromVoidFunctionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ReturnValueFromVoidFunctionRule.description) } } -class SelfBindingRuleGeneratedTests: SwiftLintTestCase { +final class SelfBindingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SelfBindingRule.description) } } -class SelfInPropertyInitializationRuleGeneratedTests: SwiftLintTestCase { +final class SelfInPropertyInitializationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SelfInPropertyInitializationRule.description) } } -class ShorthandArgumentRuleGeneratedTests: SwiftLintTestCase { +final class ShorthandArgumentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ShorthandArgumentRule.description) } } -class ShorthandOperatorRuleGeneratedTests: SwiftLintTestCase { +final class ShorthandOperatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ShorthandOperatorRule.description) } } -class ShorthandOptionalBindingRuleGeneratedTests: SwiftLintTestCase { +final class ShorthandOptionalBindingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ShorthandOptionalBindingRule.description) } } -class SingleTestClassRuleGeneratedTests: SwiftLintTestCase { +final class SingleTestClassRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SingleTestClassRule.description) } } -class SortedEnumCasesRuleGeneratedTests: SwiftLintTestCase { +final class SortedEnumCasesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SortedEnumCasesRule.description) } } -class SortedFirstLastRuleGeneratedTests: SwiftLintTestCase { +final class SortedFirstLastRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SortedFirstLastRule.description) } } -class SortedImportsRuleGeneratedTests: SwiftLintTestCase { +final class SortedImportsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SortedImportsRule.description) } } -class StatementPositionRuleGeneratedTests: SwiftLintTestCase { +final class StatementPositionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(StatementPositionRule.description) } } -class StaticOperatorRuleGeneratedTests: SwiftLintTestCase { +final class StaticOperatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(StaticOperatorRule.description) } } -class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase { +final class StaticOverFinalClassRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StaticOverFinalClassRule.description) + } +} + +final class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(StrictFilePrivateRule.description) } } -class StringLocalizationCorrectArgumentsRuleGeneratedTests: SwiftLintTestCase { +final class StringLocalizationCorrectArgumentsRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(StringLocalizationCorrectArgumentsRule.description) } } -class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase { +final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(StrongIBOutletRule.description) } } -class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase { +final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SuperfluousElseRule.description) } } -class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { +final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SwitchCaseAlignmentRule.description) } } -class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { +final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SwitchCaseOnNewlineRule.description) } } -class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { +final class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SyntacticSugarRule.description) } } -class TestCaseAccessibilityRuleGeneratedTests: SwiftLintTestCase { +final class TestCaseAccessibilityRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TestCaseAccessibilityRule.description) } } -class TextConcatenationRuleGeneratedTests: SwiftLintTestCase { +final class TextConcatenationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TextConcatenationRule.description) } } -class TextLocalizationRuleGeneratedTests: SwiftLintTestCase { +final class TextLocalizationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TextLocalizationRule.description) } } -class TodoRuleGeneratedTests: SwiftLintTestCase { +final class TodoRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TodoRule.description) } } -class ToggleBoolRuleGeneratedTests: SwiftLintTestCase { +final class ToggleBoolRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ToggleBoolRule.description) } } -class TrailingClosureRuleGeneratedTests: SwiftLintTestCase { +final class TrailingClosureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TrailingClosureRule.description) } } -class TrailingCommaRuleGeneratedTests: SwiftLintTestCase { +final class TrailingCommaRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TrailingCommaRule.description) } } -class TrailingNewlineRuleGeneratedTests: SwiftLintTestCase { +final class TrailingNewlineRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TrailingNewlineRule.description) } } -class TrailingSemicolonRuleGeneratedTests: SwiftLintTestCase { +final class TrailingSemicolonRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TrailingSemicolonRule.description) } } -class TrailingWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class TrailingWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TrailingWhitespaceRule.description) } } -class TypeBodyLengthRuleGeneratedTests: SwiftLintTestCase { +final class TypeBodyLengthRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TypeBodyLengthRule.description) } } -class TypeContentsOrderRuleGeneratedTests: SwiftLintTestCase { +final class TypeContentsOrderRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TypeContentsOrderRule.description) } } -class TypeNameRuleGeneratedTests: SwiftLintTestCase { +final class TypeNameRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TypeNameRule.description) } } -class TypesafeArrayInitRuleGeneratedTests: SwiftLintTestCase { +final class TypesafeArrayInitRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TypesafeArrayInitRule.description) } } -class UnavailableConditionRuleGeneratedTests: SwiftLintTestCase { +final class UnavailableConditionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnavailableConditionRule.description) } } -class UnavailableFunctionRuleGeneratedTests: SwiftLintTestCase { +final class UnavailableFunctionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnavailableFunctionRule.description) } } -class UnhandledThrowingTaskRuleGeneratedTests: SwiftLintTestCase { +final class UnhandledThrowingTaskRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnhandledThrowingTaskRule.description) } } -class UnneededBreakInSwitchRuleGeneratedTests: SwiftLintTestCase { +final class UnneededBreakInSwitchRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnneededBreakInSwitchRule.description) } } -class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase { +final class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnneededOverrideRule.description) } } -class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTestCase { +final class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnneededParenthesesInClosureArgumentRule.description) } } -class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase { +final class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnneededSynthesizedInitializerRule.description) } } -class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase { +final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnownedVariableCaptureRule.description) } } -class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase { +final class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UntypedErrorInCatchRule.description) } } -class UnusedCaptureListRuleGeneratedTests: SwiftLintTestCase { +final class UnusedCaptureListRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedCaptureListRule.description) } } -class UnusedClosureParameterRuleGeneratedTests: SwiftLintTestCase { +final class UnusedClosureParameterRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedClosureParameterRule.description) } } -class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { +final class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedControlFlowLabelRule.description) } } -class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { +final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedDeclarationRule.description) } } -class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { +final class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedEnumeratedRule.description) } } -class UnusedImportRuleGeneratedTests: SwiftLintTestCase { +final class UnusedImportRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedImportRule.description) } } -class UnusedOptionalBindingRuleGeneratedTests: SwiftLintTestCase { +final class UnusedOptionalBindingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedOptionalBindingRule.description) } } -class UnusedSetterValueRuleGeneratedTests: SwiftLintTestCase { +final class UnusedParameterRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedParameterRule.description) + } +} + +final class UnusedSetterValueRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedSetterValueRule.description) } } -class ValidIBInspectableRuleGeneratedTests: SwiftLintTestCase { +final class ValidIBInspectableRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(ValidIBInspectableRule.description) } } -class VerticalParameterAlignmentOnCallRuleGeneratedTests: SwiftLintTestCase { +final class VerticalParameterAlignmentOnCallRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VerticalParameterAlignmentOnCallRule.description) } } -class VerticalParameterAlignmentRuleGeneratedTests: SwiftLintTestCase { +final class VerticalParameterAlignmentRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VerticalParameterAlignmentRule.description) } } -class VerticalWhitespaceBetweenCasesRuleGeneratedTests: SwiftLintTestCase { +final class VerticalWhitespaceBetweenCasesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VerticalWhitespaceBetweenCasesRule.description) } } -class VerticalWhitespaceClosingBracesRuleGeneratedTests: SwiftLintTestCase { +final class VerticalWhitespaceClosingBracesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VerticalWhitespaceClosingBracesRule.description) } } -class VerticalWhitespaceOpeningBracesRuleGeneratedTests: SwiftLintTestCase { +final class VerticalWhitespaceOpeningBracesRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VerticalWhitespaceOpeningBracesRule.description) } } -class VerticalWhitespaceRuleGeneratedTests: SwiftLintTestCase { +final class VerticalWhitespaceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VerticalWhitespaceRule.description) } } -class VoidFunctionInTernaryConditionRuleGeneratedTests: SwiftLintTestCase { +final class VoidFunctionInTernaryConditionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VoidFunctionInTernaryConditionRule.description) } } -class VoidReturnRuleGeneratedTests: SwiftLintTestCase { +final class VoidReturnRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(VoidReturnRule.description) } } -class WeakDelegateRuleGeneratedTests: SwiftLintTestCase { +final class WeakDelegateRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(WeakDelegateRule.description) } } -class XCTFailMessageRuleGeneratedTests: SwiftLintTestCase { +final class XCTFailMessageRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(XCTFailMessageRule.description) } } -class XCTSpecificMatcherRuleGeneratedTests: SwiftLintTestCase { +final class XCTSpecificMatcherRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(XCTSpecificMatcherRule.description) } } -class YodaConditionRuleGeneratedTests: SwiftLintTestCase { +final class YodaConditionRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(YodaConditionRule.description) } diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index 8efc30b891..c4830a3ed1 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -5,7 +5,7 @@ import XCTest private let config: Configuration = { let bazelWorkspaceDirectory = ProcessInfo.processInfo.environment["BUILD_WORKSPACE_DIRECTORY"] - let rootProjectDirectory = bazelWorkspaceDirectory ?? #file.bridge() + let rootProjectDirectory = bazelWorkspaceDirectory ?? #filePath.bridge() .deletingLastPathComponent.bridge() .deletingLastPathComponent.bridge() .deletingLastPathComponent @@ -13,7 +13,7 @@ private let config: Configuration = { return Configuration(configurationFiles: [Configuration.defaultFileName]) }() -class IntegrationTests: SwiftLintTestCase { +final class IntegrationTests: SwiftLintTestCase { func testSwiftLintLints() { // This is as close as we're ever going to get to a self-hosting linter. let swiftFiles = config.lintableFiles( @@ -21,7 +21,7 @@ class IntegrationTests: SwiftLintTestCase { forceExclude: false, excludeBy: .paths(excludedPaths: config.excludedPaths())) XCTAssert( - swiftFiles.contains(where: { #file.bridge().absolutePathRepresentation() == $0.path }), + swiftFiles.contains(where: { #filePath.bridge().absolutePathRepresentation() == $0.path }), "current file should be included" ) @@ -53,60 +53,21 @@ class IntegrationTests: SwiftLintTestCase { } } - func testSimulateHomebrewTest() { - // Since this test uses the `swiftlint` binary built while building `SwiftLintPackageTests`, - // we run it only on macOS using SwiftPM. -#if os(macOS) && SWIFT_PACKAGE - let keyName = "SWIFTLINT_FRAMEWORK_TEST_ENABLE_SIMULATE_HOMEBREW_TEST" - guard ProcessInfo.processInfo.environment[keyName] != nil else { - print(""" - Skipping the opt-in test `\(#function)`. - Set the `\(keyName)` environment variable to test `\(#function)`. - """) - return - } - guard let swiftlintURL = swiftlintBuiltBySwiftPM(), - let (testSwiftURL, seatbeltURL) = prepareSandbox() else { - return - } - - defer { - try? FileManager.default.removeItem(at: testSwiftURL.deletingLastPathComponent()) - try? FileManager.default.removeItem(at: seatbeltURL) - } - - let swiftlintInSandboxArgs = ["sandbox-exec", "-f", seatbeltURL.path, "sh", "-c", - "SWIFTLINT_SWIFT_VERSION=5 \(swiftlintURL.path) --no-cache"] - let swiftlintResult = execute(swiftlintInSandboxArgs, in: testSwiftURL.deletingLastPathComponent()) - let statusWithoutCrash: Int32 = 0 - let stdoutWithoutCrash = """ - \(testSwiftURL.path):1:1: \ - warning: Trailing Newline Violation: Files should have a single trailing newline. (trailing_newline) - - """ - let stderrWithoutCrash = """ - Linting Swift files at paths \n\ - Linting 'Test.swift' (1/1) - Connection invalid - Most rules will be skipped because sourcekitd has failed. - Done linting! Found 1 violation, 0 serious in 1 file. - - """ - if #available(macOS 10.14.1, *) { - // Within a sandbox on macOS 10.14.1+, `swiftlint` crashes with "Test::Unit::AssertionFailedError" - // error in `libxpc.dylib` when calling `sourcekitd_send_request_sync`. - // - // Since Homebrew CI succeeded in bottling swiftlint 0.27.0 on release of macOS 10.14, - // `swiftlint` may not crash on macOS 10.14. But that is not confirmed. - XCTAssertNotEqual(swiftlintResult.status, statusWithoutCrash, "It is expected to crash.") - XCTAssertNotEqual(swiftlintResult.stdout, stdoutWithoutCrash) - XCTAssertNotEqual(swiftlintResult.stderr, stderrWithoutCrash) - } else { - XCTAssertEqual(swiftlintResult.status, statusWithoutCrash) - XCTAssertEqual(swiftlintResult.stdout, stdoutWithoutCrash) - XCTAssertEqual(swiftlintResult.stderr, stderrWithoutCrash) - } -#endif + func testDefaultConfigurations() { + let defaultConfig = Configuration(rulesMode: .allCommandLine).rules + .map { type(of: $0) } + .filter { $0.identifier != "custom_rules" } + .map { + """ + \($0.identifier): + \($0.init().createConfigurationDescription().yaml().indent(by: 2)) + """ + } + .joined(separator: "\n") + let referenceFile = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .appendingPathComponent("default_rule_configurations.yml") + XCTAssertEqual(defaultConfig + "\n", try String(contentsOf: referenceFile)) } } @@ -133,7 +94,7 @@ private struct StaticStringImitator { } var staticString: StaticString { - return unsafeBitCast(self, to: StaticString.self) + unsafeBitCast(self, to: StaticString.self) } } } @@ -143,118 +104,3 @@ private extension String { StaticStringImitator(string: self).withStaticString(closure) } } - -#if os(macOS) && SWIFT_PACKAGE - -private func execute(_ args: [String], - in directory: URL? = nil, - input: Data? = nil) -> (status: Int32, stdout: String, stderr: String) { - let process = Process() - process.launchPath = "/usr/bin/env" - process.arguments = args - if let directory { - process.currentDirectoryPath = directory.path - } - let stdoutPipe = Pipe(), stderrPipe = Pipe() - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - if let input { - let stdinPipe = Pipe() - process.standardInput = stdinPipe.fileHandleForReading - stdinPipe.fileHandleForWriting.write(input) - stdinPipe.fileHandleForWriting.closeFile() - } - let group = DispatchGroup(), queue = DispatchQueue.global() - var stdoutData: Data?, stderrData: Data? - process.launch() - queue.async(group: group) { stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() } - queue.async(group: group) { stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() } - process.waitUntilExit() - group.wait() - let stdout = stdoutData.map { String(decoding: $0, as: UTF8.self) } ?? "" - let stderr = stderrData.map { String(decoding: $0, as: UTF8.self) } ?? "" - return (process.terminationStatus, stdout, stderr) -} - -private func prepareSandbox() -> (testSwiftURL: URL, seatbeltURL: URL)? { - // Since `/private/tmp` is hard coded in `/usr/local/Homebrew/Library/Homebrew/sandbox.rb`, we use them. - // /private/tmp - // ├── AADA6B05-2E06-4E7F-BA48-8B3AF44415E3 - // │   └── Test.swift - // ├── AADA6B05-2E06-4E7F-BA48-8B3AF44415E3.sb - do { - // `/private/tmp` is standardized to `/tmp` that is symbolic link to `/private/tmp`. - let temporaryDirectoryURL = URL(fileURLWithPath: "/tmp", isDirectory: true) - .appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true) - - let seatbeltURL = temporaryDirectoryURL.appendingPathExtension("sb") - try sandboxProfile().write(to: seatbeltURL, atomically: true, encoding: .utf8) - - let testSwiftURL = temporaryDirectoryURL.appendingPathComponent("Test.swift") - try "import Foundation".write(to: testSwiftURL, atomically: true, encoding: .utf8) - return (testSwiftURL, seatbeltURL) - } catch { - XCTFail("\(error)") - return nil - } -} - -private func sandboxProfile() -> String { - let homeDirectory = NSHomeDirectory() - return """ - (version 1) - (debug deny) ; log all denied operations to /var/log/system.log - (allow file-write* (subpath "/private/tmp")) - (allow file-write* (subpath "/private/var/tmp")) - (allow file-write* (regex #"^/private/var/folders/[^/]+/[^/]+/[C,T]/")) - (allow file-write* (subpath "/private/tmp")) - (allow file-write* (subpath "\(homeDirectory)/Library/Caches/Homebrew")) - (allow file-write* (subpath "\(homeDirectory)/Library/Logs/Homebrew/swiftlint")) - (allow file-write* (subpath "\(homeDirectory)/Library/Developer")) - (allow file-write* (subpath "/usr/local/var/cache")) - (allow file-write* (subpath "/usr/local/var/homebrew/locks")) - (allow file-write* (subpath "/usr/local/var/log")) - (allow file-write* (subpath "/usr/local/var/run")) - (allow file-write* - (literal "/dev/ptmx") - (literal "/dev/dtracehelper") - (literal "/dev/null") - (literal "/dev/random") - (literal "/dev/zero") - (regex #"^/dev/fd/[0-9]+$") - (regex #"^/dev/ttys?[0-9]*$") - ) - (deny file-write*) ; deny all other file write operations - (allow process-exec - (literal "/bin/ps") - (with no-sandbox) - ) ; allow certain processes running without sandbox - (allow default) ; allow everything else - - """ -} - -private func swiftlintBuiltBySwiftPM() -> URL? { -#if DEBUG - let configuration = "debug" -#else - let configuration = "release" -#endif - let swiftBuildShowBinPathArgs = ["swift", "build", "--show-bin-path", "--configuration", configuration] - let binPathResult = execute(swiftBuildShowBinPathArgs) - guard binPathResult.status == 0 else { - let commandline = swiftBuildShowBinPathArgs.joined(separator: " ") - XCTFail("`\(commandline)` failed with status: \(binPathResult.status), error: \(binPathResult.stderr)") - return nil - } - let binPathString = binPathResult.stdout.components(separatedBy: CharacterSet.newlines).first! - let swiftlint = URL(fileURLWithPath: binPathString).appendingPathComponent("swiftlint") - guard FileManager.default.fileExists(atPath: swiftlint.path) else { - XCTFail("`swiftlint` does not exists.") - return nil - } - return swiftlint -} - -#endif diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml new file mode 100644 index 0000000000..7ec4ddeae4 --- /dev/null +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -0,0 +1,656 @@ +accessibility_label_for_image: + severity: warning +accessibility_trait_for_button: + severity: warning +anonymous_argument_in_multiline_closure: + severity: warning +array_init: + severity: warning +async_without_await: + severity: warning +attribute_name_spacing: + severity: error +attributes: + severity: warning + attributes_with_arguments_always_on_line_above: true + always_on_same_line: ["@IBAction", "@NSManaged"] + always_on_line_above: [] +balanced_xctest_lifecycle: + severity: warning + test_parent_classes: ["QuickSpec", "XCTestCase"] +blanket_disable_command: + severity: warning + allowed_rules: ["file_header", "file_length", "file_name", "file_name_no_space", "single_test_class"] + always_blanket_disable: [] +block_based_kvo: + severity: warning +capture_variable: + severity: warning +class_delegate_protocol: + severity: warning +closing_brace: + severity: warning +closure_body_length: + warning: 30 + error: 100 +closure_end_indentation: + severity: warning +closure_parameter_position: + severity: warning +closure_spacing: + severity: warning +collection_alignment: + severity: warning + align_colons: false +colon: + severity: warning + flexible_right_spacing: false + apply_to_dictionaries: true +comma: + severity: warning +comma_inheritance: + severity: warning +comment_spacing: + severity: warning +compiler_protocol_init: + severity: warning +computed_accessors_order: + severity: warning + order: get_set +conditional_returns_on_newline: + severity: warning + if_only: false +contains_over_filter_count: + severity: warning +contains_over_filter_is_empty: + severity: warning +contains_over_first_not_nil: + severity: warning +contains_over_range_nil_comparison: + severity: warning +contrasted_opening_brace: + severity: warning +control_statement: + severity: warning +convenience_type: + severity: warning +cyclomatic_complexity: + warning: 10 + error: 20 + ignores_case_statements: false +deployment_target: + severity: warning + iOSApplicationExtension_deployment_target: 7.0 + iOS_deployment_target: 7.0 + macOSApplicationExtension_deployment_target: 10.9 + macOS_deployment_target: 10.9 + tvOSApplicationExtension_deployment_target: 9.0 + tvOS_deployment_target: 9.0 + watchOSApplicationExtension_deployment_target: 1.0 + watchOS_deployment_target: 1.0 +direct_return: + severity: warning +discarded_notification_center_observer: + severity: warning +discouraged_assert: + severity: warning +discouraged_direct_init: + severity: warning + types: ["Bundle", "Bundle.init", "NSError", "NSError.init", "UIDevice", "UIDevice.init"] +discouraged_none_name: + severity: warning +discouraged_object_literal: + severity: warning + image_literal: true + color_literal: true +discouraged_optional_boolean: + severity: warning +discouraged_optional_collection: + severity: warning +duplicate_conditions: + severity: error +duplicate_enum_cases: + severity: error +duplicate_imports: + severity: warning +duplicated_key_in_dictionary_literal: + severity: warning +dynamic_inline: + severity: error +empty_collection_literal: + severity: warning +empty_count: + severity: error + only_after_dot: false +empty_enum_arguments: + severity: warning +empty_parameters: + severity: warning +empty_parentheses_with_trailing_closure: + severity: warning +empty_string: + severity: warning +empty_xctest_method: + severity: warning + test_parent_classes: ["QuickSpec", "XCTestCase"] +enum_case_associated_values_count: + warning: 5 + error: 6 +expiring_todo: + approaching_expiry_severity: warning + expired_severity: error + bad_formatting_severity: error + approaching_expiry_threshold: 15 + date_delimiters: + opening: "[" + closing: "]" + date_format: "MM/dd/yyyy" + date_separator: "/" +explicit_acl: + severity: warning +explicit_enum_raw_value: + severity: warning +explicit_init: + severity: warning + include_bare_init: false +explicit_self: + severity: warning +explicit_top_level_acl: + severity: warning +explicit_type_interface: + severity: warning + excluded: [] + allow_redundancy: false +extension_access_modifier: + severity: warning +fallthrough: + severity: warning +fatal_error_message: + severity: warning +file_header: + severity: warning +file_length: + warning: 400 + error: 1000 + ignore_comment_only_lines: false +file_name: + severity: warning + excluded: ["LinuxMain.swift", "main.swift"] + prefix_pattern: "" + suffix_pattern: "\+.*" + nested_type_separator: "." + require_fully_qualified_names: false +file_name_no_space: + severity: warning + excluded: [] +file_types_order: + severity: warning + order: [[supporting_type], [main_type], [extension], [preview_provider], [library_content_provider]] +final_test_case: + severity: warning + test_parent_classes: ["QuickSpec", "XCTestCase"] +first_where: + severity: warning +flatmap_over_map_reduce: + severity: warning +for_where: + severity: warning + allow_for_as_filter: false +force_cast: + severity: error +force_try: + severity: error +force_unwrapping: + severity: warning +function_body_length: + warning: 50 + error: 100 +function_default_parameter_at_end: + severity: warning + ignore_first_isolation_inheritance_parameter: true +function_parameter_count: + warning: 5 + error: 8 + ignores_default_parameters: true +generic_type_name: + min_length: + warning: 1 + error: 0 + max_length: + warning: 20 + error: 1000 + excluded: [] + allowed_symbols: [] + unallowed_symbols_severity: error + validates_start_with_lowercase: error +ibinspectable_in_extension: + severity: warning +identical_operands: + severity: warning +identifier_name: + min_length: + warning: 3 + error: 2 + max_length: + warning: 40 + error: 60 + excluded: ["^id$"] + allowed_symbols: [] + unallowed_symbols_severity: error + validates_start_with_lowercase: error + additional_operators: ["!", "%", "&", "*", "+", "-", ".", "/", "<", "=", ">", "?", "^", "|", "~"] +implicit_getter: + severity: warning +implicit_return: + severity: warning + included: [closure, function, getter, initializer, subscript] +implicitly_unwrapped_optional: + severity: warning + mode: all_except_iboutlets +inclusive_language: + severity: warning +indentation_width: + severity: warning + indentation_width: 4 + include_comments: true + include_compiler_directives: true + include_multiline_strings: true +inert_defer: + severity: warning +invalid_swiftlint_command: + severity: warning +is_disjoint: + severity: warning +joined_default_parameter: + severity: warning +large_tuple: + warning: 2 + error: 3 +last_where: + severity: warning +leading_whitespace: + severity: warning +legacy_cggeometry_functions: + severity: warning +legacy_constant: + severity: warning +legacy_constructor: + severity: warning +legacy_hashing: + severity: warning +legacy_multiple: + severity: warning +legacy_nsgeometry_functions: + severity: warning +legacy_objc_type: + severity: warning +legacy_random: + severity: warning +let_var_whitespace: + severity: warning +line_length: + warning: 120 + error: 200 + ignores_urls: false + ignores_function_declarations: false + ignores_comments: false + ignores_interpolated_strings: false + excluded_lines_patterns: [] +literal_expression_end_indentation: + severity: warning +local_doc_comment: + severity: warning +lower_acl_than_parent: + severity: warning +mark: + severity: warning +missing_docs: + warning: [open, public] + excludes_extensions: true + excludes_inherited_types: true + excludes_trivial_init: false + evaluate_effective_access_control_level: false +modifier_order: + severity: warning + preferred_modifier_order: [override, acl, setterACL, dynamic, mutators, lazy, final, required, convenience, typeMethods, owned] +multiline_arguments: + severity: warning + first_argument_location: any_line + only_enforce_after_first_closure_on_first_line: false +multiline_arguments_brackets: + severity: warning +multiline_function_chains: + severity: warning +multiline_literal_brackets: + severity: warning +multiline_parameters: + severity: warning + allows_single_line: true +multiline_parameters_brackets: + severity: warning +multiple_closures_with_trailing_closure: + severity: warning +nesting: + type_level: + warning: 1 + function_level: + warning: 2 + check_nesting_in_closures_and_statements: true + always_allow_one_type_in_functions: false + ignore_typealiases_and_associatedtypes: false +nimble_operator: + severity: warning +no_empty_block: + severity: warning + disabled_block_types: [] +no_extension_access_modifier: + severity: error +no_fallthrough_only: + severity: warning +no_grouping_extension: + severity: warning +no_magic_numbers: + severity: warning + test_parent_classes: ["QuickSpec", "XCTestCase"] +no_space_in_method_call: + severity: warning +non_optional_string_data_conversion: + severity: warning +non_overridable_class_declaration: + severity: warning + final_class_modifier: final class +notification_center_detachment: + severity: warning +ns_number_init_as_function_reference: + severity: warning +nslocalizedstring_key: + severity: warning +nslocalizedstring_require_bundle: + severity: warning +nsobject_prefer_isequal: + severity: warning +number_separator: + severity: warning + minimum_length: 0 + exclude_ranges: [] +object_literal: + severity: warning + image_literal: true + color_literal: true +one_declaration_per_file: + severity: warning +opening_brace: + severity: warning + ignore_multiline_type_headers: false + ignore_multiline_statement_conditions: false + ignore_multiline_function_signatures: false + allow_multiline_func: false +operator_usage_whitespace: + severity: warning + lines_look_around: 2 + skip_aligned_constants: true + allowed_no_space_operators: ["...", "..<"] +operator_whitespace: + severity: warning +optional_data_string_conversion: + severity: warning +optional_enum_case_matching: + severity: warning +orphaned_doc_comment: + severity: warning +overridden_super_call: + severity: warning + excluded: [] + included: ["*"] +override_in_extension: + severity: warning +pattern_matching_keywords: + severity: warning +period_spacing: + severity: warning +prefer_key_path: + severity: warning + restrict_to_standard_functions: true +prefer_nimble: + severity: warning +prefer_self_in_static_references: + severity: warning +prefer_self_type_over_type_of_self: + severity: warning +prefer_type_checking: + severity: warning +prefer_zero_over_explicit_init: + severity: warning +prefixed_toplevel_constant: + severity: warning + only_private: false +private_action: + severity: warning +private_outlet: + severity: warning + allow_private_set: false +private_over_fileprivate: + severity: warning + validate_extensions: false +private_subject: + severity: warning +private_swiftui_state: + severity: warning +private_unit_test: + severity: warning + test_parent_classes: ["QuickSpec", "XCTestCase"] + regex: "XCTestCase" +prohibited_interface_builder: + severity: warning +prohibited_super_call: + severity: warning + excluded: [] + included: ["*"] +protocol_property_accessors_order: + severity: warning +quick_discouraged_call: + severity: warning +quick_discouraged_focused_test: + severity: warning +quick_discouraged_pending_test: + severity: warning +raw_value_for_camel_cased_codable_enum: + severity: warning +reduce_boolean: + severity: warning +reduce_into: + severity: warning +redundant_discardable_let: + severity: warning +redundant_nil_coalescing: + severity: warning +redundant_objc_attribute: + severity: warning +redundant_optional_initialization: + severity: warning +redundant_self_in_closure: + severity: warning +redundant_set_access_control: + severity: warning +redundant_string_enum_value: + severity: warning +redundant_type_annotation: + severity: warning + ignore_attributes: ["IBInspectable"] + ignore_properties: false + consider_default_literal_types_redundant: false +redundant_void_return: + severity: warning + include_closures: true +required_deinit: + severity: warning +required_enum_case: + {Protocol Name}: + {Case Name 1}: {warning|error} + {Case Name 2}: {warning|error} +return_arrow_whitespace: + severity: warning +return_value_from_void_function: + severity: warning +self_binding: + severity: warning + bind_identifier: "self" +self_in_property_initialization: + severity: warning +shorthand_argument: + severity: warning + allow_until_line_after_opening_brace: 4 + always_disallow_more_than_one: false + always_disallow_member_access: false +shorthand_operator: + severity: error +shorthand_optional_binding: + severity: warning +single_test_class: + severity: warning + test_parent_classes: ["QuickSpec", "XCTestCase"] +sorted_enum_cases: + severity: warning +sorted_first_last: + severity: warning +sorted_imports: + severity: warning + grouping: names +statement_position: + severity: warning + statement_mode: default +static_operator: + severity: warning +static_over_final_class: + severity: warning +strict_fileprivate: + severity: warning +strong_iboutlet: + severity: warning +superfluous_disable_command: + severity: warning +superfluous_else: + severity: warning +switch_case_alignment: + severity: warning + indented_cases: false + ignore_one_liners: false +switch_case_on_newline: + severity: warning +syntactic_sugar: + severity: warning +test_case_accessibility: + severity: warning + allowed_prefixes: [] + test_parent_classes: ["QuickSpec", "XCTestCase"] +todo: + severity: warning + only: [TODO, FIXME] +toggle_bool: + severity: warning +trailing_closure: + severity: warning + only_single_muted_parameter: false +trailing_comma: + severity: warning + mandatory_comma: false +trailing_newline: + severity: warning +trailing_semicolon: + severity: warning +trailing_whitespace: + severity: warning + ignores_empty_lines: false + ignores_comments: true +type_body_length: + warning: 250 + error: 350 +type_contents_order: + severity: warning + order: [[case], [type_alias, associated_type], [subtype], [type_property], [instance_property], [ib_inspectable], [ib_outlet], [initializer], [type_method], [view_life_cycle_method], [ib_action], [other_method], [subscript], [deinitializer]] +type_name: + min_length: + warning: 3 + error: 0 + max_length: + warning: 40 + error: 1000 + excluded: [] + allowed_symbols: [] + unallowed_symbols_severity: error + validates_start_with_lowercase: error + validate_protocols: true +typesafe_array_init: + severity: warning +unavailable_condition: + severity: warning +unavailable_function: + severity: warning +unhandled_throwing_task: + severity: error +unneeded_break_in_switch: + severity: warning +unneeded_override: + severity: warning + affect_initializers: false +unneeded_parentheses_in_closure_argument: + severity: warning +unneeded_synthesized_initializer: + severity: warning +unowned_variable_capture: + severity: warning +untyped_error_in_catch: + severity: warning +unused_capture_list: + severity: warning +unused_closure_parameter: + severity: warning +unused_control_flow_label: + severity: warning +unused_declaration: + severity: error + include_public_and_open: false + related_usrs_to_skip: ["s:7SwiftUI15PreviewProviderP"] +unused_enumerated: + severity: warning +unused_import: + severity: warning + require_explicit_imports: false + allowed_transitive_imports: [] + always_keep_imports: [] +unused_optional_binding: + severity: warning + ignore_optional_try: false +unused_parameter: + severity: warning +unused_setter_value: + severity: warning +valid_ibinspectable: + severity: warning +vertical_parameter_alignment: + severity: warning +vertical_parameter_alignment_on_call: + severity: warning +vertical_whitespace: + severity: warning + max_empty_lines: 1 +vertical_whitespace_between_cases: + severity: warning +vertical_whitespace_closing_braces: + severity: warning + only_enforce_before_trivial_lines: false +vertical_whitespace_opening_braces: + severity: warning +void_function_in_ternary: + severity: warning +void_return: + severity: warning +weak_delegate: + severity: warning +xct_specific_matcher: + severity: warning + matchers: [one-argument-asserts, two-argument-asserts] +xctfail_message: + severity: warning +yoda_condition: + severity: warning diff --git a/Tests/MacroTests/MakeAcceptableByConfigurationElementTests.swift b/Tests/MacroTests/AcceptableByConfigurationElementTests.swift similarity index 80% rename from Tests/MacroTests/MakeAcceptableByConfigurationElementTests.swift rename to Tests/MacroTests/AcceptableByConfigurationElementTests.swift index 041d80443b..b63c5a6a2b 100644 --- a/Tests/MacroTests/MakeAcceptableByConfigurationElementTests.swift +++ b/Tests/MacroTests/AcceptableByConfigurationElementTests.swift @@ -3,15 +3,14 @@ import SwiftSyntaxMacrosTestSupport import XCTest private let macros = [ - "MakeAcceptableByConfigurationElement": MakeAcceptableByConfigurationElement.self + "AcceptableByConfigurationElement": AcceptableByConfigurationElement.self ] -// swiftlint:disable:next type_name -final class MakeAcceptableByConfigurationElementTests: XCTestCase { +final class AcceptableByConfigurationElementTests: XCTestCase { func testNoEnum() { assertMacroExpansion( """ - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement struct S { } """, @@ -29,7 +28,7 @@ final class MakeAcceptableByConfigurationElementTests: XCTestCase { func testNoStringRawType() { assertMacroExpansion( """ - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement enum E { } """, @@ -47,7 +46,7 @@ final class MakeAcceptableByConfigurationElementTests: XCTestCase { func testPrivateEnum() { assertMacroExpansion( """ - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement private enum E: String { } """, @@ -61,10 +60,10 @@ final class MakeAcceptableByConfigurationElementTests: XCTestCase { .symbol(rawValue) } private init(fromAny value: Any, context ruleID: String) throws { - if let value = value as? String, let newSelf = Self (rawValue: value) { + if let value = value as? String, let newSelf = Self(rawValue: value) { self = newSelf } else { - throw Issue.unknownConfiguration(ruleID: ruleID) + throw Issue.invalidConfiguration(ruleID: ruleID) } } } @@ -75,7 +74,7 @@ final class MakeAcceptableByConfigurationElementTests: XCTestCase { func testPublicEnum() { assertMacroExpansion( """ - @MakeAcceptableByConfigurationElement + @AcceptableByConfigurationElement public enum E: String { } """, @@ -89,10 +88,10 @@ final class MakeAcceptableByConfigurationElementTests: XCTestCase { .symbol(rawValue) } public init(fromAny value: Any, context ruleID: String) throws { - if let value = value as? String, let newSelf = Self (rawValue: value) { + if let value = value as? String, let newSelf = Self(rawValue: value) { self = newSelf } else { - throw Issue.unknownConfiguration(ruleID: ruleID) + throw Issue.invalidConfiguration(ruleID: ruleID) } } } diff --git a/Tests/MacroTests/AutoApplyTests.swift b/Tests/MacroTests/AutoApplyTests.swift deleted file mode 100644 index 86dce459ba..0000000000 --- a/Tests/MacroTests/AutoApplyTests.swift +++ /dev/null @@ -1,131 +0,0 @@ -@testable import SwiftLintCoreMacros -import SwiftSyntaxMacrosTestSupport -import XCTest - -private let macros = [ - "AutoApply": AutoApply.self -] - -final class AutoApplyTests: XCTestCase { - func testAttachToClass() { - assertMacroExpansion( - """ - @AutoApply - class C { - } - """, - expandedSource: - """ - class C { - } - """, - diagnostics: [ - DiagnosticSpec(message: SwiftLintCoreMacroError.notStruct.message, line: 1, column: 1) - ], - macros: macros) - } - - func testNoConfigurationElements() { - assertMacroExpansion( - """ - @AutoApply - struct S { - } - """, - expandedSource: - """ - struct S { - - mutating func apply(configuration: Any) throws { - - guard let _ = configuration as? [String: Any] else { - throw Issue.invalidConfiguration(ruleID: Parent.description.identifier) - } - - if !supportedKeys.isSuperset(of: configuration.keys) { - let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) - throw Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys) - } - } - } - """, - macros: macros - ) - } - - func testConfigurationElementsWithoutKeys() { - assertMacroExpansion( - """ - @AutoApply - struct S { - @ConfigurationElement - var e1 = 1 - @ConfigurationElement(value: 7) - var e2 = 2 - } - """, - expandedSource: - """ - struct S { - @ConfigurationElement - var e1 = 1 - @ConfigurationElement(value: 7) - var e2 = 2 - - mutating func apply(configuration: Any) throws { - try e1.apply(configuration, ruleID: Parent.identifier) - try e2.apply(configuration, ruleID: Parent.identifier) - guard let _ = configuration as? [String: Any] else { - return - } - - if !supportedKeys.isSuperset(of: configuration.keys) { - let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) - throw Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys) - } - } - } - """, - macros: macros - ) - } - - func testConfigurationElementsWithKeys() { - assertMacroExpansion( - """ - @AutoApply - struct S { - @ConfigurationElement(key: "e1") - var e1 = 1 - @ConfigurationElement(key: "e2", other: 7) - var e2 = 2 - } - """, - expandedSource: - """ - struct S { - @ConfigurationElement(key: "e1") - var e1 = 1 - @ConfigurationElement(key: "e2", other: 7) - var e2 = 2 - - mutating func apply(configuration: Any) throws { - - guard let configuration = configuration as? [String: Any] else { - throw Issue.invalidConfiguration(ruleID: Parent.description.identifier) - } - try e1.apply(configuration[$e1.key], ruleID: Parent.identifier) - try $e1.performAfterParseOperations() - try e2.apply(configuration[$e2.key], ruleID: Parent.identifier) - try $e2.performAfterParseOperations() - if !supportedKeys.isSuperset(of: configuration.keys) { - let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) - throw Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys) - } - } - } - """, - macros: macros - ) - } -} diff --git a/Tests/MacroTests/AutoConfigParserTests.swift b/Tests/MacroTests/AutoConfigParserTests.swift new file mode 100644 index 0000000000..86ce276120 --- /dev/null +++ b/Tests/MacroTests/AutoConfigParserTests.swift @@ -0,0 +1,242 @@ +@testable import SwiftLintCoreMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +private let macros = [ + "AutoConfigParser": AutoConfigParser.self +] + +final class AutoConfigParserTests: XCTestCase { + func testAttachToClass() { + assertMacroExpansion( + """ + @AutoConfigParser + class C { + } + """, + expandedSource: + """ + class C { + } + """, + diagnostics: [ + DiagnosticSpec(message: SwiftLintCoreMacroError.notStruct.message, line: 1, column: 1) + ], + macros: macros) + } + + func testNoConfigurationElements() { + assertMacroExpansion( + """ + @AutoConfigParser + struct S { + } + """, + expandedSource: + """ + struct S { + + mutating func apply(configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + if !supportedKeys.isSuperset(of: configuration.keys) { + let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) + Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys).print() + } + try validate() + } + } + """, + macros: macros + ) + } + + func testConfigurationElementsWithoutKeys() { + assertMacroExpansion( + """ + @AutoConfigParser + struct S { + @ConfigurationElement + var eA = 1 + @ConfigurationElement(key: "name") + var eB = 2 + } + """, + expandedSource: + """ + struct S { + @ConfigurationElement + var eA = 1 + @ConfigurationElement(key: "name") + var eB = 2 + + mutating func apply(configuration: Any) throws { + if $eA.key.isEmpty { + $eA.key = "e_a" + } + if $eB.key.isEmpty { + $eB.key = "e_b" + } + guard let configuration = configuration as? [String: Any] else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + if let value = configuration[$eA.key] { + try eA.apply(value, ruleID: Parent.identifier) + } + if let value = configuration[$eB.key] { + try eB.apply(value, ruleID: Parent.identifier) + } + if !supportedKeys.isSuperset(of: configuration.keys) { + let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) + Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys).print() + } + try validate() + } + } + """, + macros: macros + ) + } + + func testInlinedConfigurationElements() { + assertMacroExpansion( + """ + @AutoConfigParser + struct S { + @ConfigurationElement(key: "eD") + var eA = 1 + @ConfigurationElement(inline: true) + var eB = 2 + @ConfigurationElement(inline: false) + var eC = 3 + } + """, + expandedSource: + """ + struct S { + @ConfigurationElement(key: "eD") + var eA = 1 + @ConfigurationElement(inline: true) + var eB = 2 + @ConfigurationElement(inline: false) + var eC = 3 + + mutating func apply(configuration: Any) throws { + if $eA.key.isEmpty { + $eA.key = "e_a" + } + if $eC.key.isEmpty { + $eC.key = "e_c" + } + do { + try eB.apply(configuration, ruleID: Parent.identifier) + } catch let issue as Issue where issue == Issue.nothingApplied(ruleID: Parent.identifier) { + // Acceptable. Continue. + } + guard let configuration = configuration as? [String: Any] else { + return + } + if let value = configuration[$eA.key] { + try eA.apply(value, ruleID: Parent.identifier) + } + if let value = configuration[$eC.key] { + try eC.apply(value, ruleID: Parent.identifier) + } + if !supportedKeys.isSuperset(of: configuration.keys) { + let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) + Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys).print() + } + try validate() + } + } + """, + macros: macros + ) + } + + func testSeverityBasedConfigurationWithoutSeverityProperty() { + assertMacroExpansion( + """ + @AutoConfigParser + struct S: SeverityBasedRuleConfiguration { + } + """, + expandedSource: + """ + struct S: SeverityBasedRuleConfiguration { + + mutating func apply(configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + if !supportedKeys.isSuperset(of: configuration.keys) { + let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) + Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys).print() + } + try validate() + } + } + """, + diagnostics: [ + DiagnosticSpec( + message: SwiftLintCoreMacroError.severityBasedWithoutProperty.message, + line: 2, + column: 8 + ), + ], + macros: macros) + } + + func testSeverityAppliedTwice() { + assertMacroExpansion( + """ + @AutoConfigParser + struct S: SeverityBasedRuleConfiguration { + @ConfigurationElement + var severityConfiguration = .warning + @ConfigurationElement + var foo = 2 + } + """, + expandedSource: + """ + struct S: SeverityBasedRuleConfiguration { + @ConfigurationElement + var severityConfiguration = .warning + @ConfigurationElement + var foo = 2 + + mutating func apply(configuration: Any) throws { + if $severityConfiguration.key.isEmpty { + $severityConfiguration.key = "severity_configuration" + } + if $foo.key.isEmpty { + $foo.key = "foo" + } + do { + try severityConfiguration.apply(configuration, ruleID: Parent.identifier) + } catch let issue as Issue where issue == Issue.nothingApplied(ruleID: Parent.identifier) { + // Acceptable. Continue. + } + guard let configuration = configuration as? [String: Any] else { + return + } + if let value = configuration[$severityConfiguration.key] { + try severityConfiguration.apply(value, ruleID: Parent.identifier) + } + if let value = configuration[$foo.key] { + try foo.apply(value, ruleID: Parent.identifier) + } + if !supportedKeys.isSuperset(of: configuration.keys) { + let unknownKeys = Set(configuration.keys).subtracting(supportedKeys) + Issue.invalidConfigurationKeys(ruleID: Parent.identifier, keys: unknownKeys).print() + } + try validate() + } + } + """, + macros: macros + ) + } +} diff --git a/Tests/MacroTests/SwiftSyntaxRuleTests.swift b/Tests/MacroTests/SwiftSyntaxRuleTests.swift index 624d3a5d71..d5a7dd28d7 100644 --- a/Tests/MacroTests/SwiftSyntaxRuleTests.swift +++ b/Tests/MacroTests/SwiftSyntaxRuleTests.swift @@ -94,7 +94,7 @@ final class SwiftSyntaxRuleTests: XCTestCase { """, diagnostics: [ DiagnosticSpec(message: SwiftLintCoreMacroError.noBooleanLiteral.message, line: 1, column: 35), - DiagnosticSpec(message: SwiftLintCoreMacroError.noBooleanLiteral.message, line: 1, column: 63) + DiagnosticSpec(message: SwiftLintCoreMacroError.noBooleanLiteral.message, line: 1, column: 63), ], macros: macros ) diff --git a/Tests/SwiftLintFrameworkTests/AccessControlLevelTests.swift b/Tests/SwiftLintFrameworkTests/AccessControlLevelTests.swift index 7df7931bee..ac1efded9d 100644 --- a/Tests/SwiftLintFrameworkTests/AccessControlLevelTests.swift +++ b/Tests/SwiftLintFrameworkTests/AccessControlLevelTests.swift @@ -1,11 +1,12 @@ import SwiftLintCore import XCTest -class AccessControlLevelTests: SwiftLintTestCase { +final class AccessControlLevelTests: SwiftLintTestCase { func testDescription() { XCTAssertEqual(AccessControlLevel.private.description, "private") XCTAssertEqual(AccessControlLevel.fileprivate.description, "fileprivate") XCTAssertEqual(AccessControlLevel.internal.description, "internal") + XCTAssertEqual(AccessControlLevel.package.description, "package") XCTAssertEqual(AccessControlLevel.public.description, "public") XCTAssertEqual(AccessControlLevel.open.description, "open") } @@ -13,7 +14,8 @@ class AccessControlLevelTests: SwiftLintTestCase { func testPriority() { XCTAssertLessThan(AccessControlLevel.private, .fileprivate) XCTAssertLessThan(AccessControlLevel.fileprivate, .internal) - XCTAssertLessThan(AccessControlLevel.internal, .public) + XCTAssertLessThan(AccessControlLevel.internal, .package) + XCTAssertLessThan(AccessControlLevel.package, .public) XCTAssertLessThan(AccessControlLevel.public, .open) } } diff --git a/Tests/SwiftLintFrameworkTests/AttributesRuleTests.swift b/Tests/SwiftLintFrameworkTests/AttributesRuleTests.swift index 29d4306c6f..2a0f5444ee 100644 --- a/Tests/SwiftLintFrameworkTests/AttributesRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/AttributesRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class AttributesRuleTests: SwiftLintTestCase { +final class AttributesRuleTests: SwiftLintTestCase { func testAttributesWithAlwaysOnSameLine() { // Test with custom `always_on_same_line` let nonTriggeringExamples = [ @@ -15,12 +15,12 @@ class AttributesRuleTests: SwiftLintTestCase { """), Example(""" @objc(XYZFoo) class Foo: NSObject {} - """) + """), ] let triggeringExamples = [ Example("@objc\n ↓var x: String"), Example("@objc\n ↓func foo()"), - Example("@nonobjc ↓func foo()") + Example("@nonobjc ↓func foo()"), ] let alwaysOnSameLineDescription = AttributesRule.description @@ -36,12 +36,12 @@ class AttributesRuleTests: SwiftLintTestCase { let nonTriggeringExamples = [ Example("@objc\n var x: String"), Example("@objc\n func foo()"), - Example("@nonobjc\n func foo()") + Example("@nonobjc\n func foo()"), ] let triggeringExamples = [ Example("@objc ↓var x: String"), Example("@objc ↓func foo()"), - Example("@nonobjc ↓func foo()") + Example("@nonobjc ↓func foo()"), ] let alwaysOnNewLineDescription = AttributesRule.description @@ -66,7 +66,7 @@ class AttributesRuleTests: SwiftLintTestCase { @objc optional func tagDidSelect(_ title: String, sender: TagListView) @objc optional func tagDidDeselect(_ title: String, sender: TagListView) } - """) + """), ] let triggeringExamples = [ @@ -84,16 +84,21 @@ class AttributesRuleTests: SwiftLintTestCase { optional ↓func tagDidSelect(_ title: String, sender: TagListView) @objc optional func tagDidDeselect(_ title: String, sender: TagListView) } - """) + """), ] let alwaysOnNewLineDescription = AttributesRule.description .with(triggeringExamples: triggeringExamples) .with(nonTriggeringExamples: nonTriggeringExamples) - verifyRule(alwaysOnNewLineDescription, - ruleConfiguration: ["always_on_same_line": ["@discardableResult", "@objc", - "@IBAction", "@IBDesignable"]]) + verifyRule( + alwaysOnNewLineDescription, + ruleConfiguration: [ + "always_on_same_line": [ + "@discardableResult", "@objc", "@IBAction", "@IBDesignable", + ], + ] + ) } func testAttributesWithArgumentsAlwaysOnLineAboveFalse() { @@ -104,7 +109,7 @@ class AttributesRuleTests: SwiftLintTestCase { Example(""" @Environment(\\.presentationMode) private ↓var presentationMode - """) + """), ] let argumentsAlwaysOnLineDescription = AttributesRule.description diff --git a/Tests/SwiftLintFrameworkTests/BaselineTests.swift b/Tests/SwiftLintFrameworkTests/BaselineTests.swift new file mode 100644 index 0000000000..aefcca06aa --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/BaselineTests.swift @@ -0,0 +1,224 @@ +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import XCTest + +private var temporaryDirectoryPath: String { + let result = URL( + fileURLWithPath: NSTemporaryDirectory(), + isDirectory: true + ).path + +#if os(macOS) + return "/private" + result +#else + return result +#endif +} + +final class BaselineTests: XCTestCase { + private static let example = """ + import Foundation + import SwiftLintFramework + + class Example: NSObject { + private var foo: Int + private var bar: String + + init(foo: Int, bar: String) { + self.foo = foo + self.bar = bar + } // init + func someFunction() -> Int { + foo * 10 + } // someFunction + func someOtherFunction() -> String { + bar + } // someOtherFunction + func yetAnotherFunction() -> (Int, String) { + (foo, bar) + } // yetAnotherFunction + } + """ + + private static let ruleDescriptions = [ + ArrayInitRule.description, + BlockBasedKVORule.description, + ClosingBraceRule.description, + DirectReturnRule.description, + ] + + private static var currentDirectoryPath: String? + + private static func violations(for filePath: String?) -> [StyleViolation] { + ruleDescriptions.violations(for: filePath) + } + + private static func baseline(for filePath: String) -> Baseline { + Baseline(violations: ruleDescriptions.violations(for: filePath)) + } + + override static func setUp() { + super.setUp() + currentDirectoryPath = FileManager.default.currentDirectoryPath + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(temporaryDirectoryPath)) + } + + override static func tearDown() { + if let currentDirectoryPath { + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(currentDirectoryPath)) + self.currentDirectoryPath = nil + } + super.tearDown() + } + + func testWritingAndReading() throws { + try withExampleFileCreated { sourceFilePath in + let baselinePath = temporaryDirectoryPath.stringByAppendingPathComponent(UUID().uuidString) + try Baseline(violations: Self.violations(for: sourceFilePath)).write(toPath: baselinePath) + defer { + try? FileManager.default.removeItem(atPath: baselinePath) + } + let newBaseline = try Baseline(fromPath: baselinePath) + XCTAssertEqual(newBaseline, Self.baseline(for: sourceFilePath)) + } + } + + func testUnchangedViolations() throws { + try withExampleFileCreated { sourceFilePath in + XCTAssertEqual(Self.baseline(for: sourceFilePath).filter(Self.violations(for: sourceFilePath)), []) + } + } + + func testShiftedViolations() throws { + try withExampleFileCreated { sourceFilePath in + let baseline = Self.baseline(for: sourceFilePath) + let violations = try Self.violations(for: sourceFilePath).lineShifted(by: 2, path: sourceFilePath) + XCTAssertEqual(baseline.filter(violations), []) + } + } + + func testNewViolation() throws { + try testViolationDetection( + violationRuleDescriptions: Self.ruleDescriptions, + newViolationRuleDescription: EmptyCollectionLiteralRule.description, + insertionIndex: 2 + ) + } + + func testViolationDetection() throws { + let violationRuleDescriptions = [ + ArrayInitRule.description, + BlockBasedKVORule.description, + ArrayInitRule.description, + ClosingBraceRule.description, + ClosingBraceRule.description, + ClosingBraceRule.description, + BlockBasedKVORule.description, + DirectReturnRule.description, + ArrayInitRule.description, + ClosingBraceRule.description, + ] + + let ruleDescriptions = [ + ArrayInitRule.description, + BlockBasedKVORule.description, + ClosingBraceRule.description, + DirectReturnRule.description, + ] + + for ruleDescription in ruleDescriptions { + for insertionIndex in 0.. Void) throws { + let sourceFilePath = temporaryDirectoryPath.stringByAppendingPathComponent("\(UUID().uuidString).swift") + guard let data = Self.example.data(using: .utf8) else { + XCTFail("Could not convert example code to data using UTF-8 encoding") + return + } + try data.write(to: URL(fileURLWithPath: sourceFilePath)) + defer { + try? FileManager.default.removeItem(atPath: sourceFilePath) + } + try block(sourceFilePath) + } +} + +private extension [StyleViolation] { + func lineShifted(by shift: Int, path: String) throws -> [StyleViolation] { + guard shift > 0 else { + XCTFail("Shift must be positive") + return self + } + var lines = SwiftLintFile(path: path)?.lines.map(\.content) ?? [] + lines = [String](repeating: "", count: shift) + lines + if let data = lines.joined(separator: "\n").data(using: .utf8) { + try data.write(to: URL(fileURLWithPath: path)) + } + return map { + let shiftedLocation = Location( + file: path, + line: $0.location.line != nil ? $0.location.line! + shift : nil, + character: $0.location.character + ) + return $0.with(location: shiftedLocation) + } + } +} + +private extension Sequence where Element == RuleDescription { + func violations(for filePath: String?) -> [StyleViolation] { + enumerated().map { index, ruleDescription in + StyleViolation( + ruleDescription: ruleDescription, + location: Location(file: filePath, line: (index + 1) * 2, character: 1) + ) + } + } +} diff --git a/Tests/SwiftLintFrameworkTests/BlanketDisableCommandRuleTests.swift b/Tests/SwiftLintFrameworkTests/BlanketDisableCommandRuleTests.swift index 728855e913..9540c085c1 100644 --- a/Tests/SwiftLintFrameworkTests/BlanketDisableCommandRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/BlanketDisableCommandRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class BlanketDisableCommandRuleTests: SwiftLintTestCase { +final class BlanketDisableCommandRuleTests: SwiftLintTestCase { private lazy var emptyDescription = BlanketDisableCommandRule.description .with(triggeringExamples: []) .with(nonTriggeringExamples: []) @@ -14,7 +14,7 @@ class BlanketDisableCommandRuleTests: SwiftLintTestCase { Example("// swiftlint:disable file_length\n// swiftlint:enable ↓file_length"), Example("// swiftlint:disable:previous ↓file_length"), Example("// swiftlint:disable:this ↓file_length"), - Example("// swiftlint:disable:next ↓file_length") + Example("// swiftlint:disable:next ↓file_length"), ] verifyRule(emptyDescription.with(triggeringExamples: triggeringExamples), ruleConfiguration: ["always_blanket_disable": ["file_length"]], @@ -31,7 +31,7 @@ class BlanketDisableCommandRuleTests: SwiftLintTestCase { func testAllowedRules() { let nonTriggeringExamples = [ Example("// swiftlint:disable file_length"), - Example("// swiftlint:disable single_test_class") + Example("// swiftlint:disable single_test_class"), ] verifyRule(emptyDescription.with(nonTriggeringExamples: nonTriggeringExamples)) } diff --git a/Tests/SwiftLintFrameworkTests/ChildOptionSeverityConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ChildOptionSeverityConfigurationTests.swift index 042ccc7fab..10d4cd4d0c 100644 --- a/Tests/SwiftLintFrameworkTests/ChildOptionSeverityConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/ChildOptionSeverityConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ChildOptionSeverityConfigurationTests: SwiftLintTestCase { +final class ChildOptionSeverityConfigurationTests: SwiftLintTestCase { typealias TesteeType = ChildOptionSeverityConfiguration func testSeverity() { diff --git a/Tests/SwiftLintFrameworkTests/CodeIndentingRewriterTests.swift b/Tests/SwiftLintFrameworkTests/CodeIndentingRewriterTests.swift index fb3bea1fb8..071b6aff8d 100644 --- a/Tests/SwiftLintFrameworkTests/CodeIndentingRewriterTests.swift +++ b/Tests/SwiftLintFrameworkTests/CodeIndentingRewriterTests.swift @@ -3,7 +3,7 @@ import SwiftParser import SwiftSyntax import XCTest -class CodeIndentingRewriterTests: XCTestCase { +final class CodeIndentingRewriterTests: XCTestCase { func testIndentDefaultStyle() { assertIndent( source: """ diff --git a/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift b/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift index 862064b4f3..dc5cea9e4f 100644 --- a/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift @@ -2,18 +2,22 @@ import SwiftLintTestHelpers import XCTest -class CollectingRuleTests: SwiftLintTestCase { +final class CollectingRuleTests: SwiftLintTestCase { func testCollectsIntoStorage() { struct Spec: MockCollectingRule { var configuration = SeverityConfiguration(.warning) - func collectInfo(for file: SwiftLintFile) -> Int { - return 42 + func collectInfo(for _: SwiftLintFile) -> Int { + 42 } func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: Int]) -> [StyleViolation] { XCTAssertEqual(collectedInfo[file], 42) - return [StyleViolation(ruleDescription: Self.description, - location: Location(file: file, byteOffset: 0))] + return [ + StyleViolation( + ruleDescription: Self.description, + location: Location(file: file, byteOffset: 0) + ), + ] } } @@ -25,15 +29,19 @@ class CollectingRuleTests: SwiftLintTestCase { var configuration = SeverityConfiguration(.warning) func collectInfo(for file: SwiftLintFile) -> String { - return file.contents + file.contents } func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: String]) -> [StyleViolation] { let values = collectedInfo.values XCTAssertTrue(values.contains("foo")) XCTAssertTrue(values.contains("bar")) XCTAssertTrue(values.contains("baz")) - return [StyleViolation(ruleDescription: Self.description, - location: Location(file: file, byteOffset: 0))] + return [ + StyleViolation( + ruleDescription: Self.description, + location: Location(file: file, byteOffset: 0) + ), + ] } } @@ -45,14 +53,18 @@ class CollectingRuleTests: SwiftLintTestCase { struct Spec: MockCollectingRule, AnalyzerRule { var configuration = SeverityConfiguration(.warning) - func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> [String] { - return compilerArguments + func collectInfo(for _: SwiftLintFile, compilerArguments: [String]) -> [String] { + compilerArguments } func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: [String]], compilerArguments: [String]) -> [StyleViolation] { XCTAssertEqual(collectedInfo[file], compilerArguments) - return [StyleViolation(ruleDescription: Self.description, - location: Location(file: file, byteOffset: 0))] + return [ + StyleViolation( + ruleDescription: Self.description, + location: Location(file: file, byteOffset: 0) + ), + ] } } @@ -64,21 +76,29 @@ class CollectingRuleTests: SwiftLintTestCase { var configuration = SeverityConfiguration(.warning) func collectInfo(for file: SwiftLintFile) -> String { - return file.contents + file.contents } func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: String]) -> [StyleViolation] { if collectedInfo[file] == "baz" { - return [StyleViolation(ruleDescription: Self.description, - location: Location(file: file, byteOffset: 2))] + return [ + StyleViolation( + ruleDescription: Self.description, + location: Location(file: file, byteOffset: 2) + ), + ] } return [] } func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: String]) -> [Correction] { if collectedInfo[file] == "baz" { - return [Correction(ruleDescription: Self.description, - location: Location(file: file, byteOffset: 2))] + return [ + Correction( + ruleDescription: Self.description, + location: Location(file: file, byteOffset: 2) + ), + ] } return [] } @@ -87,26 +107,29 @@ class CollectingRuleTests: SwiftLintTestCase { struct AnalyzerSpec: MockCollectingRule, AnalyzerRule, CollectingCorrectableRule { var configuration = SeverityConfiguration(.warning) - func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> String { - return file.contents + func collectInfo(for file: SwiftLintFile, compilerArguments _: [String]) -> String { + file.contents } - func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: String], compilerArguments: [String]) + func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: String], compilerArguments _: [String]) -> [StyleViolation] { if collectedInfo[file] == "baz" { - return [StyleViolation(ruleDescription: Spec.description, - location: Location(file: file, byteOffset: 2))] + return [ + StyleViolation( + ruleDescription: Spec.description, + location: Location(file: file, byteOffset: 2) + ), + ] } return [] } - func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: String], - compilerArguments: [String]) -> [Correction] { - if collectedInfo[file] == "baz" { - return [Correction(ruleDescription: Spec.description, - location: Location(file: file, byteOffset: 2))] - } - return [] + func correct(file: SwiftLintFile, + collectedInfo: [SwiftLintFile: String], + compilerArguments _: [String]) -> [Correction] { + collectedInfo[file] == "baz" + ? [Correction(ruleDescription: Spec.description, location: Location(file: file, byteOffset: 2))] + : [] } } @@ -121,11 +144,11 @@ extension MockCollectingRule { @RuleConfigurationDescriptionBuilder var configurationDescription: some Documentable { RuleConfigurationOption.noOptions } static var description: RuleDescription { - return RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint) + RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint) } static var configuration: Configuration? { - return Configuration(rulesMode: .only([description.identifier]), ruleList: RuleList(rules: self)) + Configuration(rulesMode: .onlyConfiguration([identifier]), ruleList: RuleList(rules: self)) } - init(configuration: Any) throws { self.init() } + init(configuration _: Any) throws { self.init() } } diff --git a/Tests/SwiftLintFrameworkTests/CollectionAlignmentRuleTests.swift b/Tests/SwiftLintFrameworkTests/CollectionAlignmentRuleTests.swift index 010c24d1f7..364589bc88 100644 --- a/Tests/SwiftLintFrameworkTests/CollectionAlignmentRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/CollectionAlignmentRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class CollectionAlignmentRuleTests: SwiftLintTestCase { +final class CollectionAlignmentRuleTests: SwiftLintTestCase { func testCollectionAlignmentWithAlignLeft() { let baseDescription = CollectionAlignmentRule.description let examples = CollectionAlignmentRule.Examples(alignColons: false) diff --git a/Tests/SwiftLintFrameworkTests/ColonRuleTests.swift b/Tests/SwiftLintFrameworkTests/ColonRuleTests.swift index 12079bf451..095214241e 100644 --- a/Tests/SwiftLintFrameworkTests/ColonRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ColonRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class ColonRuleTests: SwiftLintTestCase { +final class ColonRuleTests: SwiftLintTestCase { func testColonWithFlexibleRightSpace() { // Verify Colon rule with test values for when flexible_right_spacing // is true. @@ -10,7 +10,7 @@ class ColonRuleTests: SwiftLintTestCase { Example("let abc: ([Void], String, Int)\n"), Example("let abc: [([Void], String, Int)]\n"), Example("func abc(def: Void) {}\n"), - Example("let abc = [Void: Void]()\n") + Example("let abc = [Void: Void]()\n"), ] let triggeringExamples: [Example] = [ Example("let abc↓:Void\n"), @@ -34,7 +34,7 @@ class ColonRuleTests: SwiftLintTestCase { Example("let abc = [Void↓ : Void]()\n"), Example("let abc = [Void↓ : Void]()\n"), Example("let abc = [1: [3↓ : 2], 3: 4]\n"), - Example("let abc = [1: [3↓ : 2], 3: 4]\n") + Example("let abc = [1: [3↓ : 2], 3: 4]\n"), ] let corrections: [Example: Example] = [ Example("let abc↓:Void\n"): Example("let abc: Void\n"), @@ -58,7 +58,7 @@ class ColonRuleTests: SwiftLintTestCase { Example("let abc = [Void↓ : Void]()\n"): Example("let abc = [Void: Void]()\n"), Example("let abc = [Void↓ : Void]()\n"): Example("let abc = [Void: Void]()\n"), Example("let abc = [1: [3↓ : 2], 3: 4]\n"): Example("let abc = [1: [3: 2], 3: 4]\n"), - Example("let abc = [1: [3↓ : 2], 3: 4]\n"): Example("let abc = [1: [3: 2], 3: 4]\n") + Example("let abc = [1: [3↓ : 2], 3: 4]\n"): Example("let abc = [1: [3: 2], 3: 4]\n"), ] let description = ColonRule.description.with(triggeringExamples: triggeringExamples) .with(nonTriggeringExamples: nonTriggeringExamples) @@ -74,7 +74,7 @@ class ColonRuleTests: SwiftLintTestCase { Example("let abc = [Void: Void]()\n"), Example("let abc = [Void : Void]()\n"), Example("let abc = [1: [3 : 2], 3: 4]\n"), - Example("let abc = [1: [3 : 2], 3: 4]\n") + Example("let abc = [1: [3 : 2], 3: 4]\n"), ] let triggeringExamples: [Example] = [ Example("let abc↓:Void\n"), @@ -98,7 +98,7 @@ class ColonRuleTests: SwiftLintTestCase { Example("func abc(def↓: Void) {}\n"), Example("func abc(def↓ :Void) {}\n"), Example("func abc(def↓ : Void) {}\n"), - Example("func abc(def: Void, ghi↓ :Void) {}\n") + Example("func abc(def: Void, ghi↓ :Void) {}\n"), ] let corrections: [Example: Example] = [ Example("let abc↓:Void\n"): Example("let abc: Void\n"), @@ -122,7 +122,7 @@ class ColonRuleTests: SwiftLintTestCase { Example("func abc(def↓: Void) {}\n"): Example("func abc(def: Void) {}\n"), Example("func abc(def↓ :Void) {}\n"): Example("func abc(def: Void) {}\n"), Example("func abc(def↓ : Void) {}\n"): Example("func abc(def: Void) {}\n"), - Example("func abc(def: Void, ghi↓ :Void) {}\n"): Example("func abc(def: Void, ghi: Void) {}\n") + Example("func abc(def: Void, ghi↓ :Void) {}\n"): Example("func abc(def: Void, ghi: Void) {}\n"), ] let description = ColonRule.description.with(triggeringExamples: triggeringExamples) diff --git a/Tests/SwiftLintFrameworkTests/CommandTests.swift b/Tests/SwiftLintFrameworkTests/CommandTests.swift index f4e697f321..6ede31b63f 100644 --- a/Tests/SwiftLintFrameworkTests/CommandTests.swift +++ b/Tests/SwiftLintFrameworkTests/CommandTests.swift @@ -1,6 +1,7 @@ // swiftlint:disable file_length import Foundation import SourceKittenFramework +@testable import SwiftLintBuiltInRules @testable import SwiftLintCore import XCTest @@ -14,7 +15,7 @@ private extension Command { } // swiftlint:disable:next type_body_length -class CommandTests: SwiftLintTestCase { +final class CommandTests: SwiftLintTestCase { // MARK: Command Creation func testNoCommandsInEmptyFile() { @@ -149,7 +150,7 @@ class CommandTests: SwiftLintTestCase { modifier: .previous) let expanded = [ Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 0, character: nil), - Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 0, character: .max) + Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 0, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -158,7 +159,7 @@ class CommandTests: SwiftLintTestCase { modifier: .previous) let expanded = [ Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 0, character: nil), - Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 0, character: .max) + Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 0, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -167,7 +168,7 @@ class CommandTests: SwiftLintTestCase { modifier: .previous) let expanded = [ Command(action: .enable, ruleIdentifiers: ["1", "2"], line: 0, character: nil), - Command(action: .disable, ruleIdentifiers: ["1", "2"], line: 0, character: .max) + Command(action: .disable, ruleIdentifiers: ["1", "2"], line: 0, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -179,7 +180,7 @@ class CommandTests: SwiftLintTestCase { modifier: .this) let expanded = [ Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 1, character: nil), - Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 1, character: .max) + Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 1, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -188,7 +189,7 @@ class CommandTests: SwiftLintTestCase { modifier: .this) let expanded = [ Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 1, character: nil), - Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 1, character: .max) + Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 1, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -197,7 +198,7 @@ class CommandTests: SwiftLintTestCase { modifier: .this) let expanded = [ Command(action: .enable, ruleIdentifiers: ["1", "2"], line: 1, character: nil), - Command(action: .disable, ruleIdentifiers: ["1", "2"], line: 1, character: .max) + Command(action: .disable, ruleIdentifiers: ["1", "2"], line: 1, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -209,7 +210,7 @@ class CommandTests: SwiftLintTestCase { modifier: .next) let expanded = [ Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 2, character: nil), - Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 2, character: .max) + Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 2, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -218,7 +219,7 @@ class CommandTests: SwiftLintTestCase { modifier: .next) let expanded = [ Command(action: .enable, ruleIdentifiers: ["rule_id"], line: 2, character: nil), - Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 2, character: .max) + Command(action: .disable, ruleIdentifiers: ["rule_id"], line: 2, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -227,7 +228,7 @@ class CommandTests: SwiftLintTestCase { modifier: .next) let expanded = [ Command(action: .enable, ruleIdentifiers: ["1", "2"], line: 2, character: nil), - Command(action: .disable, ruleIdentifiers: ["1", "2"], line: 2, character: .max) + Command(action: .disable, ruleIdentifiers: ["1", "2"], line: 2, character: .max), ] XCTAssertEqual(command.expand(), expanded) } @@ -237,7 +238,7 @@ class CommandTests: SwiftLintTestCase { func testSuperfluousDisableCommands() { XCTAssertEqual( - violations(Example("// swiftlint:disable nesting\nprint(123)\n")).map { $0.ruleIdentifier }, + violations(Example("// swiftlint:disable nesting\nprint(123)\n")).map(\.ruleIdentifier), ["blanket_disable_command", "superfluous_disable_command"] ) XCTAssertEqual( @@ -296,9 +297,7 @@ class CommandTests: SwiftLintTestCase { func testSuperfluousDisableCommandsIgnoreDelimiter() { let longComment = "Comment with a large number of words that shouldn't register as superfluous" XCTAssertEqual( - violations(Example("// swiftlint:disable nesting - \(longComment)\nprint(123)\n")).map { - $0.ruleIdentifier - }, + violations(Example("// swiftlint:disable nesting - \(longComment)\nprint(123)\n")).map(\.ruleIdentifier), ["blanket_disable_command", "superfluous_disable_command"] ) XCTAssertEqual( @@ -394,7 +393,9 @@ class CommandTests: SwiftLintTestCase { } func testSuperfluousDisableCommandsDisabledOnConfiguration() { - let rulesMode = Configuration.RulesMode.default(disabled: ["superfluous_disable_command"], optIn: []) + let rulesMode = Configuration.RulesMode.defaultConfiguration( + disabled: ["superfluous_disable_command"], optIn: [] + ) let configuration = Configuration(rulesMode: rulesMode) XCTAssertEqual( @@ -454,4 +455,29 @@ class CommandTests: SwiftLintTestCase { [] ) } + + func testSuperfluousDisableCommandsEnabledForAnalyzer() { + let configuration = Configuration( + rulesMode: .defaultConfiguration(disabled: [], optIn: [UnusedDeclarationRule.identifier]) + ) + let violations = violations( + Example(""" + public class Foo { + // swiftlint:disable:next unused_declaration + func foo() -> Int { + 1 + } + // swiftlint:disable:next unused_declaration + func bar() { + foo() + } + } + """), + config: configuration, + requiresFileOnDisk: true + ) + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations.first?.ruleIdentifier, "superfluous_disable_command") + XCTAssertEqual(violations.first?.location.line, 3) + } } diff --git a/Tests/SwiftLintFrameworkTests/CompilerProtocolInitRuleTests.swift b/Tests/SwiftLintFrameworkTests/CompilerProtocolInitRuleTests.swift index eaf29f6a67..68460fd73b 100644 --- a/Tests/SwiftLintFrameworkTests/CompilerProtocolInitRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/CompilerProtocolInitRuleTests.swift @@ -1,8 +1,8 @@ @testable import SwiftLintBuiltInRules import XCTest -class CompilerProtocolInitRuleTests: SwiftLintTestCase { - private let ruleID = CompilerProtocolInitRule.description.identifier +final class CompilerProtocolInitRuleTests: SwiftLintTestCase { + private let ruleID = CompilerProtocolInitRule.identifier func testViolationMessageForExpressibleByIntegerLiteral() throws { let config = try XCTUnwrap(makeConfig(nil, ruleID)) diff --git a/Tests/SwiftLintFrameworkTests/ComputedAccessorsOrderRuleTests.swift b/Tests/SwiftLintFrameworkTests/ComputedAccessorsOrderRuleTests.swift index f5502dfaf3..1066d61010 100644 --- a/Tests/SwiftLintFrameworkTests/ComputedAccessorsOrderRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ComputedAccessorsOrderRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ComputedAccessorsOrderRuleTests: SwiftLintTestCase { +final class ComputedAccessorsOrderRuleTests: SwiftLintTestCase { func testSetGetConfiguration() { let nonTriggeringExamples = [ Example(""" @@ -15,7 +15,7 @@ class ComputedAccessorsOrderRuleTests: SwiftLintTestCase { } } } - """) + """), ] let triggeringExamples = [ Example(""" @@ -29,7 +29,7 @@ class ComputedAccessorsOrderRuleTests: SwiftLintTestCase { } } } - """) + """), ] let description = ComputedAccessorsOrderRule.description @@ -120,7 +120,7 @@ class ComputedAccessorsOrderRuleTests: SwiftLintTestCase { } private func ruleViolations(_ example: Example, ruleConfiguration: Any? = nil) -> [StyleViolation] { - guard let config = makeConfig(ruleConfiguration, ComputedAccessorsOrderRule.description.identifier) else { + guard let config = makeConfig(ruleConfiguration, ComputedAccessorsOrderRule.identifier) else { return [] } diff --git a/Tests/SwiftLintFrameworkTests/ConditionalReturnsOnNewlineRuleTests.swift b/Tests/SwiftLintFrameworkTests/ConditionalReturnsOnNewlineRuleTests.swift index c16ae2fe36..e2ffbbbc1f 100644 --- a/Tests/SwiftLintFrameworkTests/ConditionalReturnsOnNewlineRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ConditionalReturnsOnNewlineRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class ConditionalReturnsOnNewlineRuleTests: SwiftLintTestCase { +final class ConditionalReturnsOnNewlineRuleTests: SwiftLintTestCase { func testConditionalReturnsOnNewlineWithIfOnly() { // Test with `if_only` set to true let nonTriggeringExamples = [ @@ -11,13 +11,13 @@ class ConditionalReturnsOnNewlineRuleTests: SwiftLintTestCase { Example("if textField.returnKeyType == .Next {"), Example("if true { // return }"), Example("/*if true { */ return }"), - Example("guard true else { return }") + Example("guard true else { return }"), ] let triggeringExamples = [ Example("↓if true { return }"), Example("↓if true { break } else { return }"), Example("↓if true { break } else { return }"), - Example("↓if true { return \"YES\" } else { return \"NO\" }") + Example("↓if true { return \"YES\" } else { return \"NO\" }"), ] let description = ConditionalReturnsOnNewlineRule.description diff --git a/Tests/SwiftLintFrameworkTests/ConfigurationAliasesTests.swift b/Tests/SwiftLintFrameworkTests/ConfigurationAliasesTests.swift index 26ac0c28b0..c20e8b90b5 100644 --- a/Tests/SwiftLintFrameworkTests/ConfigurationAliasesTests.swift +++ b/Tests/SwiftLintFrameworkTests/ConfigurationAliasesTests.swift @@ -7,7 +7,7 @@ final class ConfigurationAliasesTests: SwiftLintTestCase { func testConfiguresCorrectlyFromDeprecatedAlias() throws { let ruleConfiguration = [1, 2] let config = ["mock": ruleConfiguration] - let rules = try testRuleList.allRulesWrapped(configurationDict: config).map { $0.rule } + let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.rule) // swiftlint:disable:next xct_specific_matcher XCTAssertTrue(rules == [try RuleWithLevelsMock(configuration: ruleConfiguration)]) } @@ -28,7 +28,7 @@ final class ConfigurationAliasesTests: SwiftLintTestCase { // swiftlint:disable:next force_try let configuration = try! Configuration(dict: ["only_rules": ["mock"]], ruleList: testRuleList) let configuredIdentifiers = configuration.rules.map { - type(of: $0).description.identifier + type(of: $0).identifier } XCTAssertEqual(configuredIdentifiers, ["severity_level_mock"]) } diff --git a/Tests/SwiftLintFrameworkTests/ConfigurationTests+Mock.swift b/Tests/SwiftLintFrameworkTests/ConfigurationTests+Mock.swift index c540a1a565..5b530a3d13 100644 --- a/Tests/SwiftLintFrameworkTests/ConfigurationTests+Mock.swift +++ b/Tests/SwiftLintFrameworkTests/ConfigurationTests+Mock.swift @@ -90,8 +90,8 @@ struct RuleMock: Rule { static let description = RuleDescription(identifier: "RuleMock", name: "", description: "", kind: .style) - init() {} - init(configuration: Any) throws { self.init() } + init() { /* conformance for test */ } + init(configuration _: Any) throws { self.init() } - func validate(file: SwiftLintFile) -> [StyleViolation] { [] } + func validate(file _: SwiftLintFile) -> [StyleViolation] { [] } } diff --git a/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift b/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift index 52d197d4f0..e928221e9b 100644 --- a/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift +++ b/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift @@ -5,15 +5,15 @@ import XCTest // swiftlint:disable file_length private extension Configuration { - func contains(rule: T.Type) -> Bool { - return rules.contains { $0 is T } + func contains(rule _: T.Type) -> Bool { + rules.contains { $0 is T } } } extension ConfigurationTests { // MARK: - Rules Merging func testMerge() { - let config0Merge2 = Mock.Config._0.merged(withChild: Mock.Config._2, rootDirectory: "") + let config0Merge2 = Mock.Config._0.merged(withChild: Mock.Config._2) XCTAssertFalse(Mock.Config._0.contains(rule: ForceCastRule.self)) XCTAssertTrue(Mock.Config._2.contains(rule: ForceCastRule.self)) @@ -25,52 +25,77 @@ extension ConfigurationTests { XCTAssertFalse(Mock.Config._3.contains(rule: TodoRule.self)) XCTAssertFalse( - config0Merge2.merged(withChild: Mock.Config._3, rootDirectory: "").contains(rule: TodoRule.self) + config0Merge2.merged(withChild: Mock.Config._3).contains(rule: TodoRule.self) ) } // MARK: - Merging Aspects func testWarningThresholdMerging() { func configuration(forWarningThreshold warningThreshold: Int?) -> Configuration { - return Configuration( + Configuration( warningThreshold: warningThreshold, reporter: XcodeReporter.identifier ) } XCTAssertEqual(configuration(forWarningThreshold: 3) - .merged(withChild: configuration(forWarningThreshold: 2), rootDirectory: "").warningThreshold, + .merged(withChild: configuration(forWarningThreshold: 2)).warningThreshold, 2) XCTAssertEqual(configuration(forWarningThreshold: nil) - .merged(withChild: configuration(forWarningThreshold: 2), rootDirectory: "").warningThreshold, + .merged(withChild: configuration(forWarningThreshold: 2)).warningThreshold, 2) XCTAssertEqual(configuration(forWarningThreshold: 3) - .merged(withChild: configuration(forWarningThreshold: nil), rootDirectory: "").warningThreshold, + .merged(withChild: configuration(forWarningThreshold: nil)).warningThreshold, 3) XCTAssertNil(configuration(forWarningThreshold: nil) - .merged(withChild: configuration(forWarningThreshold: nil), rootDirectory: "").warningThreshold) + .merged(withChild: configuration(forWarningThreshold: nil)).warningThreshold) } func testOnlyRulesMerging() { - let baseConfiguration = Configuration(rulesMode: .default(disabled: [], - optIn: [ForceTryRule.description.identifier, - ForceCastRule.description.identifier])) - let onlyConfiguration = Configuration(rulesMode: .only([TodoRule.description.identifier])) + let baseConfiguration = Configuration( + rulesMode: .defaultConfiguration( + disabled: [], + optIn: [ + ForceTryRule.identifier, + ForceCastRule.identifier, + ] + ) + ) + let onlyConfiguration = Configuration(rulesMode: .onlyConfiguration([TodoRule.identifier])) XCTAssertTrue(baseConfiguration.contains(rule: TodoRule.self)) XCTAssertEqual(onlyConfiguration.rules.count, 1) XCTAssertTrue(onlyConfiguration.rules[0] is TodoRule) - let mergedConfiguration1 = baseConfiguration.merged(withChild: onlyConfiguration, rootDirectory: "") + let mergedConfiguration1 = baseConfiguration.merged(withChild: onlyConfiguration) XCTAssertEqual(mergedConfiguration1.rules.count, 1) XCTAssertTrue(mergedConfiguration1.rules[0] is TodoRule) // Also test the other way around - let mergedConfiguration2 = onlyConfiguration.merged(withChild: baseConfiguration, rootDirectory: "") + let mergedConfiguration2 = onlyConfiguration.merged(withChild: baseConfiguration) XCTAssertEqual(mergedConfiguration2.rules.count, 3) // 2 opt-ins + 1 from the only rules XCTAssertTrue(mergedConfiguration2.contains(rule: TodoRule.self)) XCTAssertTrue(mergedConfiguration2.contains(rule: ForceCastRule.self)) XCTAssertTrue(mergedConfiguration2.contains(rule: ForceTryRule.self)) } + func testOnlyRuleMerging() { + let ruleIdentifier = TodoRule.identifier + let onlyRuleConfiguration = Configuration.onlyRuleConfiguration(ruleIdentifier) + + let emptyDefaultConfiguration = Configuration.emptyDefaultConfiguration() + let mergedConfiguration1 = onlyRuleConfiguration.merged(withChild: emptyDefaultConfiguration) + XCTAssertEqual(mergedConfiguration1.rules.count, 1) + XCTAssertTrue(mergedConfiguration1.rules[0] is TodoRule) + + let disabledDefaultConfiguration = Configuration.disabledDefaultConfiguration(ruleIdentifier) + let mergedConfiguration2 = onlyRuleConfiguration.merged(withChild: disabledDefaultConfiguration) + XCTAssertTrue(mergedConfiguration2.rules.isEmpty) + + let enabledOnlyConfiguration = Configuration.enabledOnlyConfiguration(ForceTryRule.identifier) + let mergedConfiguration3 = onlyRuleConfiguration.merged(withChild: enabledOnlyConfiguration) + XCTAssertEqual(mergedConfiguration3.rules.count, 1) + XCTAssertTrue(mergedConfiguration3.rules[0] is TodoRule) + } + func testCustomRulesMerging() { let mergedConfiguration = Mock.Config._0CustomRules.merged( withChild: Mock.Config._2CustomRules, @@ -231,7 +256,7 @@ extension ConfigurationTests { } for path in [Mock.Dir.childConfigTest1, Mock.Dir.childConfigTest2] { - FileManager.default.changeCurrentDirectoryPath(path) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) assertEqualExceptForFileGraph( Configuration(configurationFiles: ["main.yml"]), @@ -242,7 +267,7 @@ extension ConfigurationTests { func testValidParentConfig() { for path in [Mock.Dir.parentConfigTest1, Mock.Dir.parentConfigTest2] { - FileManager.default.changeCurrentDirectoryPath(path) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) assertEqualExceptForFileGraph( Configuration(configurationFiles: ["main.yml"]), @@ -257,7 +282,7 @@ extension ConfigurationTests { } for path in [Mock.Dir.childConfigTest1, Mock.Dir.childConfigTest2] { - FileManager.default.changeCurrentDirectoryPath(path) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) assertEqualExceptForFileGraph( Configuration( @@ -275,9 +300,9 @@ extension ConfigurationTests { Mock.Dir.childConfigCycle3, Mock.Dir.parentConfigCycle1, Mock.Dir.parentConfigCycle2, - Mock.Dir.parentConfigCycle3 + Mock.Dir.parentConfigCycle3, ] { - FileManager.default.changeCurrentDirectoryPath(path) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(path)) // If the cycle is properly detected, the config should equal the default config. XCTAssertEqual( @@ -288,7 +313,7 @@ extension ConfigurationTests { } func testCommandLineConfigsCycleDetection() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.childConfigCycle4) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.childConfigCycle4)) // If the cycle is properly detected, the config should equal the default config. assertEqualExceptForFileGraph( @@ -331,23 +356,23 @@ extension ConfigurationTests { TestCase(optedInInParent: false, disabledInParent: false, optedInInChild: true, disabledInChild: true, isEnabled: false), TestCase(optedInInParent: true, disabledInParent: false, optedInInChild: true, disabledInChild: true, isEnabled: false), TestCase(optedInInParent: false, disabledInParent: true, optedInInChild: true, disabledInChild: true, isEnabled: false), - TestCase(optedInInParent: true, disabledInParent: true, optedInInChild: true, disabledInChild: true, isEnabled: false) + TestCase(optedInInParent: true, disabledInParent: true, optedInInChild: true, disabledInChild: true, isEnabled: false), // swiftlint:enable line_length ] XCTAssertEqual(testCases.unique.count, 4 * 4) let ruleType = ImplicitReturnRule.self XCTAssertTrue((ruleType as Any) is any OptInRule.Type) - let ruleIdentifier = ruleType.description.identifier + let ruleIdentifier = ruleType.identifier for testCase in testCases { - let parentConfiguration = Configuration(rulesMode: .default( + let parentConfiguration = Configuration(rulesMode: .defaultConfiguration( disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: testCase.optedInInParent ? [ruleIdentifier] : [] )) - let childConfiguration = Configuration(rulesMode: .default( + let childConfiguration = Configuration(rulesMode: .defaultConfiguration( disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: testCase.optedInInChild ? [ruleIdentifier] : [] )) - let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration, rootDirectory: "") + let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration) let isEnabled = mergedConfiguration.contains(rule: ruleType) XCTAssertEqual(isEnabled, testCase.isEnabled, testCase.message) } @@ -366,20 +391,20 @@ extension ConfigurationTests { TestCase(disabledInParent: false, disabledInChild: false, isEnabled: true), TestCase(disabledInParent: true, disabledInChild: false, isEnabled: false), TestCase(disabledInParent: false, disabledInChild: true, isEnabled: false), - TestCase(disabledInParent: true, disabledInChild: true, isEnabled: false) + TestCase(disabledInParent: true, disabledInChild: true, isEnabled: false), ] XCTAssertEqual(testCases.unique.count, 2 * 2) let ruleType = BlanketDisableCommandRule.self XCTAssertFalse(ruleType is any OptInRule.Type) - let ruleIdentifier = ruleType.description.identifier + let ruleIdentifier = ruleType.identifier for testCase in testCases { let parentConfiguration = Configuration( - rulesMode: .default(disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: []) + rulesMode: .defaultConfiguration(disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: []) ) let childConfiguration = Configuration( - rulesMode: .default(disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: []) + rulesMode: .defaultConfiguration(disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: []) ) - let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration, rootDirectory: "") + let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration) let isEnabled = mergedConfiguration.contains(rule: ruleType) XCTAssertEqual(isEnabled, testCase.isEnabled, testCase.message) } @@ -398,27 +423,128 @@ extension ConfigurationTests { TestCase(optedInInChild: false, disabledInChild: false, isEnabled: true), TestCase(optedInInChild: true, disabledInChild: false, isEnabled: true), TestCase(optedInInChild: false, disabledInChild: true, isEnabled: false), - TestCase(optedInInChild: true, disabledInChild: true, isEnabled: false) + TestCase(optedInInChild: true, disabledInChild: true, isEnabled: false), ] XCTAssertEqual(testCases.unique.count, 2 * 2) let ruleType = ImplicitReturnRule.self XCTAssertTrue((ruleType as Any) is any OptInRule.Type) - let ruleIdentifier = ruleType.description.identifier - let parentConfiguration = Configuration(rulesMode: .only([ruleIdentifier])) + let ruleIdentifier = ruleType.identifier + let parentConfiguration = Configuration(rulesMode: .onlyConfiguration([ruleIdentifier])) for testCase in testCases { - let childConfiguration = Configuration(rulesMode: .default( + let childConfiguration = Configuration(rulesMode: .defaultConfiguration( disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: testCase.optedInInChild ? [ruleIdentifier] : [] )) - let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration, rootDirectory: "") + let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration) let isEnabled = mergedConfiguration.contains(rule: ruleType) XCTAssertEqual(isEnabled, testCase.isEnabled, testCase.message) } } + // MARK: Warnings about configurations for disabled rules + func testDefaultConfigurationDisabledRuleWarnings() { + let optInRuleType = ImplicitReturnRule.self + XCTAssertTrue((optInRuleType as Any) is any OptInRule.Type) + testDefaultConfigurationDisabledRuleWarnings(for: optInRuleType) + + let defaultRuleType = BlockBasedKVORule.self + XCTAssertFalse((defaultRuleType as Any) is any OptInRule.Type) + testDefaultConfigurationDisabledRuleWarnings(for: defaultRuleType) + } + + private func testDefaultConfigurationDisabledRuleWarnings(for ruleType: any Rule.Type) { + let ruleIdentifier = ruleType.identifier + + let parentConfigurations = [ + nil, + Configuration.emptyDefaultConfiguration(), + Configuration.optInDefaultConfiguration(ruleIdentifier), + Configuration.optInDisabledDefaultConfiguration(ruleIdentifier), + Configuration.disabledDefaultConfiguration(ruleIdentifier), + Configuration.emptyOnlyConfiguration(), + Configuration.enabledOnlyConfiguration(ruleIdentifier), + Configuration.allEnabledConfiguration(), + ] + + let configurations = [ + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [])), + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [ruleIdentifier])), + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [ruleIdentifier])), + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [])), + ] + + for parentConfiguration in parentConfigurations { + for configuration in configurations { + testParentConfiguration(parentConfiguration, configuration: configuration, ruleType: ruleType) + } + } + } + + private func testParentConfiguration( + _ parentConfiguration: Configuration?, + configuration: Configuration, + ruleType: any Rule.Type + ) { + guard case .defaultConfiguration(let disabledRules, let optInRules) = configuration.rulesMode else { + XCTFail("Configuration rulesMode was not the default") + return + } + + let mergedConfiguration = parentConfiguration?.merged(withChild: configuration) ?? configuration + let isEnabled = mergedConfiguration.contains(rule: ruleType) + let issue = Configuration.validateConfiguredRuleIsEnabled( + parentConfiguration: parentConfiguration, + disabledRules: disabledRules, + optInRules: optInRules, + ruleType: ruleType + ) + XCTAssertEqual(isEnabled, issue == nil) + guard let issue else { + return + } + let ruleIdentifier = ruleType.identifier + + guard disabledRules.isEmpty, optInRules.isEmpty else { + XCTAssertEqual(issue, Issue.ruleDisabledInDisabledRules(ruleID: ruleIdentifier)) + return + } + + if parentConfiguration == nil || + parentConfiguration == Configuration.emptyDefaultConfiguration() { + XCTAssertEqual(issue, Issue.ruleNotEnabledInOptInRules(ruleID: ruleIdentifier)) + } else if parentConfiguration == Configuration.emptyOnlyConfiguration() { + if ruleType is any OptInRule.Type { + XCTAssertEqual(issue, Issue.ruleNotEnabledInOptInRules(ruleID: ruleIdentifier)) + } else { + XCTAssertEqual(issue, Issue.ruleNotEnabledInParentOnlyRules(ruleID: ruleIdentifier)) + } + } else if parentConfiguration == Configuration.optInDisabledDefaultConfiguration(ruleIdentifier) || + parentConfiguration == Configuration.disabledDefaultConfiguration(ruleIdentifier) { + XCTAssertEqual(issue, Issue.ruleDisabledInParentConfiguration(ruleID: ruleIdentifier)) + } + } + + func testOnlyConfigurationDisabledRulesWarnings() { + let optInRuleType = ImplicitReturnRule.self + XCTAssertTrue((optInRuleType as Any) is any OptInRule.Type) + testOnlyConfigurationDisabledRulesWarnings(ruleType: optInRuleType) + + let defaultRuleType = BlockBasedKVORule.self + XCTAssertFalse((defaultRuleType as Any) is any OptInRule.Type) + testOnlyConfigurationDisabledRulesWarnings(ruleType: defaultRuleType) + } + + private func testOnlyConfigurationDisabledRulesWarnings(ruleType: any Rule.Type) { + let issue = Configuration.validateConfiguredRuleIsEnabled(onlyRules: [], ruleType: ruleType) + XCTAssertEqual(issue, Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier)) + XCTAssertNil( + Configuration.validateConfiguredRuleIsEnabled(onlyRules: [ruleType.identifier], ruleType: ruleType) + ) + } + // MARK: - Remote Configs func testValidRemoteChildConfig() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigChild) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigChild)) assertEqualExceptForFileGraph( Configuration( @@ -429,7 +555,7 @@ extension ConfigurationTests { included: - Test/Test1/Test/Test - Test/Test2/Test/Test - """ + """, ] ), Configuration(configurationFiles: ["expected.yml"]) @@ -437,7 +563,7 @@ extension ConfigurationTests { } func testValidRemoteParentConfig() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigParent) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigParent)) assertEqualExceptForFileGraph( Configuration( @@ -454,7 +580,7 @@ extension ConfigurationTests { - Test/Test2/Test line_length: 80 - """ + """, ] ), Configuration(configurationFiles: ["expected.yml"]) @@ -462,19 +588,19 @@ extension ConfigurationTests { } func testsRemoteConfigNotAllowedToReferenceLocalConfig() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigLocalRef) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigLocalRef)) // If the remote file is not allowed to reference a local file, the config should equal the default config. XCTAssertEqual( Configuration( - configurationFiles: [], // not specifiying a file means the .swiftlint.yml will be used + configurationFiles: [], // not specifying a file means the .swiftlint.yml will be used mockedNetworkResults: [ "https://www.mock.com": """ line_length: 60 child_config: child2.yml - """ + """, ] ), Configuration() @@ -482,17 +608,17 @@ extension ConfigurationTests { } func testRemoteConfigCycleDetection() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigCycle) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.remoteConfigCycle)) // If the cycle is properly detected, the config should equal the default config. XCTAssertEqual( Configuration( - configurationFiles: [], // not specifiying a file means the .swiftlint.yml will be used + configurationFiles: [], // not specifying a file means the .swiftlint.yml will be used mockedNetworkResults: [ "https://www.mock.com": """ child_config: https://www.mock.com - """ + """, ] ), Configuration() @@ -509,13 +635,17 @@ extension ConfigurationTests { ) XCTAssertEqual( - configuration1.rules.map { type(of: $0).description.identifier }, - configuration2.rules.map { type(of: $0).description.identifier } + configuration1.rules.map { type(of: $0).identifier }, + configuration2.rules.map { type(of: $0).identifier } ) XCTAssertEqual( - Set(configuration1.rulesWrapper.allRulesWrapped.map { $0.rule.configurationDescription.oneLiner() }), - Set(configuration2.rulesWrapper.allRulesWrapped.map { $0.rule.configurationDescription.oneLiner() }) + Set(configuration1.rulesWrapper.allRulesWrapped.map { + $0.rule.createConfigurationDescription().oneLiner() + }), + Set(configuration2.rulesWrapper.allRulesWrapped.map { + $0.rule.createConfigurationDescription().oneLiner() + }) ) XCTAssertEqual(Set(configuration1.includedPaths), Set(configuration2.includedPaths)) @@ -523,3 +653,26 @@ extension ConfigurationTests { XCTAssertEqual(Set(configuration1.excludedPaths), Set(configuration2.excludedPaths)) } } + +private extension Configuration { + static func emptyDefaultConfiguration() -> Self { + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [])) + } + static func optInDefaultConfiguration(_ ruleIdentifier: String) -> Self { + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [ruleIdentifier])) + } + static func optInDisabledDefaultConfiguration(_ ruleIdentifier: String) -> Self { + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [ruleIdentifier])) + } + static func disabledDefaultConfiguration(_ ruleIdentifier: String) -> Self { + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [])) + } + static func emptyOnlyConfiguration() -> Self { Configuration(rulesMode: .onlyConfiguration([])) } + static func enabledOnlyConfiguration(_ ruleIdentifier: String) -> Self { + Configuration(rulesMode: .onlyConfiguration([ruleIdentifier])) + } + static func allEnabledConfiguration() -> Self { Configuration(rulesMode: .allCommandLine)} + static func onlyRuleConfiguration(_ ruleIdentifier: String) -> Self { + Configuration(rulesMode: .onlyCommandLine([ruleIdentifier])) + } +} diff --git a/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift index 4718fc728f..9fd493a068 100644 --- a/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift @@ -5,22 +5,22 @@ import XCTest // swiftlint:disable file_length -private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map({ $0.0 }) +private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0) -class ConfigurationTests: SwiftLintTestCase { +final class ConfigurationTests: SwiftLintTestCase { // MARK: Setup & Teardown - private var previousWorkingDir: String! + private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional override func setUp() { super.setUp() Configuration.resetCache() previousWorkingDir = FileManager.default.currentDirectoryPath - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) } override func tearDown() { super.tearDown() - FileManager.default.changeCurrentDirectoryPath(previousWorkingDir) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(previousWorkingDir)) } // MARK: Tests @@ -37,7 +37,7 @@ class ConfigurationTests: SwiftLintTestCase { func testNoConfiguration() { // Change to a folder where there is no `.swiftlint.yml` - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.emptyFolder) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.emptyFolder)) // Test whether the default configuration is used if there is no `.swiftlint.yml` or other config file XCTAssertEqual(Configuration(configurationFiles: []), Configuration.default) @@ -56,6 +56,10 @@ class ConfigurationTests: SwiftLintTestCase { XCTAssertEqual(reporterFrom(identifier: config.reporter).identifier, "xcode") XCTAssertFalse(config.allowZeroLintableFiles) XCTAssertFalse(config.strict) + XCTAssertFalse(config.lenient) + XCTAssertNil(config.baseline) + XCTAssertNil(config.writeBaseline) + XCTAssertFalse(config.checkForUpdates) } func testInitWithRelativePathAndRootPath() { @@ -70,6 +74,8 @@ class ConfigurationTests: SwiftLintTestCase { XCTAssertEqual(config.reporter, expectedConfig.reporter) XCTAssertTrue(config.allowZeroLintableFiles) XCTAssertTrue(config.strict) + XCTAssertNotNil(config.baseline) + XCTAssertNotNil(config.writeBaseline) } func testEnableAllRulesConfiguration() throws { @@ -82,12 +88,36 @@ class ConfigurationTests: SwiftLintTestCase { XCTAssertEqual(configuration.rules.count, RuleRegistry.shared.list.list.count) } + func testOnlyRule() throws { + let configuration = try Configuration( + dict: [:], + onlyRule: ["nesting"], + cachePath: nil + ) + + XCTAssertEqual(configuration.rules.count, 1) + } + + func testOnlyRuleMultiple() throws { + let onlyRuleIdentifiers = ["nesting", "todo"].sorted() + let configuration = try Configuration( + dict: ["only_rules": "line_length"], + onlyRule: onlyRuleIdentifiers, + cachePath: nil + ) + XCTAssertEqual(onlyRuleIdentifiers, configuration.enabledRuleIdentifiers) + + let childConfiguration = try Configuration(dict: ["disabled_rules": onlyRuleIdentifiers.last ?? ""]) + let mergedConfiguration = configuration.merged(withChild: childConfiguration) + XCTAssertEqual(onlyRuleIdentifiers.dropLast(), mergedConfiguration.enabledRuleIdentifiers) + } + func testOnlyRules() throws { let only = ["nesting", "todo"] let config = try Configuration(dict: ["only_rules": only]) let configuredIdentifiers = config.rules.map { - type(of: $0).description.identifier + type(of: $0).identifier }.sorted() XCTAssertEqual(only, configuredIdentifiers) } @@ -126,11 +156,11 @@ class ConfigurationTests: SwiftLintTestCase { let only = ["nesting", "todo"] let enabledRulesConfigDict = [ "opt_in_rules": ["line_length"], - "only_rules": only + "only_rules": only, ] let disabledRulesConfigDict = [ "disabled_rules": ["identifier_name"], - "only_rules": only + "only_rules": only, ] let combinedRulesConfigDict = enabledRulesConfigDict.reduce(into: disabledRulesConfigDict) { $0[$1.0] = $1.1 } var configuration = try? Configuration(dict: enabledRulesConfigDict) @@ -149,7 +179,7 @@ class ConfigurationTests: SwiftLintTestCase { let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys .filter({ !(["nesting", "todo"] + optInRules).contains($0) })) let configuredIdentifiers = Set(disabledConfig.rules.map { - type(of: $0).description.identifier + type(of: $0).identifier }) XCTAssertEqual(expectedIdentifiers, configuredIdentifiers) } @@ -165,10 +195,7 @@ class ConfigurationTests: SwiftLintTestCase { "initializing Configuration with valid rules in YAML string should succeed") let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys .filter({ !([validRule] + optInRules).contains($0) })) - let configuredIdentifiers = Set(configuration.rules.map { - type(of: $0).description.identifier - }) - XCTAssertEqual(expectedIdentifiers, configuredIdentifiers) + XCTAssertEqual(expectedIdentifiers, Set(configuration.enabledRuleIdentifiers)) } func testDuplicatedRules() { @@ -179,7 +206,7 @@ class ConfigurationTests: SwiftLintTestCase { let duplicateConfig2 = try? Configuration(dict: ["opt_in_rules": [optInRules.first!, optInRules.first!]]) XCTAssertEqual( - duplicateConfig2?.rules.filter { type(of: $0).description.identifier == optInRules.first! }.count, 1, + duplicateConfig2?.rules.filter { type(of: $0).identifier == optInRules.first! }.count, 1, "duplicate rules should be removed when initializing Configuration" ) @@ -195,7 +222,7 @@ class ConfigurationTests: SwiftLintTestCase { return } - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level1) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level1)) // The included path "File.swift" should be put relative to the configuration file // (~> Resources/ProjectMock/File.swift) and not relative to the path where @@ -215,7 +242,7 @@ class ConfigurationTests: SwiftLintTestCase { func testIncludedExcludedRelativeLocationLevel0() { // Same as testIncludedPathRelatedToConfigurationFileLocationLevel1(), // but run from the directory the config file resides in - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml"]) let actualIncludedPath = configuration.includedPaths.first!.bridge() .absolutePathRepresentation(rootDirectory: configuration.rootDirectory) @@ -229,12 +256,15 @@ class ConfigurationTests: SwiftLintTestCase { } private class TestFileManager: LintableFileManager { - func filesToLint(inPath path: String, rootDirectory: String? = nil) -> [String] { + func filesToLint(inPath path: String, rootDirectory _: String? = nil) -> [String] { var filesToLint: [String] = [] switch path { - case "directory": filesToLint = ["directory/File1.swift", "directory/File2.swift", - "directory/excluded/Excluded.swift", - "directory/ExcludedFile.swift"] + case "directory": filesToLint = [ + "directory/File1.swift", + "directory/File2.swift", + "directory/excluded/Excluded.swift", + "directory/ExcludedFile.swift", + ] case "directory/excluded": filesToLint = ["directory/excluded/Excluded.swift"] case "directory/ExcludedFile.swift": filesToLint = ["directory/ExcludedFile.swift"] default: XCTFail("Should not be called with path \(path)") @@ -242,8 +272,8 @@ class ConfigurationTests: SwiftLintTestCase { return filesToLint.absolutePathsStandardized() } - func modificationDate(forFileAtPath path: String) -> Date? { - return nil + func modificationDate(forFileAtPath _: String) -> Date? { + nil } func isFile(atPath path: String) -> Bool { @@ -253,9 +283,10 @@ class ConfigurationTests: SwiftLintTestCase { func testExcludedPaths() { let fileManager = TestFileManager() - let configuration = Configuration(includedPaths: ["directory"], - excludedPaths: ["directory/excluded", - "directory/ExcludedFile.swift"]) + let configuration = Configuration( + includedPaths: ["directory"], + excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"] + ) let excludedPaths = configuration.excludedPaths(fileManager: fileManager) let paths = configuration.lintablePaths(inPath: "", @@ -319,14 +350,14 @@ class ConfigurationTests: SwiftLintTestCase { let expectedFilenames = [ "DirectoryLevel1.swift", "Level0.swift", "Level1.swift", "Level2.swift", "Level3.swift", - "Main.swift", "Sub.swift" + "Main.swift", "Sub.swift", ] XCTAssertEqual(Set(expectedFilenames), Set(filenames)) } func testGlobIncludePaths() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(includedPaths: ["**/Level2"]) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, @@ -405,15 +436,15 @@ class ConfigurationTests: SwiftLintTestCase { func testConfiguresCorrectlyFromDict() throws { let ruleConfiguration = [1, 2] - let config = [RuleWithLevelsMock.description.identifier: ruleConfiguration] - let rules = try testRuleList.allRulesWrapped(configurationDict: config).map { $0.rule } + let config = [RuleWithLevelsMock.identifier: ruleConfiguration] + let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.rule) // swiftlint:disable:next xct_specific_matcher XCTAssertTrue(rules == [try RuleWithLevelsMock(configuration: ruleConfiguration)]) } func testConfigureFallsBackCorrectly() throws { - let config = [RuleWithLevelsMock.description.identifier: ["a", "b"]] - let rules = try testRuleList.allRulesWrapped(configurationDict: config).map { $0.rule } + let config = [RuleWithLevelsMock.identifier: ["a", "b"]] + let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.rule) // swiftlint:disable:next xct_specific_matcher XCTAssertTrue(rules == [RuleWithLevelsMock()]) } @@ -427,15 +458,38 @@ class ConfigurationTests: SwiftLintTestCase { let configuration = try Configuration(dict: ["strict": true]) XCTAssertTrue(configuration.strict) } + + func testLenient() throws { + let configuration = try Configuration(dict: ["lenient": true]) + XCTAssertTrue(configuration.lenient) + } + + func testBaseline() throws { + let baselinePath = "Baseline.json" + let configuration = try Configuration(dict: ["baseline": baselinePath]) + XCTAssertEqual(configuration.baseline, baselinePath) + } + + func testWriteBaseline() throws { + let baselinePath = "Baseline.json" + let configuration = try Configuration(dict: ["write_baseline": baselinePath]) + XCTAssertEqual(configuration.writeBaseline, baselinePath) + } + + func testCheckForUpdates() throws { + let configuration = try Configuration(dict: ["check_for_updates": true]) + XCTAssertTrue(configuration.checkForUpdates) + } } // MARK: - ExcludeByPrefix option tests extension ConfigurationTests { func testExcludeByPrefixExcludedPaths() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) - let configuration = Configuration(includedPaths: ["Level1"], - excludedPaths: ["Level1/Level1.swift", - "Level1/Level2/Level3"]) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) + let configuration = Configuration( + includedPaths: ["Level1"], + excludedPaths: ["Level1/Level1.swift", "Level1/Level2/Level3"] + ) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, excludeBy: .prefix) @@ -444,7 +498,7 @@ extension ConfigurationTests { } func testExcludeByPrefixForceExcludesFile() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"]) let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift", forceExclude: true, @@ -453,7 +507,7 @@ extension ConfigurationTests { } func testExcludeByPrefixForceExcludesFileNotPresentInExcluded() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(includedPaths: ["Level1"], excludedPaths: ["Level1/Level1.swift"]) let paths = configuration.lintablePaths(inPath: "Level1", @@ -464,7 +518,7 @@ extension ConfigurationTests { } func testExcludeByPrefixForceExcludesDirectory() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration( excludedPaths: [ "Level1/Level2", "Directory.swift", "ChildConfig", "ParentConfig", "NestedConfig" @@ -478,7 +532,7 @@ extension ConfigurationTests { } func testExcludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration( excludedPaths: [ "Level1", "Directory.swift/DirectoryLevel1.swift", "ChildConfig", "ParentConfig", "NestedConfig" @@ -492,7 +546,7 @@ extension ConfigurationTests { } func testExcludeByPrefixGlobExcludePaths() { - FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration( includedPaths: ["Level1"], excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"]) @@ -555,3 +609,11 @@ private extension Sequence where Element == String { map { $0.absolutePathStandardized() } } } + +private extension Configuration { + var enabledRuleIdentifiers: [String] { + rules.map { + type(of: $0).identifier + }.sorted() + } +} diff --git a/Tests/SwiftLintFrameworkTests/ContainsOverFirstNotNilRuleTests.swift b/Tests/SwiftLintFrameworkTests/ContainsOverFirstNotNilRuleTests.swift index 920a20bc49..83c162fba3 100644 --- a/Tests/SwiftLintFrameworkTests/ContainsOverFirstNotNilRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ContainsOverFirstNotNilRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ContainsOverFirstNotNilRuleTests: SwiftLintTestCase { +final class ContainsOverFirstNotNilRuleTests: SwiftLintTestCase { func testFirstReason() { let example = Example("↓myList.first { $0 % 2 == 0 } != nil") let violations = self.violations(example) @@ -21,7 +21,7 @@ class ContainsOverFirstNotNilRuleTests: SwiftLintTestCase { // MARK: - Private private func violations(_ example: Example, config: Any? = nil) -> [StyleViolation] { - guard let config = makeConfig(config, ContainsOverFirstNotNilRule.description.identifier) else { + guard let config = makeConfig(config, ContainsOverFirstNotNilRule.identifier) else { return [] } diff --git a/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift b/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift index 94ddf5e49e..4a4f11006c 100644 --- a/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift @@ -2,8 +2,13 @@ import SourceKittenFramework @testable import SwiftLintCore import XCTest -class CustomRulesTests: SwiftLintTestCase { - typealias Configuration = RegexConfiguration +// swiftlint:disable file_length +// swiftlint:disable:next type_body_length +final class CustomRulesTests: SwiftLintTestCase { + private typealias Configuration = RegexConfiguration + + private var testFile: SwiftLintFile { SwiftLintFile(path: "\(testResourcesPath)/test.txt")! } + func testCustomRuleConfigurationSetsCorrectlyWithMatchKinds() { let configDict = [ "my_custom_rule": [ @@ -11,8 +16,8 @@ class CustomRulesTests: SwiftLintTestCase { "message": "Message", "regex": "regex", "match_kinds": "comment", - "severity": "error" - ] + "severity": "error", + ], ] var comp = Configuration(identifier: "my_custom_rule") comp.name = "MyCustomRule" @@ -38,8 +43,8 @@ class CustomRulesTests: SwiftLintTestCase { "message": "Message", "regex": "regex", "excluded_match_kinds": "comment", - "severity": "error" - ] + "severity": "error", + ], ] var comp = Configuration(identifier: "my_custom_rule") comp.name = "MyCustomRule" @@ -61,7 +66,7 @@ class CustomRulesTests: SwiftLintTestCase { func testCustomRuleConfigurationThrows() { let config = 17 var customRulesConfig = CustomRulesConfiguration() - checkError(Issue.unknownConfiguration(ruleID: CustomRules.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: CustomRules.identifier)) { try customRulesConfig.apply(configuration: config) } } @@ -73,7 +78,7 @@ class CustomRulesTests: SwiftLintTestCase { "regex": "regex", "match_kinds": "comment", "excluded_match_kinds": "argument", - "severity": "error" + "severity": "error", ] var configuration = Configuration(identifier: "my_custom_rule") @@ -87,19 +92,21 @@ class CustomRulesTests: SwiftLintTestCase { func testCustomRuleConfigurationIgnoreInvalidRules() throws { let configDict = [ - "my_custom_rule": ["name": "MyCustomRule", - "message": "Message", - "regex": "regex", - "match_kinds": "comment", - "severity": "error"], - "invalid_rule": ["name": "InvalidRule"] // missing `regex` + "my_custom_rule": [ + "name": "MyCustomRule", + "message": "Message", + "regex": "regex", + "match_kinds": "comment", + "severity": "error", + ], + "invalid_rule": ["name": "InvalidRule"], // missing `regex` ] var customRulesConfig = CustomRulesConfiguration() try customRulesConfig.apply(configuration: configDict) XCTAssertEqual(customRulesConfig.customRuleConfigurations.count, 1) - let identifier = customRulesConfig.customRuleConfigurations.first?.description.identifier + let identifier = customRulesConfig.customRuleConfigurations.first?.identifier XCTAssertEqual(identifier, "my_custom_rule") } @@ -107,33 +114,51 @@ class CustomRulesTests: SwiftLintTestCase { let (regexConfig, customRules) = getCustomRules() let file = SwiftLintFile(contents: "// My file with\n// a pattern") - XCTAssertEqual(customRules.validate(file: file), - [StyleViolation(ruleDescription: regexConfig.description, - severity: .warning, - location: Location(file: nil, line: 2, character: 6), - reason: regexConfig.message)]) + XCTAssertEqual( + customRules.validate(file: file), + [ + StyleViolation( + ruleDescription: regexConfig.description, + severity: .warning, + location: Location(file: nil, line: 2, character: 6), + reason: regexConfig.message + ), + ] + ) } - func testLocalDisableCustomRule() { - let (_, customRules) = getCustomRules() - let file = SwiftLintFile(contents: "//swiftlint:disable custom \n// file with a pattern") - XCTAssertEqual(customRules.validate(file: file), []) + func testLocalDisableCustomRule() throws { + let customRules: [String: Any] = [ + "custom": [ + "regex": "pattern", + "match_kinds": "comment", + ], + ] + let example = Example("//swiftlint:disable custom \n// file with a pattern") + let violations = try violations(forExample: example, customRules: customRules) + XCTAssertTrue(violations.isEmpty) } func testLocalDisableCustomRuleWithMultipleRules() { let (configs, customRules) = getCustomRulesWithTwoRules() let file = SwiftLintFile(contents: "//swiftlint:disable \(configs.1.identifier) \n// file with a pattern") - XCTAssertEqual(customRules.validate(file: file), - [StyleViolation(ruleDescription: configs.0.description, - severity: .warning, - location: Location(file: nil, line: 2, character: 16), - reason: configs.0.message)]) + XCTAssertEqual( + customRules.validate(file: file), + [ + StyleViolation( + ruleDescription: configs.0.description, + severity: .warning, + location: Location(file: nil, line: 2, character: 16), + reason: configs.0.message + ), + ] + ) } func testCustomRulesIncludedDefault() { // Violation detected when included is omitted. let (_, customRules) = getCustomRules() - let violations = customRules.validate(file: getTestTextFile()) + let violations = customRules.validate(file: testFile) XCTAssertEqual(violations.count, 1) } @@ -144,8 +169,8 @@ class CustomRulesTests: SwiftLintTestCase { customRuleConfiguration.customRuleConfigurations = [regexConfig] customRules.configuration = customRuleConfiguration - let violations = customRules.validate(file: getTestTextFile()) - XCTAssertEqual(violations.count, 0) + let violations = customRules.validate(file: testFile) + XCTAssertTrue(violations.isEmpty) } func testCustomRulesExcludedExcludesFile() { @@ -155,8 +180,8 @@ class CustomRulesTests: SwiftLintTestCase { customRuleConfiguration.customRuleConfigurations = [regexConfig] customRules.configuration = customRuleConfiguration - let violations = customRules.validate(file: getTestTextFile()) - XCTAssertEqual(violations.count, 0) + let violations = customRules.validate(file: testFile) + XCTAssertTrue(violations.isEmpty) } func testCustomRulesExcludedArrayExcludesFile() { @@ -166,69 +191,386 @@ class CustomRulesTests: SwiftLintTestCase { customRuleConfiguration.customRuleConfigurations = [regexConfig] customRules.configuration = customRuleConfiguration - let violations = customRules.validate(file: getTestTextFile()) - XCTAssertEqual(violations.count, 0) + let violations = customRules.validate(file: testFile) + XCTAssertTrue(violations.isEmpty) } func testCustomRulesCaptureGroup() { - let (_, customRules) = getCustomRules(["regex": #"\ba\s+(\w+)"#, - "capture_group": 1]) - let violations = customRules.validate(file: getTestTextFile()) + let (_, customRules) = getCustomRules([ + "regex": #"\ba\s+(\w+)"#, + "capture_group": 1, + ]) + let violations = customRules.validate(file: testFile) XCTAssertEqual(violations.count, 1) XCTAssertEqual(violations[0].location.line, 2) XCTAssertEqual(violations[0].location.character, 6) } - private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { - var config: [String: Any] = ["regex": "pattern", - "match_kinds": "comment"] - extraConfig.forEach { config[$0] = $1 } + // MARK: - superfluous_disable_command support - var regexConfig = RegexConfiguration(identifier: "custom") - do { - try regexConfig.apply(configuration: config) - } catch { - XCTFail("Failed regex config") - } + func testCustomRulesTriggersSuperfluousDisableCommand() throws { + let customRuleIdentifier = "forbidden" + let customRules: [String: Any] = [ + customRuleIdentifier: [ + "regex": "FORBIDDEN", + ], + ] + let example = Example(""" + // swiftlint:disable:next custom_rules + let ALLOWED = 2 + """) - var customRuleConfiguration = CustomRulesConfiguration() - customRuleConfiguration.customRuleConfigurations = [regexConfig] + let violations = try violations(forExample: example, customRules: customRules) + XCTAssertEqual(violations.count, 1) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "custom_rules")) + } - var customRules = CustomRules() - customRules.configuration = customRuleConfiguration + func testSpecificCustomRuleTriggersSuperfluousDisableCommand() throws { + let customRuleIdentifier = "forbidden" + let customRules: [String: Any] = [ + customRuleIdentifier: [ + "regex": "FORBIDDEN", + ], + ] + + let example = Example(""" + // swiftlint:disable:next \(customRuleIdentifier) + let ALLOWED = 2 + """) + + let violations = try violations(forExample: example, customRules: customRules) + XCTAssertEqual(violations.count, 1) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: customRuleIdentifier)) + } + + func testSpecificAndCustomRulesTriggersSuperfluousDisableCommand() throws { + let customRuleIdentifier = "forbidden" + let customRules: [String: Any] = [ + customRuleIdentifier: [ + "regex": "FORBIDDEN", + ], + ] + + let example = Example(""" + // swiftlint:disable:next custom_rules \(customRuleIdentifier) + let ALLOWED = 2 + """) + + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 2) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "custom_rules")) + XCTAssertTrue(violations[1].isSuperfluousDisableCommandViolation(for: "\(customRuleIdentifier)")) + } + + func testCustomRulesViolationAndViolationOfSuperfluousDisableCommand() throws { + let customRuleIdentifier = "forbidden" + let customRules: [String: Any] = [ + customRuleIdentifier: [ + "regex": "FORBIDDEN", + ], + ] + + let example = Example(""" + let FORBIDDEN = 1 + // swiftlint:disable:next \(customRuleIdentifier) + let ALLOWED = 2 + """) + + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].ruleIdentifier, customRuleIdentifier) + XCTAssertTrue(violations[1].isSuperfluousDisableCommandViolation(for: customRuleIdentifier)) + } + + func testDisablingCustomRulesDoesNotTriggerSuperfluousDisableCommand() throws { + let customRules: [String: Any] = [ + "forbidden": [ + "regex": "FORBIDDEN", + ], + ] + + let example = Example(""" + // swiftlint:disable:next custom_rules + let FORBIDDEN = 1 + """) + + XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty) + } + + func testMultipleSpecificCustomRulesTriggersSuperfluousDisableCommand() throws { + let customRules = [ + "forbidden": [ + "regex": "FORBIDDEN", + ], + "forbidden2": [ + "regex": "FORBIDDEN2", + ], + ] + let example = Example(""" + // swiftlint:disable:next forbidden forbidden2 + let ALLOWED = 2 + """) + + let violations = try self.violations(forExample: example, customRules: customRules) + XCTAssertEqual(violations.count, 2) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "forbidden")) + XCTAssertTrue(violations[1].isSuperfluousDisableCommandViolation(for: "forbidden2")) + } + + func testUnviolatedSpecificCustomRulesTriggersSuperfluousDisableCommand() throws { + let customRules = [ + "forbidden": [ + "regex": "FORBIDDEN", + ], + "forbidden2": [ + "regex": "FORBIDDEN2", + ], + ] + let example = Example(""" + // swiftlint:disable:next forbidden forbidden2 + let FORBIDDEN = 1 + """) + + let violations = try self.violations(forExample: example, customRules: customRules) + XCTAssertEqual(violations.count, 1) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "forbidden2")) + } + + func testViolatedSpecificAndGeneralCustomRulesTriggersSuperfluousDisableCommand() throws { + let customRules = [ + "forbidden": [ + "regex": "FORBIDDEN", + ], + "forbidden2": [ + "regex": "FORBIDDEN2", + ], + ] + let example = Example(""" + // swiftlint:disable:next forbidden forbidden2 custom_rules + let FORBIDDEN = 1 + """) + + let violations = try self.violations(forExample: example, customRules: customRules) + XCTAssertEqual(violations.count, 1) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "forbidden2")) + } + + func testSuperfluousDisableCommandWithMultipleCustomRules() throws { + let customRules: [String: Any] = [ + "custom1": [ + "regex": "pattern", + "match_kinds": "comment", + ], + "custom2": [ + "regex": "10", + "match_kinds": "number", + ], + "custom3": [ + "regex": "100", + "match_kinds": "number", + ], + ] + + let example = Example( + """ + // swiftlint:disable custom1 custom3 + return 10 + """ + ) + + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 3) + XCTAssertEqual(violations[0].ruleIdentifier, "custom2") + XCTAssertTrue(violations[1].isSuperfluousDisableCommandViolation(for: "custom1")) + XCTAssertTrue(violations[2].isSuperfluousDisableCommandViolation(for: "custom3")) + } + + func testViolatedCustomRuleDoesNotTriggerSuperfluousDisableCommand() throws { + let customRules: [String: Any] = [ + "dont_print": [ + "regex": "print\\(" + ], + ] + let example = Example(""" + // swiftlint:disable:next dont_print + print("Hello, world") + """) + XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty) + } + + func testDisableAllDoesNotTriggerSuperfluousDisableCommand() throws { + let customRules: [String: Any] = [ + "dont_print": [ + "regex": "print\\(" + ], + ] + let example = Example(""" + // swiftlint:disable:next all + print("Hello, world") + """) + XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty) + } + + func testDisableAllAndDisableSpecificCustomRuleDoesNotTriggerSuperfluousDisableCommand() throws { + let customRules: [String: Any] = [ + "dont_print": [ + "regex": "print\\(" + ], + ] + let example = Example(""" + // swiftlint:disable:next all dont_print + print("Hello, world") + """) + XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty) + } + + func testNestedCustomRuleDisablesDoNotTriggerSuperfluousDisableCommand() throws { + let customRules: [String: Any] = [ + "rule1": [ + "regex": "pattern1" + ], + "rule2": [ + "regex": "pattern2" + ], + ] + let example = Example(""" + // swiftlint:disable rule1 + // swiftlint:disable rule2 + let pattern2 = "" + // swiftlint:enable rule2 + let pattern1 = "" + // swiftlint:enable rule1 + """) + XCTAssertTrue(try violations(forExample: example, customRules: customRules).isEmpty) + } + + func testNestedAndOverlappingCustomRuleDisables() throws { + let customRules: [String: Any] = [ + "rule1": [ + "regex": "pattern1" + ], + "rule2": [ + "regex": "pattern2" + ], + "rule3": [ + "regex": "pattern3" + ], + ] + let example = Example(""" + // swiftlint:disable rule1 + // swiftlint:disable rule2 + // swiftlint:disable rule3 + let pattern2 = "" + // swiftlint:enable rule2 + // swiftlint:enable rule3 + let pattern1 = "" + // swiftlint:enable rule1 + """) + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 1) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "rule3")) + } + + func testSuperfluousDisableRuleOrder() throws { + let customRules: [String: Any] = [ + "rule1": [ + "regex": "pattern1" + ], + "rule2": [ + "regex": "pattern2" + ], + "rule3": [ + "regex": "pattern3" + ], + ] + let example = Example(""" + // swiftlint:disable rule1 + // swiftlint:disable rule2 rule3 + // swiftlint:enable rule3 rule2 + // swiftlint:disable rule2 + // swiftlint:enable rule1 + // swiftlint:enable rule2 + """) + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 4) + XCTAssertTrue(violations[0].isSuperfluousDisableCommandViolation(for: "rule1")) + XCTAssertTrue(violations[1].isSuperfluousDisableCommandViolation(for: "rule2")) + XCTAssertTrue(violations[2].isSuperfluousDisableCommandViolation(for: "rule3")) + XCTAssertTrue(violations[3].isSuperfluousDisableCommandViolation(for: "rule2")) + } + + // MARK: - Private + + private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { + var config: [String: Any] = [ + "regex": "pattern", + "match_kinds": "comment", + ] + extraConfig.forEach { config[$0] = $1 } + + let regexConfig = configuration(withIdentifier: "custom", configurationDict: config) + let customRules = customRules(withConfigurations: [regexConfig]) return (regexConfig, customRules) } private func getCustomRulesWithTwoRules() -> ((Configuration, Configuration), CustomRules) { - let config1 = ["regex": "pattern", - "match_kinds": "comment"] + let config1 = [ + "regex": "pattern", + "match_kinds": "comment", + ] - var regexConfig1 = Configuration(identifier: "custom1") - do { - try regexConfig1.apply(configuration: config1) - } catch { - XCTFail("Failed regex config") - } + let regexConfig1 = configuration(withIdentifier: "custom1", configurationDict: config1) - let config2 = ["regex": "something", - "match_kinds": "comment"] + let config2 = [ + "regex": "something", + "match_kinds": "comment", + ] + + let regexConfig2 = configuration(withIdentifier: "custom2", configurationDict: config2) + + let customRules = customRules(withConfigurations: [regexConfig1, regexConfig2]) + return ((regexConfig1, regexConfig2), customRules) + } + + private func violations(forExample example: Example, customRules: [String: Any]) throws -> [StyleViolation] { + let configDict: [String: Any] = [ + "only_rules": ["custom_rules", "superfluous_disable_command"], + "custom_rules": customRules, + ] + let configuration = try SwiftLintCore.Configuration(dict: configDict) + return SwiftLintTestHelpers.violations( + example.skipWrappingInCommentTest(), + config: configuration + ) + } - var regexConfig2 = Configuration(identifier: "custom2") + private func configuration(withIdentifier identifier: String, configurationDict: [String: Any]) -> Configuration { + var regexConfig = Configuration(identifier: identifier) do { - try regexConfig2.apply(configuration: config2) + try regexConfig.apply(configuration: configurationDict) } catch { XCTFail("Failed regex config") } + return regexConfig + } + private func customRules(withConfigurations configurations: [Configuration]) -> CustomRules { var customRuleConfiguration = CustomRulesConfiguration() - customRuleConfiguration.customRuleConfigurations = [regexConfig1, regexConfig2] - + customRuleConfiguration.customRuleConfigurations = configurations var customRules = CustomRules() customRules.configuration = customRuleConfiguration - return ((regexConfig1, regexConfig2), customRules) + return customRules } +} - private func getTestTextFile() -> SwiftLintFile { - return SwiftLintFile(path: "\(testResourcesPath)/test.txt")! +private extension StyleViolation { + func isSuperfluousDisableCommandViolation(for ruleIdentifier: String) -> Bool { + self.ruleIdentifier == SuperfluousDisableCommandRule.identifier && + reason.contains("SwiftLint rule '\(ruleIdentifier)' did not trigger a violation") } } diff --git a/Tests/SwiftLintFrameworkTests/CyclomaticComplexityConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/CyclomaticComplexityConfigurationTests.swift index ec12e35713..eb9638a82f 100644 --- a/Tests/SwiftLintFrameworkTests/CyclomaticComplexityConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/CyclomaticComplexityConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { +final class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { func testCyclomaticComplexityConfigurationInitializerSetsLevels() { let warning = 10 let error = 30 @@ -35,9 +35,11 @@ class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { let warning1 = 10 let error1 = 30 let length1 = SeverityLevelsConfiguration(warning: warning1, error: error1) - let config1: [String: Any] = ["warning": warning1, - "error": error1, - "ignores_case_statements": true] + let config1: [String: Any] = [ + "warning": warning1, + "error": error1, + "ignores_case_statements": true, + ] let warning2 = 20 let error2 = 40 @@ -62,14 +64,14 @@ class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { func testCyclomaticComplexityConfigurationThrowsOnBadConfigValues() { let badConfigs: [[String: Any]] = [ ["warning": true], - ["ignores_case_statements": 300] + ["ignores_case_statements": 300], ] for badConfig in badConfigs { var configuration = CyclomaticComplexityConfiguration( length: SeverityLevelsConfiguration(warning: 100, error: 150) ) - checkError(Issue.invalidConfiguration(ruleID: CyclomaticComplexityRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: CyclomaticComplexityRule.identifier)) { try configuration.apply(configuration: badConfig) } } diff --git a/Tests/SwiftLintFrameworkTests/CyclomaticComplexityRuleTests.swift b/Tests/SwiftLintFrameworkTests/CyclomaticComplexityRuleTests.swift index be4233ccab..8dab89a1b4 100644 --- a/Tests/SwiftLintFrameworkTests/CyclomaticComplexityRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/CyclomaticComplexityRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class CyclomaticComplexityRuleTests: SwiftLintTestCase { +final class CyclomaticComplexityRuleTests: SwiftLintTestCase { private lazy var complexSwitchExample: Example = { var example = "func switcheroo() {\n" example += " switch foo {\n" diff --git a/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift index 370d85c03e..f9edd35491 100644 --- a/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class DeploymentTargetConfigurationTests: SwiftLintTestCase { +final class DeploymentTargetConfigurationTests: SwiftLintTestCase { private typealias Version = DeploymentTargetConfiguration.Version // swiftlint:disable:next function_body_length @@ -87,10 +87,12 @@ class DeploymentTargetConfigurationTests: SwiftLintTestCase { ) XCTAssertEqual(configuration.severityConfiguration.severity, .warning) - try configuration.apply(configuration: ["tvOS_deployment_target": 10.2, - "tvOSApplicationExtension_deployment_target": 9.1, - "watchOS_deployment_target": 5, - "watchOSApplicationExtension_deployment_target": 2.2]) + try configuration.apply(configuration: [ + "tvOS_deployment_target": 10.2, + "tvOSApplicationExtension_deployment_target": 9.1, + "watchOS_deployment_target": 5, + "watchOSApplicationExtension_deployment_target": 2.2, + ]) XCTAssertEqual( configuration.iOSDeploymentTarget, Version(platform: .iOS, major: 10, minor: 1) @@ -132,12 +134,12 @@ class DeploymentTargetConfigurationTests: SwiftLintTestCase { ["iOS_deployment_target": ""], ["iOS_deployment_target": "5.x"], ["iOS_deployment_target": true], - ["invalid": true] + ["invalid": true], ] for badConfig in badConfigs { var configuration = DeploymentTargetConfiguration() - checkError(Issue.unknownConfiguration(ruleID: DeploymentTargetRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: DeploymentTargetRule.identifier)) { try configuration.apply(configuration: badConfig) } } diff --git a/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift b/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift index 220f1839e7..b101b89b0c 100644 --- a/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class DeploymentTargetRuleTests: SwiftLintTestCase { +final class DeploymentTargetRuleTests: SwiftLintTestCase { func testMacOSAttributeReason() { let example = Example("@available(macOS 10.11, *)\nclass A {}") let violations = self.violations(example, config: ["macOS_deployment_target": "10.14.0"]) @@ -35,7 +35,7 @@ class DeploymentTargetRuleTests: SwiftLintTestCase { } private func violations(_ example: Example, config: Any?) -> [StyleViolation] { - guard let config = makeConfig(config, DeploymentTargetRule.description.identifier) else { + guard let config = makeConfig(config, DeploymentTargetRule.identifier) else { return [] } diff --git a/Tests/SwiftLintFrameworkTests/DisableAllTests.swift b/Tests/SwiftLintFrameworkTests/DisableAllTests.swift index e860186680..a435b8fba1 100644 --- a/Tests/SwiftLintFrameworkTests/DisableAllTests.swift +++ b/Tests/SwiftLintFrameworkTests/DisableAllTests.swift @@ -1,12 +1,12 @@ import SwiftLintFramework import XCTest -class DisableAllTests: SwiftLintTestCase { +final class DisableAllTests: SwiftLintTestCase { /// Example violations. Could be replaced with other single violations. private let violatingPhrases = [ Example("let r = 0"), // Violates identifier_name Example(#"let myString:String = """#), // Violates colon_whitespace - Example("// TODO: Some todo") // Violates todo + Example("// TODO: Some todo"), // Violates todo ] // MARK: Violating Phrase diff --git a/Tests/SwiftLintFrameworkTests/DiscouragedDirectInitRuleTests.swift b/Tests/SwiftLintFrameworkTests/DiscouragedDirectInitRuleTests.swift index 4ad5f98016..74f0f4fd57 100644 --- a/Tests/SwiftLintFrameworkTests/DiscouragedDirectInitRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/DiscouragedDirectInitRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class DiscouragedDirectInitRuleTests: SwiftLintTestCase { +final class DiscouragedDirectInitRuleTests: SwiftLintTestCase { private let baseDescription = DiscouragedDirectInitRule.description func testDiscouragedDirectInitWithConfiguredSeverity() { @@ -10,12 +10,12 @@ class DiscouragedDirectInitRuleTests: SwiftLintTestCase { func testDiscouragedDirectInitWithNewIncludedTypes() { let triggeringExamples = [ Example("let foo = ↓Foo()"), - Example("let bar = ↓Bar()") + Example("let bar = ↓Bar()"), ] let nonTriggeringExamples = [ Example("let foo = Foo(arg: toto)"), - Example("let bar = Bar(arg: \"toto\")") + Example("let bar = Bar(arg: \"toto\")"), ] let description = baseDescription diff --git a/Tests/SwiftLintFrameworkTests/DiscouragedObjectLiteralRuleTests.swift b/Tests/SwiftLintFrameworkTests/DiscouragedObjectLiteralRuleTests.swift index a1679dc833..1a2f17e04e 100644 --- a/Tests/SwiftLintFrameworkTests/DiscouragedObjectLiteralRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/DiscouragedObjectLiteralRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class DiscouragedObjectLiteralRuleTests: SwiftLintTestCase { +final class DiscouragedObjectLiteralRuleTests: SwiftLintTestCase { func testWithImageLiteral() { let baseDescription = DiscouragedObjectLiteralRule.description let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [ diff --git a/Tests/SwiftLintFrameworkTests/DuplicateImportsRuleTests.swift b/Tests/SwiftLintFrameworkTests/DuplicateImportsRuleTests.swift index d1e9d37d07..5a125b426b 100644 --- a/Tests/SwiftLintFrameworkTests/DuplicateImportsRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/DuplicateImportsRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class DuplicateImportsRuleTests: XCTestCase { +final class DuplicateImportsRuleTests: XCTestCase { func testDisableCommand() { let content = """ import InspireAPI diff --git a/Tests/SwiftLintFrameworkTests/EmptyCountRuleTests.swift b/Tests/SwiftLintFrameworkTests/EmptyCountRuleTests.swift index 2b6e0bc83c..7dddfe4f23 100644 --- a/Tests/SwiftLintFrameworkTests/EmptyCountRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/EmptyCountRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class EmptyCountRuleTests: SwiftLintTestCase { +final class EmptyCountRuleTests: SwiftLintTestCase { func testEmptyCountWithOnlyAfterDot() { // Test with `only_after_dot` set to true let nonTriggeringExamples = [ @@ -13,7 +13,7 @@ class EmptyCountRuleTests: SwiftLintTestCase { Example("[Int]().count == 0o07\n"), Example("discount == 0\n"), Example("order.discount == 0\n"), - Example("count == 0\n") + Example("count == 0\n"), ] let triggeringExamples = [ Example("[Int]().↓count == 0\n"), @@ -22,7 +22,7 @@ class EmptyCountRuleTests: SwiftLintTestCase { Example("[Int]().↓count == 0x0\n"), Example("[Int]().↓count == 0x00_00\n"), Example("[Int]().↓count == 0b00\n"), - Example("[Int]().↓count == 0o00\n") + Example("[Int]().↓count == 0o00\n"), ] let corrections = [ @@ -53,7 +53,7 @@ class EmptyCountRuleTests: SwiftLintTestCase { Example("count == 0 && [Int]().↓count == 0o00"): Example("count == 0 && [Int]().isEmpty"), Example("[Int]().count != 3 && [Int]().↓count != 0 || count == 0 && [Int]().count > 2"): - Example("[Int]().count != 3 && ![Int]().isEmpty || count == 0 && [Int]().count > 2") + Example("[Int]().count != 3 && ![Int]().isEmpty || count == 0 && [Int]().count > 2"), ] let description = EmptyCountRule.description diff --git a/Tests/SwiftLintFrameworkTests/ExampleTests.swift b/Tests/SwiftLintFrameworkTests/ExampleTests.swift index f1ab22a655..fda1b3f6e5 100644 --- a/Tests/SwiftLintFrameworkTests/ExampleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExampleTests.swift @@ -1,7 +1,7 @@ import SwiftLintFramework import XCTest -class ExampleTests: SwiftLintTestCase { +final class ExampleTests: SwiftLintTestCase { func testEquatableDoesNotLookAtFile() { let first = Example("foo", file: "a", line: 1) let second = Example("foo", file: "b", line: 1) diff --git a/Tests/SwiftLintFrameworkTests/ExpiringTodoRuleTests.swift b/Tests/SwiftLintFrameworkTests/ExpiringTodoRuleTests.swift index cbc059c260..a95bb4ec5a 100644 --- a/Tests/SwiftLintFrameworkTests/ExpiringTodoRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExpiringTodoRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ExpiringTodoRuleTests: SwiftLintTestCase { +final class ExpiringTodoRuleTests: SwiftLintTestCase { private lazy var config: Configuration = makeConfiguration() func testExpiringTodo() { @@ -141,7 +141,7 @@ class ExpiringTodoRuleTests: SwiftLintTestCase { } private func violations(_ example: Example) -> [StyleViolation] { - return SwiftLintFrameworkTests.violations(example, config: config) + SwiftLintFrameworkTests.violations(example, config: config) } private func dateString(for status: ExpiringTodoRule.ExpiryViolationLevel) -> String { @@ -185,19 +185,19 @@ class ExpiringTodoRuleTests: SwiftLintTestCase { "date_format": config.dateFormat, "date_delimiters": [ "opening": config.dateDelimiters.opening, - "closing": config.dateDelimiters.closing + "closing": config.dateDelimiters.closing, ], - "date_separator": config.dateSeparator + "date_separator": config.dateSeparator, ] } - return makeConfig(serializedConfig, ExpiringTodoRule.description.identifier)! + return makeConfig(serializedConfig, ExpiringTodoRule.identifier)! } } fileprivate extension Configuration { var ruleConfiguration: ExpiringTodoConfiguration { // swiftlint:disable:next force_cast - return (rules.first(where: { $0 is ExpiringTodoRule }) as! ExpiringTodoRule).configuration + (rules.first(where: { $0 is ExpiringTodoRule }) as! ExpiringTodoRule).configuration } } diff --git a/Tests/SwiftLintFrameworkTests/ExplicitInitRuleTests.swift b/Tests/SwiftLintFrameworkTests/ExplicitInitRuleTests.swift index b46b1b12e7..65dfee4653 100644 --- a/Tests/SwiftLintFrameworkTests/ExplicitInitRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExplicitInitRuleTests.swift @@ -1,16 +1,16 @@ @testable import SwiftLintBuiltInRules -class ExplicitInitRuleTests: SwiftLintTestCase { +final class ExplicitInitRuleTests: SwiftLintTestCase { func testIncludeBareInit() { let nonTriggeringExamples = [ Example("let foo = Foo()"), - Example("let foo = init()") + Example("let foo = init()"), ] + ExplicitInitRule.description.nonTriggeringExamples let triggeringExamples = [ Example("let foo: Foo = ↓.init()"), Example("let foo: [Foo] = [↓.init(), ↓.init()]"), - Example("foo(↓.init())") + Example("foo(↓.init())"), ] let description = ExplicitInitRule.description diff --git a/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift index aa2f56f480..ec413e59a4 100644 --- a/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift @@ -2,7 +2,7 @@ @testable import SwiftLintCore import XCTest -class ExplicitTypeInterfaceConfigurationTests: SwiftLintTestCase { +final class ExplicitTypeInterfaceConfigurationTests: SwiftLintTestCase { func testDefaultConfiguration() { let config = ExplicitTypeInterfaceConfiguration() XCTAssertEqual(config.severityConfiguration.severity, .warning) @@ -11,9 +11,13 @@ class ExplicitTypeInterfaceConfigurationTests: SwiftLintTestCase { func testApplyingCustomConfiguration() throws { var config = ExplicitTypeInterfaceConfiguration() - try config.apply(configuration: ["severity": "error", - "excluded": ["local"], - "allow_redundancy": true] as [String: any Sendable]) + try config.apply( + configuration: [ + "severity": "error", + "excluded": ["local"], + "allow_redundancy": true, + ] as [String: any Sendable] + ) XCTAssertEqual(config.severityConfiguration.severity, .error) XCTAssertEqual(config.allowedKinds, Set([.instance, .class, .static])) XCTAssertTrue(config.allowRedundancy) @@ -21,22 +25,23 @@ class ExplicitTypeInterfaceConfigurationTests: SwiftLintTestCase { func testInvalidKeyInCustomConfiguration() { var config = ExplicitTypeInterfaceConfiguration() - checkError(Issue.invalidConfigurationKeys(ruleID: ExplicitTypeInterfaceRule.identifier, keys: ["invalidKey"])) { - try config.apply(configuration: ["invalidKey": "error"]) - } + XCTAssertEqual( + try Issue.captureConsole { try config.apply(configuration: ["invalidKey": "error"]) }, + "warning: Configuration for 'explicit_type_interface' rule contains the invalid key(s) 'invalidKey'." + ) } func testInvalidTypeOfCustomConfiguration() { var config = ExplicitTypeInterfaceConfiguration() - checkError(Issue.invalidConfiguration(ruleID: ExplicitTypeInterfaceRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: ExplicitTypeInterfaceRule.identifier)) { try config.apply(configuration: "invalidKey") } } func testInvalidTypeOfValueInCustomConfiguration() { var config = ExplicitTypeInterfaceConfiguration() - checkError(Issue.unknownConfiguration(ruleID: ExplicitTypeInterfaceRule.description.identifier)) { - try config.apply(configuration: ["severity": 1]) + checkError(Issue.invalidConfiguration(ruleID: ExplicitTypeInterfaceRule.identifier)) { + try config.apply(configuration: ["severity": "foo"]) } } diff --git a/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift index 7e678e4eb8..37d079aa3a 100644 --- a/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { +final class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { func testLocalVars() { let nonTriggeringExamples = [ Example("func foo() {\nlet intVal: Int = 1\n}"), @@ -10,7 +10,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { let x: Int = 1 } } - """) + """), ] let triggeringExamples = [ Example("func foo() {\nlet ↓intVal = 1\n}"), @@ -20,7 +20,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { let ↓x = 1 } } - """) + """), ] let description = ExplicitTypeInterfaceRule.description .with(triggeringExamples: triggeringExamples) @@ -44,12 +44,12 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { func testExcludeClassVars() { let nonTriggeringExamples = ExplicitTypeInterfaceRule.description.nonTriggeringExamples + [ Example("class Foo {\n static var myStaticVar = 0\n}\n"), - Example("class Foo {\n static let myStaticLet = 0\n}\n") + Example("class Foo {\n static let myStaticLet = 0\n}\n"), ] let triggeringExamples: [Example] = [ Example("class Foo {\n var ↓myVar = 0\n\n}\n"), - Example("class Foo {\n let ↓mylet = 0\n\n}\n"), - Example("class Foo {\n class var ↓myClassVar = 0\n}\n") + Example("class Foo {\n let ↓myLet = 0\n\n}\n"), + Example("class Foo {\n class var ↓myClassVar = 0\n}\n"), ] let description = ExplicitTypeInterfaceRule.description .with(triggeringExamples: triggeringExamples) @@ -72,15 +72,15 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { Example("class Foo {\n let array = [String]()\n}\n"), Example("class Foo {\n let dict = [String: String]()\n}\n"), Example("class Foo {\n let dict = [String: [String: Array]]()\n}\n"), - Example("class Foo {\n let l10n = L10n.Communication.self\n}\n") + Example("class Foo {\n let l10n = L10n.Communication.self\n}\n"), ] let triggeringExamples: [Example] = [ Example("class Foo {\n var ↓myVar = 0\n\n}\n"), - Example("class Foo {\n let ↓mylet = 0\n\n}\n"), + Example("class Foo {\n let ↓myLet = 0\n\n}\n"), Example("class Foo {\n static var ↓myStaticVar = 0\n}\n"), Example("class Foo {\n class var ↓myClassVar = 0\n}\n"), Example("class Foo {\n let ↓array = [\"foo\", \"bar\"]\n}\n"), - Example("class Foo {\n let ↓dict = [\"foo\": \"bar\"]\n}\n") + Example("class Foo {\n let ↓dict = [\"foo\": \"bar\"]\n}\n"), ] let description = ExplicitTypeInterfaceRule.description .with(triggeringExamples: triggeringExamples) @@ -106,7 +106,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { case let error as SomeError: break default: break } - """) + """), ] let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples let description = ExplicitTypeInterfaceRule.description @@ -139,7 +139,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { } } } - """) + """), ] let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples let description = ExplicitTypeInterfaceRule.description @@ -150,7 +150,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { } func testFastEnumerationDeclaration() { - let nonTriggeringExaples = [ + let nonTriggeringExamples = [ Example(""" func foo() { let elements: [Int] = [1, 2] @@ -162,13 +162,13 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { let elements: [Int] = [1, 2] for (index, element) in elements.enumerated() {} } - """) + """), ] let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples let description = ExplicitTypeInterfaceRule.description .with(triggeringExamples: triggeringExamples) - .with(nonTriggeringExamples: nonTriggeringExaples) + .with(nonTriggeringExamples: nonTriggeringExamples) verifyRule(description) } @@ -198,7 +198,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { case var (x, y): break } } - """) + """), ] let triggeringExamples = [ @@ -226,7 +226,7 @@ class ExplicitTypeInterfaceRuleTests: SwiftLintTestCase { default: let ↓fooBar = 1 } } - """) + """), ] let description = ExplicitTypeInterfaceRule.description diff --git a/Tests/SwiftLintFrameworkTests/ExtendedNSStringTests.swift b/Tests/SwiftLintFrameworkTests/ExtendedNSStringTests.swift index 06c0632084..66ec4b8330 100644 --- a/Tests/SwiftLintFrameworkTests/ExtendedNSStringTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExtendedNSStringTests.swift @@ -1,7 +1,7 @@ import SourceKittenFramework import XCTest -class ExtendedNSStringTests: SwiftLintTestCase { +final class ExtendedNSStringTests: SwiftLintTestCase { func testLineAndCharacterForByteOffset_forContentsContainingMultibyteCharacters() { let contents = "" + "import Foundation\n" + // 18 characters diff --git a/Tests/SwiftLintFrameworkTests/ExtendedStringTests.swift b/Tests/SwiftLintFrameworkTests/ExtendedStringTests.swift index bd1f36b73e..d504d3f4d0 100644 --- a/Tests/SwiftLintFrameworkTests/ExtendedStringTests.swift +++ b/Tests/SwiftLintFrameworkTests/ExtendedStringTests.swift @@ -1,6 +1,6 @@ import XCTest -class ExtendedStringTests: SwiftLintTestCase { +final class ExtendedStringTests: SwiftLintTestCase { func testCountOccurrences() { XCTAssertEqual("aabbabaaba".countOccurrences(of: "a"), 6) XCTAssertEqual("".countOccurrences(of: "a"), 0) diff --git a/Tests/SwiftLintFrameworkTests/FileHeaderRuleTests.swift b/Tests/SwiftLintFrameworkTests/FileHeaderRuleTests.swift index 2779b09c9c..8867afcf4a 100644 --- a/Tests/SwiftLintFrameworkTests/FileHeaderRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FileHeaderRuleTests.swift @@ -3,7 +3,7 @@ import XCTest private let fixturesDirectory = "\(TestResources.path)/FileHeaderRuleFixtures" -class FileHeaderRuleTests: SwiftLintTestCase { +final class FileHeaderRuleTests: SwiftLintTestCase { private func validate(fileName: String, using configuration: Any) throws -> [StyleViolation] { let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! let rule = try FileHeaderRule(configuration: configuration) @@ -17,14 +17,14 @@ class FileHeaderRuleTests: SwiftLintTestCase { func testFileHeaderWithRequiredString() { let nonTriggeringExamples = [ Example("// **Header"), - Example("//\n// **Header") + Example("//\n// **Header"), ] let triggeringExamples = [ Example("↓// Copyright\n"), Example("let foo = \"**Header\""), Example("let foo = 2 // **Header"), Example("let foo = 2\n// **Header"), - Example("let foo = 2 // **Header") + Example("let foo = 2 // **Header"), ] let description = FileHeaderRule.description .with(nonTriggeringExamples: nonTriggeringExamples) @@ -38,12 +38,12 @@ class FileHeaderRuleTests: SwiftLintTestCase { func testFileHeaderWithRequiredPattern() { let nonTriggeringExamples = [ Example("// Copyright © 2016 Realm"), - Example("//\n// Copyright © 2016 Realm)") + Example("//\n// Copyright © 2016 Realm)"), ] let triggeringExamples = [ Example("↓// Copyright\n"), Example("↓// Copyright © foo Realm"), - Example("↓// Copyright © 2016 MyCompany") + Example("↓// Copyright © 2016 MyCompany"), ] let description = FileHeaderRule.description .with(nonTriggeringExamples: nonTriggeringExamples) @@ -77,11 +77,11 @@ class FileHeaderRuleTests: SwiftLintTestCase { Example("let foo = \"**All rights reserved.\""), Example("let foo = 2 // **All rights reserved."), Example("let foo = 2\n// **All rights reserved."), - Example("let foo = 2 // **All rights reserved.") + Example("let foo = 2 // **All rights reserved."), ] let triggeringExamples = [ Example("// ↓**All rights reserved."), - Example("//\n// ↓**All rights reserved.") + Example("//\n// ↓**All rights reserved."), ] let description = FileHeaderRule.description .with(nonTriggeringExamples: nonTriggeringExamples) @@ -97,11 +97,11 @@ class FileHeaderRuleTests: SwiftLintTestCase { Example("// FileHeaderRuleTests.m\n"), Example("let foo = \"FileHeaderRuleTests.swift\""), Example("let foo = 2 // FileHeaderRuleTests.swift."), - Example("let foo = 2\n // FileHeaderRuleTests.swift.") + Example("let foo = 2\n // FileHeaderRuleTests.swift."), ] let triggeringExamples = [ Example("//↓ FileHeaderRuleTests.swift"), - Example("//\n//↓ FileHeaderRuleTests.swift") + Example("//\n//↓ FileHeaderRuleTests.swift"), ] let description = FileHeaderRule.description .with(nonTriggeringExamples: nonTriggeringExamples) @@ -114,11 +114,11 @@ class FileHeaderRuleTests: SwiftLintTestCase { func testFileHeaderWithForbiddenPatternAndDocComment() { let nonTriggeringExamples = [ Example("/// This is great tool with tests.\nclass GreatTool {}"), - Example("class GreatTool {}") + Example("class GreatTool {}"), ] let triggeringExamples = [ Example("// FileHeaderRule↓Tests.swift"), - Example("//\n// FileHeaderRule↓Tests.swift") + Example("//\n// FileHeaderRule↓Tests.swift"), ] let description = FileHeaderRule.description .with(nonTriggeringExamples: nonTriggeringExamples) @@ -154,8 +154,9 @@ class FileHeaderRuleTests: SwiftLintTestCase { func testFileHeaderWithRequiredPatternUsingFilenamePlaceholder() { let configuration1 = ["required_pattern": "// SWIFTLINT_CURRENT_FILENAME\n.*\\d{4}"] - let configuration2 = ["required_pattern": - "// Copyright © \\d{4}\n// File: \"SWIFTLINT_CURRENT_FILENAME\""] + let configuration2 = [ + "required_pattern": "// Copyright © \\d{4}\n// File: \"SWIFTLINT_CURRENT_FILENAME\"", + ] // Non triggering tests XCTAssert(try validate(fileName: "FileNameMatchingSimple.swift", using: configuration1).isEmpty) diff --git a/Tests/SwiftLintFrameworkTests/FileLengthRuleTests.swift b/Tests/SwiftLintFrameworkTests/FileLengthRuleTests.swift index f55ebbdcea..6b629889bf 100644 --- a/Tests/SwiftLintFrameworkTests/FileLengthRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FileLengthRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class FileLengthRuleTests: SwiftLintTestCase { +final class FileLengthRuleTests: SwiftLintTestCase { func testFileLengthWithDefaultConfiguration() { verifyRule(FileLengthRule.description, commentDoesntViolate: false, testMultiByteOffsets: false, testShebang: false) @@ -13,7 +13,7 @@ class FileLengthRuleTests: SwiftLintTestCase { let nonTriggeringExamples = [ Example((repeatElement("print(\"swiftlint\")\n", count: 400) + ["//\n"]).joined()), Example(repeatElement("print(\"swiftlint\")\n", count: 400).joined()), - Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()) + Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()), ] let description = FileLengthRule.description diff --git a/Tests/SwiftLintFrameworkTests/FileNameNoSpaceRuleTests.swift b/Tests/SwiftLintFrameworkTests/FileNameNoSpaceRuleTests.swift index ff83c128bd..4aee8dcc7c 100644 --- a/Tests/SwiftLintFrameworkTests/FileNameNoSpaceRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FileNameNoSpaceRuleTests.swift @@ -2,11 +2,11 @@ import SourceKittenFramework @testable import SwiftLintBuiltInRules import XCTest -private let fixturesDirectory = #file.bridge() +private let fixturesDirectory = #filePath.bridge() .deletingLastPathComponent.bridge() .appendingPathComponent("Resources/FileNameNoSpaceRuleFixtures") -class FileNameNoSpaceRuleTests: SwiftLintTestCase { +final class FileNameNoSpaceRuleTests: SwiftLintTestCase { private func validate(fileName: String, excludedOverride: [String]? = nil) throws -> [StyleViolation] { let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! let rule: FileNameNoSpaceRule diff --git a/Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift b/Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift index 2ac1953856..4e3a2923fa 100644 --- a/Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift @@ -3,26 +3,35 @@ import XCTest private let fixturesDirectory = "\(TestResources.path)/FileNameRuleFixtures" -class FileNameRuleTests: SwiftLintTestCase { - private func validate(fileName: String, excludedOverride: [String]? = nil, - prefixPattern: String? = nil, suffixPattern: String? = nil, - nestedTypeSeparator: String? = nil) throws -> [StyleViolation] { +final class FileNameRuleTests: SwiftLintTestCase { + private func validate(fileName: String, + excluded: [String]? = nil, + prefixPattern: String? = nil, + suffixPattern: String? = nil, + nestedTypeSeparator: String? = nil, + requireFullyQualifiedNames: Bool = false) throws -> [StyleViolation] { let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! - let rule: FileNameRule - if let excluded = excludedOverride { - rule = try FileNameRule(configuration: ["excluded": excluded]) - } else if let prefixPattern, let suffixPattern { - rule = try FileNameRule(configuration: ["prefix_pattern": prefixPattern, "suffix_pattern": suffixPattern]) - } else if let prefixPattern { - rule = try FileNameRule(configuration: ["prefix_pattern": prefixPattern]) - } else if let suffixPattern { - rule = try FileNameRule(configuration: ["suffix_pattern": suffixPattern]) - } else if let nestedTypeSeparator { - rule = try FileNameRule(configuration: ["nested_type_separator": nestedTypeSeparator]) - } else { - rule = FileNameRule() + + var configuration = [String: Any]() + + if let excluded { + configuration["excluded"] = excluded + } + if let prefixPattern { + configuration["prefix_pattern"] = prefixPattern + } + if let suffixPattern { + configuration["suffix_pattern"] = suffixPattern + } + if let nestedTypeSeparator { + configuration["nested_type_separator"] = nestedTypeSeparator + } + if requireFullyQualifiedNames { + configuration["require_fully_qualified_names"] = requireFullyQualifiedNames } + let rule = try FileNameRule(configuration: configuration) + return rule.validate(file: file) } @@ -50,14 +59,30 @@ class FileNameRuleTests: SwiftLintTestCase { XCTAssert(try validate(fileName: "Notification.Name+Extension.swift").isEmpty) } + func testNestedTypeDoesntTrigger() { + XCTAssert(try validate(fileName: "Nested.MyType.swift").isEmpty) + } + + func testMultipleLevelsDeeplyNestedTypeDoesntTrigger() { + XCTAssert(try validate(fileName: "Multiple.Levels.Deeply.Nested.MyType.swift").isEmpty) + } + + func testNestedTypeNotFullyQualifiedDoesntTrigger() { + XCTAssert(try validate(fileName: "MyType.swift").isEmpty) + } + + func testNestedTypeNotFullyQualifiedDoesTriggerWithOverride() { + XCTAssert(try validate(fileName: "MyType.swift", requireFullyQualifiedNames: true).isNotEmpty) + } + func testNestedTypeSeparatorDoesntTrigger() { XCTAssert(try validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "").isEmpty) XCTAssert(try validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: "__").isEmpty) } func testWrongNestedTypeSeparatorDoesTrigger() { - XCTAssert(try !validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: ".").isEmpty) - XCTAssert(try !validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "__").isEmpty) + XCTAssert(try validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: ".").isNotEmpty) + XCTAssert(try validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "__").isNotEmpty) } func testMisspelledNameDoesTrigger() { @@ -65,11 +90,11 @@ class FileNameRuleTests: SwiftLintTestCase { } func testMisspelledNameDoesntTriggerWithOverride() { - XCTAssert(try validate(fileName: "MyStructf.swift", excludedOverride: ["MyStructf.swift"]).isEmpty) + XCTAssert(try validate(fileName: "MyStructf.swift", excluded: ["MyStructf.swift"]).isEmpty) } func testMainDoesTriggerWithoutOverride() { - XCTAssertEqual(try validate(fileName: "main.swift", excludedOverride: []).count, 1) + XCTAssertEqual(try validate(fileName: "main.swift", excluded: []).count, 1) } func testCustomSuffixPattern() { diff --git a/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift b/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift index e5b7d29290..e9a6f8c93d 100644 --- a/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class FileTypesOrderRuleTests: SwiftLintTestCase { +final class FileTypesOrderRuleTests: SwiftLintTestCase { // swiftlint:disable:next function_body_length func testFileTypesOrderReversedOrder() { // Test with reversed `order` entries @@ -66,7 +66,7 @@ class FileTypesOrderRuleTests: SwiftLintTestCase { LibraryItem(ContentView()) } } - """) + """), ] let reversedOrderDescription = FileTypesOrderRule.description @@ -108,7 +108,7 @@ class FileTypesOrderRuleTests: SwiftLintTestCase { extension TestViewController: UITableViewDelegate { func someMethod() {} } - """) + """), ] let triggeringExamples = [ Example(""" @@ -132,7 +132,7 @@ class FileTypesOrderRuleTests: SwiftLintTestCase { } class TestViewController: UIViewController {} - """) + """), ] let groupedOrderDescription = FileTypesOrderRule.description diff --git a/Tests/SwiftLintFrameworkTests/FunctionBodyLengthRuleTests.swift b/Tests/SwiftLintFrameworkTests/FunctionBodyLengthRuleTests.swift index e53ae1c410..d849b2996d 100644 --- a/Tests/SwiftLintFrameworkTests/FunctionBodyLengthRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FunctionBodyLengthRuleTests.swift @@ -3,27 +3,33 @@ import XCTest private func funcWithBody(_ body: String, violates: Bool = false, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line) -> Example { let marker = violates ? "↓" : "" return Example("func \(marker)abc() {\n\(body)}\n", file: file, line: line) } -private func violatingFuncWithBody(_ body: String, file: StaticString = #file, line: UInt = #line) -> Example { - return funcWithBody(body, violates: true, file: file, line: line) +private func violatingFuncWithBody(_ body: String, file: StaticString = #filePath, line: UInt = #line) -> Example { + funcWithBody(body, violates: true, file: file, line: line) } -class FunctionBodyLengthRuleTests: SwiftLintTestCase { +final class FunctionBodyLengthRuleTests: SwiftLintTestCase { func testFunctionBodyLengths() { let longFunctionBody = funcWithBody(repeatElement("x = 0\n", count: 49).joined()) XCTAssertEqual(self.violations(longFunctionBody), []) let longerFunctionBody = violatingFuncWithBody(repeatElement("x = 0\n", count: 51).joined()) - XCTAssertEqual(self.violations(longerFunctionBody), [StyleViolation( - ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 6), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines")]) + XCTAssertEqual( + self.violations(longerFunctionBody), + [ + StyleViolation( + ruleDescription: FunctionBodyLengthRule.description, + location: Location(file: nil, line: 1, character: 6), + reason: "Function body should span 50 lines or less excluding comments and " + + "whitespace: currently spans 51 lines" + ), + ] + ) let longerFunctionBodyWithEmptyLines = funcWithBody( repeatElement("\n", count: 100).joined() @@ -42,11 +48,17 @@ class FunctionBodyLengthRuleTests: SwiftLintTestCase { repeatElement("x = 0\n", count: 51).joined() + "// comment only line should be ignored.\n" ) - XCTAssertEqual(self.violations(longerFunctionBodyWithComments), [StyleViolation( - ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 6), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines")]) + XCTAssertEqual( + self.violations(longerFunctionBodyWithComments), + [ + StyleViolation( + ruleDescription: FunctionBodyLengthRule.description, + location: Location(file: nil, line: 1, character: 6), + reason: "Function body should span 50 lines or less excluding comments and " + + "whitespace: currently spans 51 lines" + ), + ] + ) } func testFunctionBodyLengthsWithMultilineComments() { @@ -60,11 +72,17 @@ class FunctionBodyLengthRuleTests: SwiftLintTestCase { repeatElement("x = 0\n", count: 51).joined() + "/* multi line comment only line should be ignored.\n*/\n" ) - XCTAssertEqual(self.violations(longerFunctionBodyWithMultilineComments), [StyleViolation( - ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 6), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines")]) + XCTAssertEqual( + self.violations(longerFunctionBodyWithMultilineComments), + [ + StyleViolation( + ruleDescription: FunctionBodyLengthRule.description, + location: Location(file: nil, line: 1, character: 6), + reason: "Function body should span 50 lines or less excluding comments and " + + "whitespace: currently spans 51 lines" + ), + ] + ) } func testConfiguration() { @@ -83,7 +101,7 @@ class FunctionBodyLengthRuleTests: SwiftLintTestCase { } private func violations(_ example: Example, configuration: Any? = nil) -> [StyleViolation] { - let config = makeConfig(configuration, FunctionBodyLengthRule.description.identifier)! + let config = makeConfig(configuration, FunctionBodyLengthRule.identifier)! return SwiftLintFrameworkTests.violations(example, config: config) } } diff --git a/Tests/SwiftLintFrameworkTests/FunctionParameterCountRuleTests.swift b/Tests/SwiftLintFrameworkTests/FunctionParameterCountRuleTests.swift index 3a3af12910..c8f3e3ebf3 100644 --- a/Tests/SwiftLintFrameworkTests/FunctionParameterCountRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FunctionParameterCountRuleTests.swift @@ -2,14 +2,14 @@ private func funcWithParameters(_ parameters: String, violates: Bool = false, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line) -> Example { let marker = violates ? "↓" : "" return Example("func \(marker)abc(\(parameters)) {}\n", file: file, line: line) } -class FunctionParameterCountRuleTests: SwiftLintTestCase { +final class FunctionParameterCountRuleTests: SwiftLintTestCase { func testFunctionParameterCount() { let baseDescription = FunctionParameterCountRule.description let nonTriggeringExamples = [ diff --git a/Tests/SwiftLintFrameworkTests/GenericTypeNameRuleTests.swift b/Tests/SwiftLintFrameworkTests/GenericTypeNameRuleTests.swift index 0d35be7e27..8473e98a8f 100644 --- a/Tests/SwiftLintFrameworkTests/GenericTypeNameRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/GenericTypeNameRuleTests.swift @@ -1,16 +1,16 @@ @testable import SwiftLintBuiltInRules -class GenericTypeNameRuleTests: SwiftLintTestCase { +final class GenericTypeNameRuleTests: SwiftLintTestCase { func testGenericTypeNameWithExcluded() { let baseDescription = GenericTypeNameRule.description let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [ Example("func foo {}"), Example("func foo {}"), - Example("func foo {}") + Example("func foo {}"), ] let triggeringExamples = baseDescription.triggeringExamples + [ Example("func foo {}"), - Example("func foo {}") + Example("func foo {}"), ] let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples, triggeringExamples: triggeringExamples) @@ -25,7 +25,7 @@ class GenericTypeNameRuleTests: SwiftLintTestCase { Example("typealias StringDictionary = Dictionary"), Example("class Foo {}"), Example("struct Foo {}"), - Example("enum Foo {}") + Example("enum Foo {}"), ] let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples) @@ -48,7 +48,7 @@ class GenericTypeNameRuleTests: SwiftLintTestCase { Example("func foo<↓type>() {}"), Example("class Foo<↓type> {}"), Example("struct Foo<↓type> {}"), - Example("enum Foo<↓type> {}") + Example("enum Foo<↓type> {}"), ] let nonTriggeringExamples = baseDescription.nonTriggeringExamples + triggeringExamplesToRemove.removingViolationMarkers() diff --git a/Tests/SwiftLintFrameworkTests/GlobTests.swift b/Tests/SwiftLintFrameworkTests/GlobTests.swift index 17f7f5f6e6..0f94aaddc2 100644 --- a/Tests/SwiftLintFrameworkTests/GlobTests.swift +++ b/Tests/SwiftLintFrameworkTests/GlobTests.swift @@ -3,7 +3,7 @@ import XCTest final class GlobTests: SwiftLintTestCase { private var mockPath: String { - return testResourcesPath.stringByAppendingPathComponent("ProjectMock") + testResourcesPath.stringByAppendingPathComponent("ProjectMock") } func testNonExistingDirectory() { @@ -53,7 +53,7 @@ final class GlobTests: SwiftLintTestCase { func testMatchesMultipleFiles() { let expectedFiles: Set = [ mockPath.stringByAppendingPathComponent("Level0.swift"), - mockPath.stringByAppendingPathComponent("Directory.swift") + mockPath.stringByAppendingPathComponent("Directory.swift"), ] let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("*.swift")) @@ -75,7 +75,7 @@ final class GlobTests: SwiftLintTestCase { "Level1/Level2/Level2.swift", "Level1/Level2/Level3/Level3.swift", "NestedConfig/Test/Main.swift", - "NestedConfig/Test/Sub/Sub.swift" + "NestedConfig/Test/Sub/Sub.swift", ].map(mockPath.stringByAppendingPathComponent) ) diff --git a/Tests/SwiftLintFrameworkTests/IdentifierNameRuleTests.swift b/Tests/SwiftLintFrameworkTests/IdentifierNameRuleTests.swift index 6a8bc753ec..324d4d197e 100644 --- a/Tests/SwiftLintFrameworkTests/IdentifierNameRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/IdentifierNameRuleTests.swift @@ -1,17 +1,17 @@ @testable import SwiftLintBuiltInRules import XCTest -class IdentifierNameRuleTests: SwiftLintTestCase { +final class IdentifierNameRuleTests: SwiftLintTestCase { func testIdentifierNameWithExcluded() { let baseDescription = IdentifierNameRule.description let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [ Example("let Apple = 0"), Example("let some_apple = 0"), - Example("let Test123 = 0") + Example("let Test123 = 0"), ] let triggeringExamples = baseDescription.triggeringExamples + [ Example("let ap_ple = 0"), - Example("let AppleJuice = 0") + Example("let AppleJuice = 0"), ] let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples, triggeringExamples: triggeringExamples) @@ -24,7 +24,7 @@ class IdentifierNameRuleTests: SwiftLintTestCase { Example("let myLet$ = 0"), Example("let myLet% = 0"), Example("let myLet$% = 0"), - Example("let _myLet = 0") + Example("let _myLet = 0"), ] let triggeringExamples = baseDescription.triggeringExamples.filter { !$0.code.contains("_") } let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples, @@ -51,7 +51,7 @@ class IdentifierNameRuleTests: SwiftLintTestCase { Example("class C { class let ↓MyLet = 0 }"), Example("class C { static func ↓MyFunc() {} }"), Example("class C { class func ↓MyFunc() {} }"), - Example("func ↓√ (arg: Double) -> Double { arg }") + Example("func ↓√ (arg: Double) -> Double { arg }"), ] let nonTriggeringExamples = baseDescription.nonTriggeringExamples + triggeringExamplesToRemove.removingViolationMarkers() @@ -68,12 +68,12 @@ class IdentifierNameRuleTests: SwiftLintTestCase { let triggeringExamples = [ Example("let ↓MyLet = 0"), Example("enum Foo { case ↓MyCase }"), - Example("func ↓IsOperator(name: String) -> Bool { true }") + Example("func ↓IsOperator(name: String) -> Bool { true }"), ] let nonTriggeringExamples = [ Example("let myLet = 0"), Example("enum Foo { case myCase }"), - Example("func isOperator(name: String) -> Bool { true }") + Example("func isOperator(name: String) -> Bool { true }"), ] verifyRule( @@ -99,11 +99,11 @@ class IdentifierNameRuleTests: SwiftLintTestCase { ]) .with(nonTriggeringExamples: [ Example("let MyLet = 0"), - Example("enum Foo { case myCase }") + Example("enum Foo { case myCase }"), ]), ruleConfiguration: [ "validates_start_with_lowercase": true, - "allowed_symbols": ["M"] + "allowed_symbols": ["M"], ] as [String: any Sendable] ) } diff --git a/Tests/SwiftLintFrameworkTests/ImplicitGetterRuleTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitGetterRuleTests.swift index 8a83b92bbe..0905bf01f9 100644 --- a/Tests/SwiftLintFrameworkTests/ImplicitGetterRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ImplicitGetterRuleTests.swift @@ -3,7 +3,7 @@ import XCTest final class ImplicitGetterRuleTests: SwiftLintTestCase { func testPropertyReason() throws { - let config = try XCTUnwrap(makeConfig(nil, ImplicitGetterRule.description.identifier)) + let config = try XCTUnwrap(makeConfig(nil, ImplicitGetterRule.identifier)) let example = Example(""" class Foo { var foo: Int { @@ -20,7 +20,7 @@ final class ImplicitGetterRuleTests: SwiftLintTestCase { } func testSubscriptReason() throws { - let config = try XCTUnwrap(makeConfig(nil, ImplicitGetterRule.description.identifier)) + let config = try XCTUnwrap(makeConfig(nil, ImplicitGetterRule.identifier)) let example = Example(""" class Foo { subscript(i: Int) -> Int { diff --git a/Tests/SwiftLintFrameworkTests/ImplicitReturnConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitReturnConfigurationTests.swift index 82e34d95a5..b19dd28e02 100644 --- a/Tests/SwiftLintFrameworkTests/ImplicitReturnConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/ImplicitReturnConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ImplicitReturnConfigurationTests: SwiftLintTestCase { +final class ImplicitReturnConfigurationTests: SwiftLintTestCase { func testImplicitReturnConfigurationFromDictionary() throws { var configuration = ImplicitReturnConfiguration(includedKinds: Set()) let config: [String: Any] = [ @@ -11,8 +11,8 @@ class ImplicitReturnConfigurationTests: SwiftLintTestCase { "function", "getter", "initializer", - "subscript" - ] + "subscript", + ], ] try configuration.apply(configuration: config) @@ -21,7 +21,7 @@ class ImplicitReturnConfigurationTests: SwiftLintTestCase { .function, .getter, .initializer, - .subscript + .subscript, ]) XCTAssertEqual(configuration.severityConfiguration.severity, .error) XCTAssertEqual(configuration.includedKinds, expectedKinds) @@ -31,7 +31,7 @@ class ImplicitReturnConfigurationTests: SwiftLintTestCase { var configuration = ImplicitReturnConfiguration() let config = ["included": ["foreach"]] as [String: any Sendable] - checkError(Issue.unknownConfiguration(ruleID: ImplicitReturnRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: ImplicitReturnRule.identifier)) { try configuration.apply(configuration: config) } } diff --git a/Tests/SwiftLintFrameworkTests/ImplicitReturnRuleTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitReturnRuleTests.swift index 7e5870a3ab..6aa4036f22 100644 --- a/Tests/SwiftLintFrameworkTests/ImplicitReturnRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ImplicitReturnRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class ImplicitReturnRuleTests: SwiftLintTestCase { +final class ImplicitReturnRuleTests: SwiftLintTestCase { func testOnlyClosureKindIncluded() { var nonTriggeringExamples = ImplicitReturnRuleExamples.nonTriggeringExamples + ImplicitReturnRuleExamples.triggeringExamples diff --git a/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift index bfffed210f..6ba01c625c 100644 --- a/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift @@ -2,7 +2,7 @@ import XCTest // swiftlint:disable:next type_name -class ImplicitlyUnwrappedOptionalConfigurationTests: SwiftLintTestCase { +final class ImplicitlyUnwrappedOptionalConfigurationTests: SwiftLintTestCase { func testImplicitlyUnwrappedOptionalConfigurationProperlyAppliesConfigurationFromDictionary() throws { var configuration = ImplicitlyUnwrappedOptionalConfiguration( severityConfiguration: SeverityConfiguration(.warning), @@ -30,7 +30,7 @@ class ImplicitlyUnwrappedOptionalConfigurationTests: SwiftLintTestCase { let badConfigs: [[String: Any]] = [ ["mode": "everything"], ["mode": false], - ["mode": 42] + ["mode": 42], ] for badConfig in badConfigs { @@ -39,7 +39,7 @@ class ImplicitlyUnwrappedOptionalConfigurationTests: SwiftLintTestCase { mode: .allExceptIBOutlets ) - checkError(Issue.unknownConfiguration(ruleID: ImplicitlyUnwrappedOptionalRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: ImplicitlyUnwrappedOptionalRule.identifier)) { try configuration.apply(configuration: badConfig) } } diff --git a/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift index b9245c2035..4d677b5bc2 100644 --- a/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ImplicitlyUnwrappedOptionalRuleTests: SwiftLintTestCase { +final class ImplicitlyUnwrappedOptionalRuleTests: SwiftLintTestCase { func testImplicitlyUnwrappedOptionalRuleDefaultConfiguration() { let rule = ImplicitlyUnwrappedOptionalRule() XCTAssertEqual(rule.configuration.mode, .allExceptIBOutlets) @@ -13,7 +13,7 @@ class ImplicitlyUnwrappedOptionalRuleTests: SwiftLintTestCase { let triggeringExamples = [ Example("@IBOutlet private var label: UILabel!"), Example("@IBOutlet var label: UILabel!"), - Example("let int: Int!") + Example("let int: Int!"), ] let nonTriggeringExamples = [Example("if !boolean {}")] @@ -29,7 +29,7 @@ class ImplicitlyUnwrappedOptionalRuleTests: SwiftLintTestCase { let triggeringExamples = [ Example("private weak var label: ↓UILabel!"), Example("weak var label: ↓UILabel!"), - Example("@objc weak var label: ↓UILabel!") + Example("@objc weak var label: ↓UILabel!"), ] let nonTriggeringExamples = [ @@ -37,7 +37,7 @@ class ImplicitlyUnwrappedOptionalRuleTests: SwiftLintTestCase { Example("@IBOutlet var label: UILabel!"), Example("@IBOutlet weak var label: UILabel!"), Example("var label: UILabel!"), - Example("let int: Int!") + Example("let int: Int!"), ] let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples) diff --git a/Tests/SwiftLintFrameworkTests/InclusiveLanguageRuleTests.swift b/Tests/SwiftLintFrameworkTests/InclusiveLanguageRuleTests.swift index be3649c97a..a4eecf6901 100644 --- a/Tests/SwiftLintFrameworkTests/InclusiveLanguageRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/InclusiveLanguageRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class InclusiveLanguageRuleTests: SwiftLintTestCase { +final class InclusiveLanguageRuleTests: SwiftLintTestCase { func testNonTriggeringExamplesWithNonDefaultConfig() { InclusiveLanguageRuleExamples.nonTriggeringExamplesWithConfig.forEach { example in let description = InclusiveLanguageRule.description diff --git a/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift b/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift index 4b5597d142..b0234978bc 100644 --- a/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift @@ -1,14 +1,20 @@ @testable import SwiftLintBuiltInRules +@testable import SwiftLintCore import SwiftLintTestHelpers import XCTest -class IndentationWidthRuleTests: SwiftLintTestCase { - func testInvalidIndentation() { +final class IndentationWidthRuleTests: SwiftLintTestCase { + func testInvalidIndentation() throws { var testee = IndentationWidthConfiguration() + let defaultValue = testee.indentationWidth + for indentation in [0, -1, -5] { - checkError(Issue.invalidConfiguration(ruleID: IndentationWidthRule.description.identifier)) { - try testee.apply(configuration: ["indentation_width": indentation]) - } + XCTAssertEqual( + try Issue.captureConsole { try testee.apply(configuration: ["indentation_width": indentation]) }, + "warning: Invalid configuration for 'indentation_width' rule. Falling back to default." + ) + // Value remains the default. + XCTAssertEqual(testee.indentationWidth, defaultValue) } } @@ -265,7 +271,7 @@ class IndentationWidthRuleTests: SwiftLintTestCase { includeComments: Bool = true, includeCompilerDirectives: Bool = true, includeMultilineStrings: Bool = true, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) -> Int { var configDict: [String: Any] = [:] @@ -276,7 +282,7 @@ class IndentationWidthRuleTests: SwiftLintTestCase { configDict["include_compiler_directives"] = includeCompilerDirectives configDict["include_multiline_strings"] = includeMultilineStrings - guard let config = makeConfig(configDict, IndentationWidthRule.description.identifier) else { + guard let config = makeConfig(configDict, IndentationWidthRule.identifier) else { XCTFail("Unable to create rule configuration.", file: (file), line: line) return 0 } @@ -291,7 +297,7 @@ class IndentationWidthRuleTests: SwiftLintTestCase { includeComments: Bool = true, includeCompilerDirectives: Bool = true, includeMultilineStrings: Bool = true, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { XCTAssertEqual( @@ -316,7 +322,7 @@ class IndentationWidthRuleTests: SwiftLintTestCase { includeComments: Bool = true, includeCompilerDirectives: Bool = true, includeMultilineStrings: Bool = true, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { assertViolations( @@ -337,7 +343,7 @@ class IndentationWidthRuleTests: SwiftLintTestCase { includeComments: Bool = true, includeCompilerDirectives: Bool = true, includeMultilineStrings: Bool = true, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { assertViolations( diff --git a/Tests/SwiftLintFrameworkTests/LineEndingTests.swift b/Tests/SwiftLintFrameworkTests/LineEndingTests.swift index 149e626848..8a1caa3a0e 100644 --- a/Tests/SwiftLintFrameworkTests/LineEndingTests.swift +++ b/Tests/SwiftLintFrameworkTests/LineEndingTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class LineEndingTests: SwiftLintTestCase { +final class LineEndingTests: SwiftLintTestCase { func testCarriageReturnDoesNotCauseError() { XCTAssert( violations( diff --git a/Tests/SwiftLintFrameworkTests/LineLengthConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/LineLengthConfigurationTests.swift index cc488e71a5..55360aabe8 100644 --- a/Tests/SwiftLintFrameworkTests/LineLengthConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/LineLengthConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class LineLengthConfigurationTests: SwiftLintTestCase { +final class LineLengthConfigurationTests: SwiftLintTestCase { private let severityLevels = SeverityLevelsConfiguration(warning: 100, error: 150) func testLineLengthConfigurationInitializerSetsLength() { @@ -69,9 +69,9 @@ class LineLengthConfigurationTests: SwiftLintTestCase { } func testLineLengthConfigurationThrowsOnBadConfig() { - let config = "unknown" + let config = ["warning": "unknown"] var configuration = LineLengthConfiguration(length: severityLevels) - checkError(Issue.invalidConfiguration(ruleID: LineLengthRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: LineLengthRule.identifier)) { try configuration.apply(configuration: config) } } @@ -79,12 +79,12 @@ class LineLengthConfigurationTests: SwiftLintTestCase { func testLineLengthConfigurationThrowsOnBadConfigValues() { let badConfigs: [[String: Any]] = [ ["warning": true], - ["ignores_function_declarations": 300] + ["ignores_function_declarations": 300], ] for badConfig in badConfigs { var configuration = LineLengthConfiguration(length: severityLevels) - checkError(Issue.invalidConfiguration(ruleID: LineLengthRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: LineLengthRule.identifier)) { try configuration.apply(configuration: badConfig) } } @@ -119,11 +119,13 @@ class LineLengthConfigurationTests: SwiftLintTestCase { let warning1 = 100 let error1 = 100 let length1 = SeverityLevelsConfiguration(warning: warning1, error: error1) - let config1: [String: Any] = ["warning": warning1, - "error": error1, - "ignores_urls": true, - "ignores_function_declarations": true, - "ignores_comments": true] + let config1: [String: Any] = [ + "warning": warning1, + "error": error1, + "ignores_urls": true, + "ignores_function_declarations": true, + "ignores_comments": true, + ] let warning2 = 200 let error2 = 200 @@ -131,9 +133,11 @@ class LineLengthConfigurationTests: SwiftLintTestCase { let config2: [String: Int] = ["warning": warning2, "error": error2] let length3 = SeverityLevelsConfiguration(warning: warning2) - let config3: [String: Bool] = ["ignores_urls": false, - "ignores_function_declarations": false, - "ignores_comments": false] + let config3: [String: Bool] = [ + "ignores_urls": false, + "ignores_function_declarations": false, + "ignores_comments": false, + ] do { try configuration.apply(configuration: config1) diff --git a/Tests/SwiftLintFrameworkTests/LineLengthRuleTests.swift b/Tests/SwiftLintFrameworkTests/LineLengthRuleTests.swift index f4a97ec9cd..497ce3b6af 100644 --- a/Tests/SwiftLintFrameworkTests/LineLengthRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/LineLengthRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class LineLengthRuleTests: SwiftLintTestCase { +final class LineLengthRuleTests: SwiftLintTestCase { private let longFunctionDeclarations = [ Example("public func superDuperLongFunctionDeclaration(a: String, b: String, " + "c: String, d: String, e: String, f: String, g: String, h: String, i: String, " + @@ -11,7 +11,7 @@ class LineLengthRuleTests: SwiftLintTestCase { "c: String, d: String, e: String, f: String, g: String, h: String, i: String, " + "j: String, k: String, l: String, m: String, n: String, o: String, p: String, " + "q: String, r: String, s: String, t: String, u: String, v: String, w: String, " + - "x: String, y: String, z: String) {\n") + "x: String, y: String, z: String) {\n"), ] private let longComment = Example(String(repeating: "/", count: 121) + "\n") @@ -50,7 +50,7 @@ class LineLengthRuleTests: SwiftLintTestCase { let triggeringLines = [Example(String(repeating: "/", count: 121) + "\(url)\n")] let nonTriggeringLines = [ Example("\(url) " + String(repeating: "/", count: 118) + " \(url)\n"), - Example("\(url)/" + String(repeating: "a", count: 120)) + Example("\(url)/" + String(repeating: "a", count: 120)), ] let baseDescription = LineLengthRule.description diff --git a/Tests/SwiftLintFrameworkTests/LintOrAnalyzeOptionsTests.swift b/Tests/SwiftLintFrameworkTests/LintOrAnalyzeOptionsTests.swift new file mode 100644 index 0000000000..07ec490791 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/LintOrAnalyzeOptionsTests.swift @@ -0,0 +1,72 @@ +@testable import SwiftLintFramework +import XCTest + +final class LintOrAnalyzeOptionsTests: XCTestCase { + private typealias Leniency = LintOrAnalyzeOptions.Leniency + + func testLeniency() { + let parameters = [ + Leniency(strict: false, lenient: false), + Leniency(strict: true, lenient: true), + Leniency(strict: true, lenient: false), + Leniency(strict: false, lenient: true), + ] + + for commandLine in parameters { + let options = LintOrAnalyzeOptions(leniency: commandLine) + for configuration in parameters { + let leniency = options.leniency(strict: configuration.strict, lenient: configuration.lenient) + if commandLine.strict { + // Command line takes precedence. + XCTAssertTrue(leniency.strict) + if !commandLine.lenient { + // `--strict` should disable configuration lenience. + XCTAssertFalse(leniency.lenient) + } + } else if commandLine.lenient { + // Command line takes precedence, and should override + // `strict` in the configuration. + XCTAssertTrue(leniency.lenient) + XCTAssertFalse(leniency.strict) + } else if configuration.strict { + XCTAssertTrue(leniency.strict) + } else if configuration.lenient { + XCTAssertTrue(leniency.lenient) + } + } + } + } +} + +private extension LintOrAnalyzeOptions { + init(leniency: Leniency) { + self.init(mode: .lint, + paths: [], + useSTDIN: true, + configurationFiles: [], + strict: leniency.strict, + lenient: leniency.lenient, + forceExclude: false, + useExcludingByPrefix: false, + useScriptInputFiles: false, + useScriptInputFileLists: false, + benchmark: false, + reporter: nil, + baseline: nil, + writeBaseline: nil, + workingDirectory: nil, + quiet: false, + output: nil, + progress: false, + cachePath: nil, + ignoreCache: false, + enableAllRules: false, + onlyRule: [], + autocorrect: false, + format: false, + compilerLogPath: nil, + compileCommands: nil, + checkForUpdates: false + ) + } +} diff --git a/Tests/SwiftLintFrameworkTests/LinterCacheTests.swift b/Tests/SwiftLintFrameworkTests/LinterCacheTests.swift index 60cada91e7..3191a6a9f1 100644 --- a/Tests/SwiftLintFrameworkTests/LinterCacheTests.swift +++ b/Tests/SwiftLintFrameworkTests/LinterCacheTests.swift @@ -11,7 +11,7 @@ private struct CacheTestHelper { private var fileManager: TestFileManager { // swiftlint:disable:next force_cast - return cache.fileManager as! TestFileManager + cache.fileManager as! TestFileManager } fileprivate init(dict: [String: Any], cache: LinterCache) { @@ -31,12 +31,12 @@ private struct CacheTestHelper { StyleViolation(ruleDescription: ruleDescription, severity: .error, location: Location(file: file, line: 5, character: nil), - reason: "Something is wrong") + reason: "Something is wrong"), ] } fileprivate func makeConfig(dict: [String: Any]) -> Configuration { - return try! Configuration(dict: dict, ruleList: ruleList) // swiftlint:disable:this force_try + try! Configuration(dict: dict, ruleList: ruleList) // swiftlint:disable:this force_try } fileprivate func touch(file: String) { @@ -48,37 +48,40 @@ private struct CacheTestHelper { } fileprivate func fileCount() -> Int { - return fileManager.stubbedModificationDateByPath.count + fileManager.stubbedModificationDateByPath.count } } private class TestFileManager: LintableFileManager { - fileprivate func filesToLint(inPath: String, rootDirectory: String? = nil) -> [String] { - return [] + fileprivate func filesToLint(inPath _: String, rootDirectory _: String? = nil) -> [String] { + [] } fileprivate var stubbedModificationDateByPath = [String: Date]() fileprivate func modificationDate(forFileAtPath path: String) -> Date? { - return stubbedModificationDateByPath[path] + stubbedModificationDateByPath[path] } - fileprivate func isFile(atPath path: String) -> Bool { + fileprivate func isFile(atPath _: String) -> Bool { false } } -class LinterCacheTests: SwiftLintTestCase { +final class LinterCacheTests: SwiftLintTestCase { // MARK: Test Helpers private var cache = LinterCache(fileManager: TestFileManager()) private func makeCacheTestHelper(dict: [String: Any]) -> CacheTestHelper { - return CacheTestHelper(dict: dict, cache: cache) + CacheTestHelper(dict: dict, cache: cache) } - private func cacheAndValidate(violations: [StyleViolation], forFile: String, configuration: Configuration, - file: StaticString = #file, line: UInt = #line) { + private func cacheAndValidate(violations: [StyleViolation], + forFile: String, + configuration: Configuration, + file: StaticString = #filePath, + line: UInt = #line) { cache.cache(violations: violations, forFile: forFile, configuration: configuration) cache = cache.flushed() XCTAssertEqual(cache.violations(forFile: forFile, configuration: configuration)!, @@ -86,7 +89,8 @@ class LinterCacheTests: SwiftLintTestCase { } private func cacheAndValidateNoViolationsTwoFiles(configuration: Configuration, - file: StaticString = #file, line: UInt = #line) { + file: StaticString = #filePath, + line: UInt = #line) { let (file1, file2) = ("file1.swift", "file2.swift") // swiftlint:disable:next force_cast let fileManager = cache.fileManager as! TestFileManager @@ -96,8 +100,10 @@ class LinterCacheTests: SwiftLintTestCase { cacheAndValidate(violations: [], forFile: file2, configuration: configuration, file: file, line: line) } - private func validateNewConfigDoesntHitCache(dict: [String: Any], initialConfig: Configuration, - file: StaticString = #file, line: UInt = #line) throws { + private func validateNewConfigDoesntHitCache(dict: [String: Any], + initialConfig: Configuration, + file: StaticString = #filePath, + line: UInt = #line) throws { let newConfig = try Configuration(dict: dict) let (file1, file2) = ("file1.swift", "file2.swift") @@ -195,7 +201,7 @@ class LinterCacheTests: SwiftLintTestCase { let initialConfig = try Configuration( dict: [ "only_rules": ["custom_rules", "rule1"], - "custom_rules": ["rule1": ["regex": "([n,N]inja)"]] + "custom_rules": ["rule1": ["regex": "([n,N]inja)"]], ], ruleList: RuleList(rules: CustomRules.self) ) @@ -205,7 +211,7 @@ class LinterCacheTests: SwiftLintTestCase { try validateNewConfigDoesntHitCache( dict: [ "only_rules": ["custom_rules", "rule1"], - "custom_rules": ["rule1": ["regex": "([n,N]injas)"]] + "custom_rules": ["rule1": ["regex": "([n,N]injas)"]], ], initialConfig: initialConfig ) @@ -214,7 +220,7 @@ class LinterCacheTests: SwiftLintTestCase { try validateNewConfigDoesntHitCache( dict: [ "only_rules": ["custom_rules", "rule1"], - "custom_rules": ["rule1": ["regex": "([n,N]injas)"], "rule2": ["regex": "([k,K]ittens)"]] + "custom_rules": ["rule1": ["regex": "([n,N]injas)"], "rule2": ["regex": "([k,K]ittens)"]], ], initialConfig: initialConfig ) diff --git a/Tests/SwiftLintFrameworkTests/MissingDocsRuleTests.swift b/Tests/SwiftLintFrameworkTests/MissingDocsRuleTests.swift index f8480e5214..531373d1bc 100644 --- a/Tests/SwiftLintFrameworkTests/MissingDocsRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/MissingDocsRuleTests.swift @@ -1,13 +1,14 @@ @testable import SwiftLintBuiltInRules import XCTest -class MissingDocsRuleTests: SwiftLintTestCase { +final class MissingDocsRuleTests: SwiftLintTestCase { func testDescriptionEmpty() { let configuration = MissingDocsConfiguration() XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "warning: [open, public]; excludes_extensions: true; " + - "excludes_inherited_types: true; excludes_trivial_init: false" + "excludes_inherited_types: true; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: false" ) } @@ -16,7 +17,8 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "warning: [open, public]; excludes_extensions: false; " + - "excludes_inherited_types: false; excludes_trivial_init: false" + "excludes_inherited_types: false; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: false" ) } @@ -25,16 +27,22 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "warning: [open, public]; excludes_extensions: false; " + - "excludes_inherited_types: true; excludes_trivial_init: false" + "excludes_inherited_types: true; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: false" ) } func testDescriptionExcludesExtensionsTrueExcludesInheritedTypesFalse() { - let configuration = MissingDocsConfiguration(excludesExtensions: true, excludesInheritedTypes: false) + let configuration = MissingDocsConfiguration( + excludesExtensions: true, + excludesInheritedTypes: false, + evaluateEffectiveAccessControlLevel: true + ) XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "warning: [open, public]; excludes_extensions: true; " + - "excludes_inherited_types: false; excludes_trivial_init: false" + "excludes_inherited_types: false; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: true" ) } @@ -44,29 +52,38 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "error: [open]; excludes_extensions: true; " + - "excludes_inherited_types: true; excludes_trivial_init: false" + "excludes_inherited_types: true; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: false" ) } func testDescriptionMultipleSeverities() { let configuration = MissingDocsConfiguration( - parameters: [RuleParameter(severity: .error, value: .open), - RuleParameter(severity: .warning, value: .public)]) + parameters: [ + RuleParameter(severity: .error, value: .open), + RuleParameter(severity: .warning, value: .public), + ] + ) XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "error: [open]; warning: [public]; excludes_extensions: true; " + - "excludes_inherited_types: true; excludes_trivial_init: false" + "excludes_inherited_types: true; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: false" ) } func testDescriptionMultipleAcls() { let configuration = MissingDocsConfiguration( - parameters: [RuleParameter(severity: .warning, value: .open), - RuleParameter(severity: .warning, value: .public)]) + parameters: [ + RuleParameter(severity: .warning, value: .open), + RuleParameter(severity: .warning, value: .public), + ] + ) XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "warning: [open, public]; excludes_extensions: true; " + - "excludes_inherited_types: true; excludes_trivial_init: false" + "excludes_inherited_types: true; excludes_trivial_init: false; " + + "evaluate_effective_access_control_level: false" ) } @@ -75,7 +92,8 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertEqual( configuration.parameterDescription?.oneLiner(), "warning: [open, public]; excludes_extensions: true; " + - "excludes_inherited_types: true; excludes_trivial_init: true" + "excludes_inherited_types: true; excludes_trivial_init: true; " + + "evaluate_effective_access_control_level: false" ) } @@ -93,8 +111,10 @@ class MissingDocsRuleTests: SwiftLintTestCase { try? configuration.apply(configuration: ["warning": "public", "error": "open"]) XCTAssertEqual( configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue }, - [RuleParameter(severity: .warning, value: .public), - RuleParameter(severity: .error, value: .open)] + [ + RuleParameter(severity: .warning, value: .public), + RuleParameter(severity: .error, value: .open), + ] ) } @@ -103,8 +123,10 @@ class MissingDocsRuleTests: SwiftLintTestCase { try? configuration.apply(configuration: ["warning": ["public", "open"]]) XCTAssertEqual( configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue }, - [RuleParameter(severity: .warning, value: .public), - RuleParameter(severity: .warning, value: .open)] + [ + RuleParameter(severity: .warning, value: .public), + RuleParameter(severity: .warning, value: .open), + ] ) XCTAssertTrue(configuration.excludesExtensions) XCTAssertTrue(configuration.excludesInheritedTypes) @@ -122,8 +144,10 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertTrue(configuration.excludesInheritedTypes) XCTAssertEqual( configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue }, - [RuleParameter(severity: .warning, value: .public), - RuleParameter(severity: .warning, value: .open)] + [ + RuleParameter(severity: .warning, value: .public), + RuleParameter(severity: .warning, value: .open), + ] ) } @@ -141,8 +165,10 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertFalse(configuration.excludesInheritedTypes) XCTAssertEqual( configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue }, - [RuleParameter(severity: .warning, value: .public), - RuleParameter(severity: .warning, value: .open)] + [ + RuleParameter(severity: .warning, value: .public), + RuleParameter(severity: .warning, value: .open), + ] ) } @@ -153,8 +179,10 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertTrue(configuration.excludesInheritedTypes) XCTAssertEqual( configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue }, - [RuleParameter(severity: .warning, value: .public), - RuleParameter(severity: .warning, value: .open)] + [ + RuleParameter(severity: .warning, value: .public), + RuleParameter(severity: .warning, value: .open), + ] ) } @@ -165,8 +193,10 @@ class MissingDocsRuleTests: SwiftLintTestCase { XCTAssertFalse(configuration.excludesInheritedTypes) XCTAssertEqual( configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue }, - [RuleParameter(severity: .warning, value: .public), - RuleParameter(severity: .warning, value: .open)] + [ + RuleParameter(severity: .warning, value: .public), + RuleParameter(severity: .warning, value: .open), + ] ) } @@ -176,7 +206,7 @@ class MissingDocsRuleTests: SwiftLintTestCase { configuration: [ "excludes_extensions": true, "excludes_inherited_types": false, - "error": ["public"] + "error": ["public"], ] as [String: any Sendable] ) @@ -187,56 +217,4 @@ class MissingDocsRuleTests: SwiftLintTestCase { [RuleParameter(severity: .error, value: .public)] ) } - - func testWithExcludesExtensionsDisabled() { - // Perform additional tests with the ignores_comments settings disabled. - let baseDescription = MissingDocsRule.description - let triggeringComments = [ - Example(""" - public extension A {} - """ - ) - ] - let nonTriggeringExamples = baseDescription.nonTriggeringExamples - .filter { !triggeringComments.contains($0) } - let triggeringExamples = baseDescription.triggeringExamples + triggeringComments - let description = baseDescription - .with(nonTriggeringExamples: nonTriggeringExamples) - .with(triggeringExamples: triggeringExamples) - verifyRule(description, - ruleConfiguration: ["excludes_extensions": false]) - } - - func testWithExcludesInheritedTypesDisabled() { - // Perform additional tests with the ignores_comments settings disabled. - let baseDescription = MissingDocsRule.description - let triggeringComments = [ - // locally-defined superclass member is documented, but subclass member is not - Example(""" - /// docs - public class A { - /// docs - public func b() {} - } - // no docs - public class B: A { override public func b() {} } - """), - // externally-defined superclass member is documented, but subclass member is not - Example(""" - import Foundation - // no docs - public class B: NSObject { - // no docs - override public var description: String { fatalError() } } - """) - ] - let nonTriggeringExamples = baseDescription.nonTriggeringExamples - .filter { !triggeringComments.contains($0) } - let triggeringExamples = baseDescription.triggeringExamples + triggeringComments - let description = baseDescription - .with(nonTriggeringExamples: nonTriggeringExamples) - .with(triggeringExamples: triggeringExamples) - verifyRule(description, - ruleConfiguration: ["excludes_inherited_types": false]) - } } diff --git a/Tests/SwiftLintFrameworkTests/ModifierOrderTests.swift b/Tests/SwiftLintFrameworkTests/ModifierOrderTests.swift index 86bd3f45a6..fdcff1f16f 100644 --- a/Tests/SwiftLintFrameworkTests/ModifierOrderTests.swift +++ b/Tests/SwiftLintFrameworkTests/ModifierOrderTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class ModifierOrderTests: SwiftLintTestCase { +final class ModifierOrderTests: SwiftLintTestCase { func testAttributeTypeMethod() { let descriptionOverride = ModifierOrderRule.description .with(nonTriggeringExamples: [ @@ -14,7 +14,7 @@ class ModifierOrderTests: SwiftLintTestCase { public class SomeClass { static public func someFunc() {} } - """) + """), ]) .with(triggeringExamples: [ Example(""" @@ -26,7 +26,7 @@ class ModifierOrderTests: SwiftLintTestCase { public class SomeClass { public static func someFunc() {} } - """) + """), ]) .with(corrections: [:]) @@ -43,7 +43,7 @@ class ModifierOrderTests: SwiftLintTestCase { " fileprivate static func bar() {} \n" + " open class func barFoo() {} }"), Example("public struct Foo {" + - " private mutating func bar() {} }") + " private mutating func bar() {} }"), ]) .with(triggeringExamples: [ Example("public protocol Foo: class {} \n" + @@ -52,18 +52,24 @@ class ModifierOrderTests: SwiftLintTestCase { " static fileprivate func bar() {} \n" + " class open func barFoo() {} }"), Example("public struct Foo {" + - " mutating private func bar() {} }") + " mutating private func bar() {} }"), ]) .with(corrections: [:]) - verifyRule(descriptionOverride, - ruleConfiguration: ["preferred_modifier_order": ["acl", - "typeMethods", - "owned", - "setterACL", - "final", - "mutators", - "override"]]) + verifyRule( + descriptionOverride, + ruleConfiguration: [ + "preferred_modifier_order": [ + "acl", + "typeMethods", + "owned", + "setterACL", + "final", + "mutators", + "override", + ], + ] + ) } // swiftlint:disable:next function_body_length @@ -112,7 +118,7 @@ class ModifierOrderTests: SwiftLintTestCase { public class Foo { @NSCopying public final var foo: NSString } - """#) + """#), ]) .with(triggeringExamples: [ Example(#""" @@ -156,7 +162,7 @@ class ModifierOrderTests: SwiftLintTestCase { public class Foo { @NSManaged final public var foo: NSString } - """) + """), ]) .with(corrections: [:]) @@ -186,7 +192,7 @@ class ModifierOrderTests: SwiftLintTestCase { class Foo { final override private weak var bar: UIView? } - """) + """), ]) .with(triggeringExamples: [ Example(""" @@ -208,7 +214,7 @@ class ModifierOrderTests: SwiftLintTestCase { class Foo { override final private weak var bar: UIView? } - """) + """), ]) .with(corrections: [:]) @@ -276,7 +282,7 @@ class ModifierOrderTests: SwiftLintTestCase { """): Example(""" final private class Foo {} - """) + """), ]) verifyRule(descriptionOverride, @@ -347,7 +353,7 @@ class ModifierOrderTests: SwiftLintTestCase { class Foo { var bar: UIView? } - """) + """), ]) verifyRule(descriptionOverride, @@ -369,7 +375,7 @@ class ModifierOrderTests: SwiftLintTestCase { """): Example(""" public protocol Foo: class {}\n - """) + """), ]) verifyRule(descriptionOverride, @@ -377,7 +383,7 @@ class ModifierOrderTests: SwiftLintTestCase { } func testViolationMessage() { - let ruleID = ModifierOrderRule.description.identifier + let ruleID = ModifierOrderRule.identifier guard let config = makeConfig(["preferred_modifier_order": ["acl", "final"]], ruleID) else { XCTFail("Failed to create configuration") return diff --git a/Tests/SwiftLintFrameworkTests/MultilineArgumentsRuleTests.swift b/Tests/SwiftLintFrameworkTests/MultilineArgumentsRuleTests.swift index 5704e7a72a..2f66f76dbc 100644 --- a/Tests/SwiftLintFrameworkTests/MultilineArgumentsRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/MultilineArgumentsRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class MultilineArgumentsRuleTests: SwiftLintTestCase { +final class MultilineArgumentsRuleTests: SwiftLintTestCase { func testMultilineArgumentsWithWithNextLine() { let nonTriggeringExamples = [ Example("foo()"), @@ -11,12 +11,12 @@ class MultilineArgumentsRuleTests: SwiftLintTestCase { " 3,\n" + " bar: baz) { }"), Example("foo(\n" + - " 4, bar: baz) { }") + " 4, bar: baz) { }"), ] let triggeringExamples = [ Example("foo(↓1,\n" + - " bar: baz) { }") + " bar: baz) { }"), ] let description = MultilineArgumentsRule.description @@ -35,7 +35,7 @@ class MultilineArgumentsRuleTests: SwiftLintTestCase { " bar()\n" + "}"), Example("foo(3,\n" + - " bar: 3) { }") + " bar: 3) { }"), ] let triggeringExamples = [ @@ -43,7 +43,7 @@ class MultilineArgumentsRuleTests: SwiftLintTestCase { " ↓1, ↓bar: baz) { }"), Example("foo(\n" + " ↓2,\n" + - " bar: baz) { }") + " bar: baz) { }"), ] let description = MultilineArgumentsRule.description @@ -76,7 +76,7 @@ class MultilineArgumentsRuleTests: SwiftLintTestCase { "})"), Example("foo(a: a, b: { [weak self] in\n" + "}, c: { flag in\n" + - "})") + "})"), ] let triggeringExamples = [ @@ -86,7 +86,7 @@ class MultilineArgumentsRuleTests: SwiftLintTestCase { Example("foo(a: a, b: b,\n" + " c: c, d: {\n" + " }, d: {\n" + - "})") + "})"), ] let description = MultilineArgumentsRule.description diff --git a/Tests/SwiftLintFrameworkTests/MultilineParametersConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/MultilineParametersConfigurationTests.swift new file mode 100644 index 0000000000..2114d6409e --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/MultilineParametersConfigurationTests.swift @@ -0,0 +1,39 @@ +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import XCTest + +final class MultilineParametersConfigurationTests: SwiftLintTestCase { + func testInvalidMaxNumberOfSingleLineParameters() throws { + for maxNumberOfSingleLineParameters in [0, -1] { + var config = MultilineParametersConfiguration() + + XCTAssertEqual( + try Issue.captureConsole { + try config.apply( + configuration: ["max_number_of_single_line_parameters": maxNumberOfSingleLineParameters] + ) + }, + """ + warning: Inconsistent configuration for 'multiline_parameters' rule: Option \ + 'max_number_of_single_line_parameters' should be >= 1. + """ + ) + } + } + + func testInvalidMaxNumberOfSingleLineParametersWithSingleLineEnabled() throws { + var config = MultilineParametersConfiguration() + + XCTAssertEqual( + try Issue.captureConsole { + try config.apply( + configuration: ["max_number_of_single_line_parameters": 2, "allows_single_line": false] + ) + }, + """ + warning: Inconsistent configuration for 'multiline_parameters' rule: Option \ + 'max_number_of_single_line_parameters' has no effect when 'allows_single_line' is false. + """ + ) + } +} diff --git a/Tests/SwiftLintFrameworkTests/NameConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/NameConfigurationTests.swift index 6dc803762f..fc39b2bf29 100644 --- a/Tests/SwiftLintFrameworkTests/NameConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/NameConfigurationTests.swift @@ -1,15 +1,17 @@ @testable import SwiftLintBuiltInRules import XCTest -class NameConfigurationTests: SwiftLintTestCase { +final class NameConfigurationTests: SwiftLintTestCase { typealias TesteeType = NameConfiguration func testNameConfigurationSetsCorrectly() { - let config = [ "min_length": ["warning": 17, "error": 7], - "max_length": ["warning": 170, "error": 700], - "excluded": "id", - "allowed_symbols": ["$"], - "validates_start_with_lowercase": "warning"] as [String: any Sendable] + let config: [String: any Sendable] = [ + "min_length": ["warning": 17, "error": 7], + "max_length": ["warning": 170, "error": 700], + "excluded": "id", + "allowed_symbols": ["$"], + "validates_start_with_lowercase": "warning", + ] var nameConfig = TesteeType(minLengthWarning: 0, minLengthError: 0, maxLengthWarning: 0, @@ -65,7 +67,7 @@ class NameConfigurationTests: SwiftLintTestCase { minLengthError: 0, maxLengthWarning: 0, maxLengthError: 0) - checkError(Issue.unknownConfiguration(ruleID: RuleMock.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: RuleMock.identifier)) { try nameConfig.apply(configuration: config) } } diff --git a/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift b/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift index 0fee8ca53a..435ca660be 100644 --- a/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/NestingRuleTests.swift @@ -5,7 +5,7 @@ private let detectingTypes = ["actor", "class", "struct", "enum"] // swiftlint:disable:next type_body_length -class NestingRuleTests: SwiftLintTestCase { +final class NestingRuleTests: SwiftLintTestCase { // swiftlint:disable:next function_body_length func testNestingWithAlwaysAllowOneTypeInFunctions() { var nonTriggeringExamples = NestingRule.description.nonTriggeringExamples @@ -41,7 +41,7 @@ class NestingRuleTests: SwiftLintTestCase { \(type) Example_1 {} } } - """) + """), ] }) nonTriggeringExamples.append(contentsOf: detectingTypes.flatMap { type -> [Example] in @@ -92,7 +92,7 @@ class NestingRuleTests: SwiftLintTestCase { } } } - """) + """), ] }) @@ -134,10 +134,11 @@ class NestingRuleTests: SwiftLintTestCase { } } } - """) + """), ] } + // swiftlint:disable:next closure_body_length triggeringExamples.append(contentsOf: detectingTypes.flatMap { type -> [Example] in [ .init(""" @@ -194,12 +195,12 @@ class NestingRuleTests: SwiftLintTestCase { } } } - """) + """), ] }) let description = RuleDescription( - identifier: NestingRule.description.identifier, + identifier: NestingRule.identifier, name: NestingRule.description.name, description: NestingRule.description.description, kind: .metrics, @@ -213,6 +214,7 @@ class NestingRuleTests: SwiftLintTestCase { // swiftlint:disable:next function_body_length func testNestingWithoutCheckNestingInClosuresAndStatements() { var nonTriggeringExamples = NestingRule.description.nonTriggeringExamples + // swiftlint:disable:next closure_body_length nonTriggeringExamples.append(contentsOf: detectingTypes.flatMap { type -> [Example] in [ .init(""" @@ -382,10 +384,11 @@ class NestingRuleTests: SwiftLintTestCase { } } } - """) + """), ] }) + // swiftlint:disable:next closure_body_length var triggeringExamples = detectingTypes.flatMap { type -> [Example] in [ .init(""" @@ -440,7 +443,7 @@ class NestingRuleTests: SwiftLintTestCase { } } } - """) + """), ] } @@ -492,11 +495,11 @@ class NestingRuleTests: SwiftLintTestCase { } } } - """) + """), ]) let description = RuleDescription( - identifier: NestingRule.description.identifier, + identifier: NestingRule.identifier, name: NestingRule.description.name, description: NestingRule.description.description, kind: .metrics, @@ -543,7 +546,7 @@ class NestingRuleTests: SwiftLintTestCase { typealias AssociatedType = Int } } - """) + """), ] }) diff --git a/Tests/SwiftLintFrameworkTests/NoEmptyBlockConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/NoEmptyBlockConfigurationTests.swift new file mode 100644 index 0000000000..22b22f58d6 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/NoEmptyBlockConfigurationTests.swift @@ -0,0 +1,54 @@ +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import XCTest + +final class NoEmptyBlockConfigurationTests: SwiftLintTestCase { + func testDefaultConfiguration() { + let config = NoEmptyBlockConfiguration() + XCTAssertEqual(config.severityConfiguration.severity, .warning) + XCTAssertEqual(config.enabledBlockTypes, NoEmptyBlockConfiguration.CodeBlockType.all) + } + + func testApplyingCustomConfiguration() throws { + var config = NoEmptyBlockConfiguration() + try config.apply( + configuration: [ + "severity": "error", + "disabled_block_types": ["function_bodies"], + ] as [String: any Sendable] + ) + XCTAssertEqual(config.severityConfiguration.severity, .error) + XCTAssertEqual(config.enabledBlockTypes, Set([.initializerBodies, .statementBlocks, .closureBlocks])) + } + + func testInvalidKeyInCustomConfiguration() { + var config = NoEmptyBlockConfiguration() + XCTAssertEqual( + try Issue.captureConsole { try config.apply(configuration: ["invalidKey": "error"]) }, + "warning: Configuration for 'no_empty_block' rule contains the invalid key(s) 'invalidKey'." + ) + } + + func testInvalidTypeOfCustomConfiguration() { + var config = NoEmptyBlockConfiguration() + checkError(Issue.invalidConfiguration(ruleID: NoEmptyBlockRule.identifier)) { + try config.apply(configuration: "invalidKey") + } + } + + func testInvalidTypeOfValueInCustomConfiguration() { + var config = NoEmptyBlockConfiguration() + checkError(Issue.invalidConfiguration(ruleID: NoEmptyBlockRule.identifier)) { + try config.apply(configuration: ["severity": "foo"]) + } + } + + func testConsoleDescription() throws { + var config = NoEmptyBlockConfiguration() + try config.apply(configuration: ["disabled_block_types": ["initializer_bodies", "statement_blocks"]]) + XCTAssertEqual( + RuleConfigurationDescription.from(configuration: config).oneLiner(), + "severity: warning; disabled_block_types: [initializer_bodies, statement_blocks]" + ) + } +} diff --git a/Tests/SwiftLintFrameworkTests/NumberSeparatorRuleTests.swift b/Tests/SwiftLintFrameworkTests/NumberSeparatorRuleTests.swift index 522fc516ec..e80d93b639 100644 --- a/Tests/SwiftLintFrameworkTests/NumberSeparatorRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/NumberSeparatorRuleTests.swift @@ -2,24 +2,24 @@ import SwiftParser import XCTest -class NumberSeparatorRuleTests: SwiftLintTestCase { +final class NumberSeparatorRuleTests: SwiftLintTestCase { func testNumberSeparatorWithMinimumLength() { let nonTriggeringExamples = [ Example("let foo = 10_000"), Example("let foo = 1000"), Example("let foo = 1000.0001"), Example("let foo = 10_000.0001"), - Example("let foo = 1000.00001") + Example("let foo = 1000.00001"), ] let triggeringExamples = [ Example("let foo = ↓1_000"), Example("let foo = ↓1.000_1"), - Example("let foo = ↓1_000.000_1") + Example("let foo = ↓1_000.000_1"), ] let corrections = [ Example("let foo = ↓1_000"): Example("let foo = 1000"), Example("let foo = ↓1.000_1"): Example("let foo = 1.0001"), - Example("let foo = ↓1_000.000_1"): Example("let foo = 1000.0001") + Example("let foo = ↓1_000.000_1"): Example("let foo = 1000.0001"), ] let description = NumberSeparatorRule.description @@ -35,17 +35,17 @@ class NumberSeparatorRuleTests: SwiftLintTestCase { Example("let foo = 1_000.000_000_1"), Example("let foo = 1.000_001"), Example("let foo = 100.0001"), - Example("let foo = 1_000.000_01") + Example("let foo = 1_000.000_01"), ] let triggeringExamples = [ Example("let foo = ↓1000"), Example("let foo = ↓1.000_1"), - Example("let foo = ↓1_000.000_1") + Example("let foo = ↓1_000.000_1"), ] let corrections = [ Example("let foo = ↓1000"): Example("let foo = 1_000"), Example("let foo = ↓1.000_1"): Example("let foo = 1.0001"), - Example("let foo = ↓1_000.000_1"): Example("let foo = 1_000.0001") + Example("let foo = ↓1_000.000_1"): Example("let foo = 1_000.0001"), ] let description = NumberSeparatorRule.description @@ -67,19 +67,19 @@ class NumberSeparatorRuleTests: SwiftLintTestCase { Example("let foo = 2.10042"), Example("let foo = 2.100_42"), Example("let foo = 2.833333"), - Example("let foo = 2.833_333") + Example("let foo = 2.833_333"), ] let triggeringExamples = [ Example("let foo = ↓1000"), Example("let foo = ↓2100"), Example("let foo = ↓1.920442"), - Example("let foo = ↓3.343434") + Example("let foo = ↓3.343434"), ] let corrections = [ Example("let foo = ↓1000"): Example("let foo = 1_000"), Example("let foo = ↓2100"): Example("let foo = 2_100"), Example("let foo = ↓1.920442"): Example("let foo = 1.920_442"), - Example("let foo = ↓3.343434"): Example("let foo = 3.343_434") + Example("let foo = ↓3.343434"): Example("let foo = 3.343_434"), ] let description = NumberSeparatorRule.description @@ -92,9 +92,9 @@ class NumberSeparatorRuleTests: SwiftLintTestCase { ruleConfiguration: [ "exclude_ranges": [ ["min": 1900, "max": 2030], - ["min": 2.0, "max": 3.0] + ["min": 2.0, "max": 3.0], ] as Any, - "minimum_fraction_length": 3 + "minimum_fraction_length": 3, ] as Any ) } diff --git a/Tests/SwiftLintFrameworkTests/ObjectLiteralRuleTests.swift b/Tests/SwiftLintFrameworkTests/ObjectLiteralRuleTests.swift index 64a42fddc6..bfdb9ba375 100644 --- a/Tests/SwiftLintFrameworkTests/ObjectLiteralRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/ObjectLiteralRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class ObjectLiteralRuleTests: SwiftLintTestCase { +final class ObjectLiteralRuleTests: SwiftLintTestCase { // MARK: - Instance Properties private let imageLiteralTriggeringExamples = ["", ".init"].flatMap { (method: String) -> [Example] in ["UI", "NS"].flatMap { (prefix: String) -> [Example] in @@ -15,13 +15,13 @@ class ObjectLiteralRuleTests: SwiftLintTestCase { [ Example("let color = ↓\(prefix)Color\(method)(red: 0.3, green: 0.3, blue: 0.3, alpha: 1)"), Example("let color = ↓\(prefix)Color\(method)(red: 100 / 255.0, green: 50 / 255.0, blue: 0, alpha: 1)"), - Example("let color = ↓\(prefix)Color\(method)(white: 0.5, alpha: 1)") + Example("let color = ↓\(prefix)Color\(method)(white: 0.5, alpha: 1)"), ] } } private var allTriggeringExamples: [Example] { - return imageLiteralTriggeringExamples + colorLiteralTriggeringExamples + imageLiteralTriggeringExamples + colorLiteralTriggeringExamples } // MARK: - Test Methods diff --git a/Tests/SwiftLintFrameworkTests/OpeningBraceRuleTests.swift b/Tests/SwiftLintFrameworkTests/OpeningBraceRuleTests.swift index bdc0a0b943..445b2afd44 100644 --- a/Tests/SwiftLintFrameworkTests/OpeningBraceRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/OpeningBraceRuleTests.swift @@ -1,25 +1,120 @@ @testable import SwiftLintBuiltInRules -class OpeningBraceRuleTests: SwiftLintTestCase { - func testDefaultExamplesRunInMultilineMode() { +final class OpeningBraceRuleTests: SwiftLintTestCase { + func testDefaultNonTriggeringExamplesWithMultilineOptionsTrue() { let description = OpeningBraceRule.description - .with(triggeringExamples: OpeningBraceRule.description.triggeringExamples.removing([ - Example("func abc(a: A,\n\tb: B)\n↓{"), - Example(""" - internal static func getPointer() - -> UnsafeMutablePointer<_ThreadLocalStorage> - ↓{ - return _swift_stdlib_threadLocalStorageGet().assumingMemoryBound( - to: _ThreadLocalStorage.self) - } - """) - ])) + .with(triggeringExamples: []) + .with(corrections: [:]) + + verifyRule(description, ruleConfiguration: [ + "ignore_multiline_statement_conditions": true, + "ignore_multiline_type_headers": true, + "ignore_multiline_function_signatures": true, + ]) + } + + func testWithIgnoreMultilineTypeHeadersTrue() { + let nonTriggeringExamples = [ + Example(""" + extension A + where B: Equatable + {} + """), + Example(""" + struct S: Comparable, + Identifiable + { + init() {} + } + """), + ] + + let triggeringExamples = [ + Example(""" + struct S + ↓{} + """), + Example(""" + extension A where B: Equatable + ↓{ + + } + """), + Example(""" + class C + // with comments + ↓{} + """), + ] + + let description = OpeningBraceRule.description + .with(nonTriggeringExamples: nonTriggeringExamples) + .with(triggeringExamples: triggeringExamples) + .with(corrections: [:]) + + verifyRule(description, ruleConfiguration: ["ignore_multiline_type_headers": true]) + } + + func testWithIgnoreMultilineStatementConditionsTrue() { + let nonTriggeringExamples = [ + Example(""" + while + abc + {} + """), + Example(""" + if x { + + } else if + y, + z + { + + } + """), + Example(""" + if + condition1, + let var1 = var1 + {} + """), + ] + + let triggeringExamples = [ + Example(""" + if x + ↓{} + """), + Example(""" + if x { + + } else if y, z + ↓{} + """), + Example(""" + if x { - verifyRule(description, ruleConfiguration: ["allow_multiline_func": true]) + } else + ↓{} + """), + Example(""" + while abc + // comments + ↓{ + } + """), + ] + + let description = OpeningBraceRule.description + .with(nonTriggeringExamples: nonTriggeringExamples) + .with(triggeringExamples: triggeringExamples) + .with(corrections: [:]) + + verifyRule(description, ruleConfiguration: ["ignore_multiline_statement_conditions": true]) } // swiftlint:disable:next function_body_length - func testWithAllowMultilineTrue() { + func testWithIgnoreMultilineFunctionSignaturesTrue() { let nonTriggeringExamples = [ Example(""" func abc( @@ -50,7 +145,7 @@ class OpeningBraceRuleTests: SwiftLintTestCase { } } - """) + """), ] let triggeringExamples = [ @@ -79,7 +174,14 @@ class OpeningBraceRuleTests: SwiftLintTestCase { } } - """) + """), + Example(""" + class C { + init(a: Int) + // with comments + ↓{} + } + """), ] let description = OpeningBraceRule.description @@ -87,7 +189,7 @@ class OpeningBraceRuleTests: SwiftLintTestCase { .with(triggeringExamples: triggeringExamples) .with(corrections: [:]) - verifyRule(description, ruleConfiguration: ["allow_multiline_func": true]) + verifyRule(description, ruleConfiguration: ["ignore_multiline_function_signatures": true]) } } diff --git a/Tests/SwiftLintFrameworkTests/PreferKeyPathRuleTests.swift b/Tests/SwiftLintFrameworkTests/PreferKeyPathRuleTests.swift new file mode 100644 index 0000000000..4b99289fc0 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/PreferKeyPathRuleTests.swift @@ -0,0 +1,29 @@ +@testable import SwiftLintBuiltInRules +import XCTest + +final class PreferKeyPathRuleTests: SwiftLintTestCase { + private static let extendedMode = ["restrict_to_standard_functions": false] + + func testIdentityExpressionInSwift6() throws { + try XCTSkipIf(SwiftVersion.current < .six) + + let description = PreferKeyPathRule.description + .with(nonTriggeringExamples: [ + Example("f.filter { a in b }"), + Example("f.g { $1 }", configuration: Self.extendedMode), + ]) + .with(triggeringExamples: [ + Example("f.compactMap ↓{ $0 }"), + Example("f.map ↓{ a in a }"), + Example("f.g { $0 }", configuration: Self.extendedMode), + ]) + .with(corrections: [ + Example("f.map ↓{ $0 }"): + Example("f.map(\\.self)"), + Example("f.g { $0 }", configuration: Self.extendedMode): + Example("f.g(\\.self)"), + ]) + + verifyRule(description) + } +} diff --git a/Tests/SwiftLintFrameworkTests/PrefixedTopLevelConstantRuleTests.swift b/Tests/SwiftLintFrameworkTests/PrefixedTopLevelConstantRuleTests.swift index 5509fc3bd4..d2102a6db2 100644 --- a/Tests/SwiftLintFrameworkTests/PrefixedTopLevelConstantRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/PrefixedTopLevelConstantRuleTests.swift @@ -4,12 +4,12 @@ final class PrefixedTopLevelConstantRuleTests: SwiftLintTestCase { func testPrivateOnly() { let triggeringExamples = [ Example("private let ↓Foo = 20.0"), - Example("fileprivate let ↓foo = 20.0") + Example("fileprivate let ↓foo = 20.0"), ] let nonTriggeringExamples = [ Example("let Foo = 20.0"), Example("internal let Foo = \"Foo\""), - Example("public let Foo = 20.0") + Example("public let Foo = 20.0"), ] let description = PrefixedTopLevelConstantRule.description diff --git a/Tests/SwiftLintFrameworkTests/PrivateOverFilePrivateRuleTests.swift b/Tests/SwiftLintFrameworkTests/PrivateOverFilePrivateRuleTests.swift index e3cba45751..56a97267ef 100644 --- a/Tests/SwiftLintFrameworkTests/PrivateOverFilePrivateRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/PrivateOverFilePrivateRuleTests.swift @@ -1,17 +1,17 @@ @testable import SwiftLintBuiltInRules -class PrivateOverFilePrivateRuleTests: SwiftLintTestCase { +final class PrivateOverFilePrivateRuleTests: SwiftLintTestCase { func testPrivateOverFilePrivateValidatingExtensions() { let baseDescription = PrivateOverFilePrivateRule.description let triggeringExamples = baseDescription.triggeringExamples + [ Example("↓fileprivate extension String {}"), Example("↓fileprivate \n extension String {}"), - Example("↓fileprivate extension \n String {}") + Example("↓fileprivate extension \n String {}"), ] let corrections = [ Example("↓fileprivate extension String {}"): Example("private extension String {}"), Example("↓fileprivate \n extension String {}"): Example("private \n extension String {}"), - Example("↓fileprivate extension \n String {}"): Example("private extension \n String {}") + Example("↓fileprivate extension \n String {}"): Example("private extension \n String {}"), ] let description = baseDescription.with(nonTriggeringExamples: []) diff --git a/Tests/SwiftLintFrameworkTests/RegexConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/RegexConfigurationTests.swift index 4f0487a43e..5da2c08750 100644 --- a/Tests/SwiftLintFrameworkTests/RegexConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/RegexConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintCore import XCTest -class RegexConfigurationTests: SwiftLintTestCase { +final class RegexConfigurationTests: SwiftLintTestCase { func testShouldValidateIsTrueByDefault() { let config = RegexConfiguration(identifier: "example") XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) @@ -11,7 +11,7 @@ class RegexConfigurationTests: SwiftLintTestCase { var config = RegexConfiguration(identifier: "example") try config.apply(configuration: [ "regex": "try!", - "excluded": "Tests/.*\\.swift" + "excluded": "Tests/.*\\.swift", ]) XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) @@ -24,8 +24,8 @@ class RegexConfigurationTests: SwiftLintTestCase { "regex": "try!", "excluded": [ "^Tests/.*\\.swift", - "^MyFramework/Tests/.*\\.swift" - ] as Any + "^MyFramework/Tests/.*\\.swift", + ] as Any, ]) XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) @@ -37,7 +37,7 @@ class RegexConfigurationTests: SwiftLintTestCase { var config = RegexConfiguration(identifier: "example") try config.apply(configuration: [ "regex": "try!", - "included": "App/.*\\.swift" + "included": "App/.*\\.swift", ]) XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) @@ -51,8 +51,8 @@ class RegexConfigurationTests: SwiftLintTestCase { "regex": "try!", "included": [ "App/.*\\.swift", - "MyFramework/.*\\.swift" - ] as Any + "MyFramework/.*\\.swift", + ] as Any, ]) XCTAssertFalse(config.shouldValidate(filePath: "Tests/file.swift")) @@ -66,12 +66,12 @@ class RegexConfigurationTests: SwiftLintTestCase { "regex": "try!", "included": [ "App/.*\\.swift", - "MyFramework/.*\\.swift" + "MyFramework/.*\\.swift", ] as Any, "excluded": [ "Tests/.*\\.swift", - "App/Fixtures/.*\\.swift" - ] as Any + "App/Fixtures/.*\\.swift", + ] as Any, ]) XCTAssertTrue(config.shouldValidate(filePath: "App/file.swift")) diff --git a/Tests/SwiftLintFrameworkTests/RegionTests.swift b/Tests/SwiftLintFrameworkTests/RegionTests.swift index 714dc8ee59..3935f583c7 100644 --- a/Tests/SwiftLintFrameworkTests/RegionTests.swift +++ b/Tests/SwiftLintFrameworkTests/RegionTests.swift @@ -1,7 +1,7 @@ import SwiftLintCore import XCTest -class RegionTests: SwiftLintTestCase { +final class RegionTests: SwiftLintTestCase { // MARK: Regions From Files func testNoRegionsInEmptyFile() { @@ -41,7 +41,7 @@ class RegionTests: SwiftLintTestCase { disabledRuleIdentifiers: ["rule_id"]), Region(start: Location(file: nil, line: 2, character: 28), end: Location(file: nil, line: .max, character: .max), - disabledRuleIdentifiers: []) + disabledRuleIdentifiers: []), ]) } // enable/disable @@ -53,7 +53,7 @@ class RegionTests: SwiftLintTestCase { disabledRuleIdentifiers: []), Region(start: Location(file: nil, line: 2, character: 29), end: Location(file: nil, line: .max, character: .max), - disabledRuleIdentifiers: ["rule_id"]) + disabledRuleIdentifiers: ["rule_id"]), ]) } } @@ -68,7 +68,7 @@ class RegionTests: SwiftLintTestCase { disabledRuleIdentifiers: ["1", "2", "3"]), Region(start: Location(file: nil, line: 2, character: .max), end: Location(file: nil, line: .max, character: .max), - disabledRuleIdentifiers: []) + disabledRuleIdentifiers: []), ]) } @@ -97,7 +97,7 @@ class RegionTests: SwiftLintTestCase { disabledRuleIdentifiers: ["3"]), Region(start: Location(file: nil, line: 6, character: 22), end: Location(file: nil, line: .max, character: .max), - disabledRuleIdentifiers: []) + disabledRuleIdentifiers: []), ]) } } diff --git a/Tests/SwiftLintFrameworkTests/ReporterTests.swift b/Tests/SwiftLintFrameworkTests/ReporterTests.swift index 10ae241ccb..252881c47f 100644 --- a/Tests/SwiftLintFrameworkTests/ReporterTests.swift +++ b/Tests/SwiftLintFrameworkTests/ReporterTests.swift @@ -4,7 +4,36 @@ import SourceKittenFramework @testable import SwiftLintCore import XCTest -class ReporterTests: SwiftLintTestCase { +final class ReporterTests: SwiftLintTestCase { + private let violations = [ + StyleViolation( + ruleDescription: LineLengthRule.description, + location: Location(file: "filename", line: 1, character: 1), + reason: "Violation Reason 1" + ), + StyleViolation( + ruleDescription: LineLengthRule.description, + severity: .error, + location: Location(file: "filename", line: 1), + reason: "Violation Reason 2" + ), + StyleViolation( + ruleDescription: SyntacticSugarRule.description, + severity: .error, + location: Location( + file: FileManager.default.currentDirectoryPath + "/path/file.swift", + line: 1, + character: 2 + ), + reason: "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array"), + StyleViolation( + ruleDescription: ColonRule.description, + severity: .error, + location: Location(file: nil), + reason: nil + ), + ] + func testReporterFromString() { for reporter in reportersList { XCTAssertEqual(reporter.identifier, reporterFrom(identifier: reporter.identifier).identifier) @@ -12,53 +41,35 @@ class ReporterTests: SwiftLintTestCase { } private func stringFromFile(_ filename: String) -> String { - return SwiftLintFile(path: "\(testResourcesPath)/\(filename)")!.contents - } - - private func generateViolations() -> [StyleViolation] { - let location = Location(file: "filename", line: 1, character: 2) - return [ - StyleViolation(ruleDescription: LineLengthRule.description, - location: location, - reason: "Violation Reason"), - StyleViolation(ruleDescription: LineLengthRule.description, - severity: .error, - location: location, - reason: "Violation Reason"), - StyleViolation(ruleDescription: SyntacticSugarRule.description, - severity: .error, - location: location, - reason: "Shorthand syntactic sugar should be used" + - ", i.e. [Int] instead of Array"), - StyleViolation(ruleDescription: ColonRule.description, - severity: .error, - location: Location(file: nil), - reason: nil) - ] - } - - func testXcodeReporter() { - let expectedOutput = stringFromFile("CannedXcodeReporterOutput.txt") - let result = XcodeReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + SwiftLintFile(path: "\(testResourcesPath)/\(filename)")!.contents } - func testEmojiReporter() { - let expectedOutput = stringFromFile("CannedEmojiReporterOutput.txt") - let result = EmojiReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testXcodeReporter() throws { + try assertEqualContent( + referenceFile: "CannedXcodeReporterOutput.txt", + reporterType: XcodeReporter.self + ) } - func testGitHubActionsLoggingReporter() { - let expectedOutput = stringFromFile("CannedGitHubActionsLoggingReporterOutput.txt") - let result = GitHubActionsLoggingReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testEmojiReporter() throws { + try assertEqualContent( + referenceFile: "CannedEmojiReporterOutput.txt", + reporterType: EmojiReporter.self + ) } - func testGitLabJUnitReporter() { - let expectedOutput = stringFromFile("CannedGitLabJUnitReporterOutput.xml") - let result = GitLabJUnitReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testGitHubActionsLoggingReporter() throws { + try assertEqualContent( + referenceFile: "CannedGitHubActionsLoggingReporterOutput.txt", + reporterType: GitHubActionsLoggingReporter.self + ) + } + + func testGitLabJUnitReporter() throws { + try assertEqualContent( + referenceFile: "CannedGitLabJUnitReporterOutput.xml", + reporterType: GitLabJUnitReporter.self + ) } private func jsonValue(_ jsonString: String) throws -> NSObject { @@ -74,61 +85,75 @@ class ReporterTests: SwiftLintTestCase { } func testJSONReporter() throws { - let expectedOutput = stringFromFile("CannedJSONReporterOutput.json") - let result = JSONReporter.generateReport(generateViolations()) - XCTAssertEqual(try jsonValue(result), try jsonValue(expectedOutput)) + try assertEqualContent( + referenceFile: "CannedJSONReporterOutput.json", + reporterType: JSONReporter.self, + stringConverter: { try jsonValue($0) } + ) } - func testCSVReporter() { - let expectedOutput = stringFromFile("CannedCSVReporterOutput.csv") - let result = CSVReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testCSVReporter() throws { + try assertEqualContent( + referenceFile: "CannedCSVReporterOutput.csv", + reporterType: CSVReporter.self + ) } - func testCheckstyleReporter() { - let expectedOutput = stringFromFile("CannedCheckstyleReporterOutput.xml") - let result = CheckstyleReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testCheckstyleReporter() throws { + try assertEqualContent( + referenceFile: "CannedCheckstyleReporterOutput.xml", + reporterType: CheckstyleReporter.self + ) } - func testCodeClimateReporter() { - let expectedOutput = stringFromFile("CannedCodeClimateReporterOutput.json") - let result = CodeClimateReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testCodeClimateReporter() throws { + try assertEqualContent( + referenceFile: "CannedCodeClimateReporterOutput.json", + reporterType: CodeClimateReporter.self + ) } - func testJunitReporter() { - let expectedOutput = stringFromFile("CannedJunitReporterOutput.xml") - let result = JUnitReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testSARIFReporter() throws { + try assertEqualContent( + referenceFile: "CannedSARIFReporterOutput.json", + reporterType: SARIFReporter.self + ) } - func testHTMLReporter() { - let expectedOutput = stringFromFile("CannedHTMLReporterOutput.html") - let result = HTMLReporter.generateReport( - generateViolations(), - swiftlintVersion: "1.2.3", - dateString: "13/12/2016" + func testJunitReporter() throws { + try assertEqualContent( + referenceFile: "CannedJunitReporterOutput.xml", + reporterType: JUnitReporter.self ) - XCTAssertEqual(result, expectedOutput) } - func testSonarQubeReporter() { - let expectedOutput = stringFromFile("CannedSonarQubeReporterOutput.json") - let result = SonarQubeReporter.generateReport(generateViolations()) - XCTAssertEqual(try jsonValue(result), try jsonValue(expectedOutput)) + func testHTMLReporter() throws { + try assertEqualContent( + referenceFile: "CannedHTMLReporterOutput.html", + reporterType: HTMLReporter.self + ) } - func testMarkdownReporter() { - let expectedOutput = stringFromFile("CannedMarkdownReporterOutput.md") - let result = MarkdownReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testSonarQubeReporter() throws { + try assertEqualContent( + referenceFile: "CannedSonarQubeReporterOutput.json", + reporterType: SonarQubeReporter.self, + stringConverter: { try jsonValue($0) } + ) } - func testRelativePathReporter() { - let expectedOutput = stringFromFile("CannedRelativePathReporterOutput.txt") - let result = RelativePathReporter.generateReport(generateViolations()) - XCTAssertEqual(result, expectedOutput) + func testMarkdownReporter() throws { + try assertEqualContent( + referenceFile: "CannedMarkdownReporterOutput.md", + reporterType: MarkdownReporter.self + ) + } + + func testRelativePathReporter() throws { + try assertEqualContent( + referenceFile: "CannedRelativePathReporterOutput.txt", + reporterType: RelativePathReporter.self + ) } func testRelativePathReporterPaths() { @@ -151,7 +176,7 @@ class ReporterTests: SwiftLintTestCase { location: Location(file: "filename", line: 1, character: 2), reason: "Violation Reason" ) - let result = SummaryReporter.generateReport(generateViolations() + [correctableViolation]) + let result = SummaryReporter.generateReport(violations + [correctableViolation]) XCTAssertEqual(result, expectedOutput) } @@ -161,4 +186,41 @@ class ReporterTests: SwiftLintTestCase { let result = SummaryReporter.generateReport([]) XCTAssertEqual(result, expectedOutput) } + + private func assertEqualContent(referenceFile: String, + reporterType: any Reporter.Type, + stringConverter: (String) throws -> some Equatable = { $0 }, + file: StaticString = #filePath, + line: UInt = #line) throws { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + let reference = stringFromFile(referenceFile).replacingOccurrences( + of: "${CURRENT_WORKING_DIRECTORY}", + with: FileManager.default.currentDirectoryPath + ).replacingOccurrences( + of: "${SWIFTLINT_VERSION}", + with: SwiftLintCore.Version.current.value + ).replacingOccurrences( + of: "${TODAYS_DATE}", + with: dateFormatter.string(from: Date()) + ) + let reporterOutput = reporterType.generateReport(violations) + let convertedReference = try stringConverter(reference) + let convertedReporterOutput = try stringConverter(reporterOutput) + if convertedReference != convertedReporterOutput { + let referenceURL = URL(fileURLWithPath: "\(testResourcesPath)/\(referenceFile)") + try reporterOutput.replacingOccurrences( + of: FileManager.default.currentDirectoryPath, + with: "${CURRENT_WORKING_DIRECTORY}" + ).replacingOccurrences( + of: SwiftLintCore.Version.current.value, + with: "${SWIFTLINT_VERSION}" + ).replacingOccurrences( + of: dateFormatter.string(from: Date()), + with: "${TODAYS_DATE}" + ) + .write(to: referenceURL, atomically: true, encoding: .utf8) + } + XCTAssertEqual(convertedReference, convertedReporterOutput, file: file, line: line) + } } diff --git a/Tests/SwiftLintFrameworkTests/RequiredEnumCaseConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/RequiredEnumCaseConfigurationTests.swift index 6055b59a50..1ec83cbcde 100644 --- a/Tests/SwiftLintFrameworkTests/RequiredEnumCaseConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/RequiredEnumCaseConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class RequiredEnumCaseConfigurationTests: SwiftLintTestCase { +final class RequiredEnumCaseConfigurationTests: SwiftLintTestCase { private typealias RuleConfiguration = RequiredEnumCaseConfiguration private typealias RequiredCase = RuleConfiguration.RequiredCase diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedCSVReporterOutput.csv b/Tests/SwiftLintFrameworkTests/Resources/CannedCSVReporterOutput.csv index 3b59168ff0..2db0eb122f 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedCSVReporterOutput.csv +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedCSVReporterOutput.csv @@ -1,5 +1,5 @@ file,line,character,severity,type,reason,rule_id -filename,1,2,Warning,Line Length,Violation Reason,line_length -filename,1,2,Error,Line Length,Violation Reason,line_length -filename,1,2,Error,Syntactic Sugar,"Shorthand syntactic sugar should be used, i.e. [Int] instead of Array",syntactic_sugar +filename,1,1,Warning,Line Length,Violation Reason 1,line_length +filename,1,,Error,Line Length,Violation Reason 2,line_length +${CURRENT_WORKING_DIRECTORY}/path/file.swift,1,2,Error,Syntactic Sugar,"Shorthand syntactic sugar should be used, i.e. [Int] instead of Array",syntactic_sugar ,,,Error,Colon Spacing,Colons should be next to the identifier when specifying a type and next to the key in dictionary literals,colon \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedCheckstyleReporterOutput.xml b/Tests/SwiftLintFrameworkTests/Resources/CannedCheckstyleReporterOutput.xml index b5cd3c2ccd..320a251ce8 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedCheckstyleReporterOutput.xml +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedCheckstyleReporterOutput.xml @@ -3,9 +3,11 @@ - - - + + + + + \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedCodeClimateReporterOutput.json b/Tests/SwiftLintFrameworkTests/Resources/CannedCodeClimateReporterOutput.json index a091aa6bd7..c6f197c017 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedCodeClimateReporterOutput.json +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedCodeClimateReporterOutput.json @@ -1,9 +1,9 @@ [ { "check_name" : "Line Length", - "description" : "Violation Reason", + "description" : "Violation Reason 1", "engine_name" : "SwiftLint", - "fingerprint" : "917a85854a9500cfd28520fa4875a3ecbba171e522f13452c7adec71fc14497a", + "fingerprint" : "4a17aef14fdc2dbdd95ab2ee78d1b7d6cc289539d290b283cbabedd30e929f5f", "location" : { "lines" : { "begin" : 1, @@ -11,14 +11,14 @@ }, "path" : "filename" }, - "severity" : "MINOR", + "severity" : "minor", "type" : "issue" }, { "check_name" : "Line Length", - "description" : "Violation Reason", + "description" : "Violation Reason 2", "engine_name" : "SwiftLint", - "fingerprint" : "917a85854a9500cfd28520fa4875a3ecbba171e522f13452c7adec71fc14497a", + "fingerprint" : "4a17aef14fdc2dbdd95ab2ee78d1b7d6cc289539d290b283cbabedd30e929f5f", "location" : { "lines" : { "begin" : 1, @@ -26,22 +26,22 @@ }, "path" : "filename" }, - "severity" : "MAJOR", + "severity" : "major", "type" : "issue" }, { "check_name" : "Syntactic Sugar", "description" : "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array", "engine_name" : "SwiftLint", - "fingerprint" : "6477f518963149b01807a31e7067be97dc39a30b93673e1e246f812e9ea5ef21", + "fingerprint" : "752322cea7c7ad97a20777d51a8d44c33a7e037290344c8fed6881ec916b6f1a", "location" : { "lines" : { "begin" : 1, "end" : 1 }, - "path" : "filename" + "path" : "path/file.swift" }, - "severity" : "MAJOR", + "severity" : "major", "type" : "issue" }, { @@ -56,7 +56,7 @@ }, "path" : null }, - "severity" : "MAJOR", + "severity" : "major", "type" : "issue" } ] \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedEmojiReporterOutput.txt b/Tests/SwiftLintFrameworkTests/Resources/CannedEmojiReporterOutput.txt index 8087d60ce3..7830e72d6c 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedEmojiReporterOutput.txt +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedEmojiReporterOutput.txt @@ -1,6 +1,7 @@ +${CURRENT_WORKING_DIRECTORY}/path/file.swift +⛔️ Line 1: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) Other ⛔️ Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) filename -⛔️ Line 1: Violation Reason (line_length) -⛔️ Line 1: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) -⚠️ Line 1: Violation Reason (line_length) \ No newline at end of file +⛔️ Line 1: Violation Reason 2 (line_length) +⚠️ Line 1: Violation Reason 1 (line_length) \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedGitHubActionsLoggingReporterOutput.txt b/Tests/SwiftLintFrameworkTests/Resources/CannedGitHubActionsLoggingReporterOutput.txt index 4303095881..b7071ab8e3 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedGitHubActionsLoggingReporterOutput.txt +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedGitHubActionsLoggingReporterOutput.txt @@ -1,4 +1,4 @@ -::warning file=filename,line=1,col=2::Violation Reason (line_length) -::error file=filename,line=1,col=2::Violation Reason (line_length) -::error file=filename,line=1,col=2::Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) +::warning file=filename,line=1,col=1::Violation Reason 1 (line_length) +::error file=filename,line=1,col=1::Violation Reason 2 (line_length) +::error file=path/file.swift,line=1,col=2::Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) ::error file=,line=1,col=1::Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedGitLabJUnitReporterOutput.xml b/Tests/SwiftLintFrameworkTests/Resources/CannedGitLabJUnitReporterOutput.xml index 339a7808d7..cfbbfc8fec 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedGitLabJUnitReporterOutput.xml +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedGitLabJUnitReporterOutput.xml @@ -3,29 +3,29 @@ Severity: warning Rule: line_length -Reason: Violation Reason +Reason: Violation Reason 1 File: filename Line: 1 -Column: 2 +Column: 1 Severity: error Rule: line_length -Reason: Violation Reason +Reason: Violation Reason 2 File: filename Line: 1 -Column: 2 +Column: nil - + Severity: error Rule: syntactic_sugar Reason: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array<Int> -File: filename +File: path/file.swift Line: 1 Column: 2 diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedHTMLReporterOutput.html b/Tests/SwiftLintFrameworkTests/Resources/CannedHTMLReporterOutput.html index 82cfc00a2f..928353df6e 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedHTMLReporterOutput.html +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedHTMLReporterOutput.html @@ -78,20 +78,20 @@

Violations

1 filename - 1:2 + 1:1 Warning - Violation Reason + Violation Reason 1 2 filename - 1:2 + 1:0 Error - Violation Reason + Violation Reason 2 3 - filename + path/file.swift 1:2 Error Shorthand syntactic sugar should be used, i.e. [Int] instead of Array<Int> @@ -114,7 +114,7 @@

Summary

Total files with violations - 1 + 2 Total warnings @@ -132,7 +132,7 @@

Summary

Created with SwiftLint - 1.2.3 on 13/12/2016 + ${SWIFTLINT_VERSION} on ${TODAYS_DATE}

\ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedJSONReporterOutput.json b/Tests/SwiftLintFrameworkTests/Resources/CannedJSONReporterOutput.json index e20478afd6..a125107eb3 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedJSONReporterOutput.json +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedJSONReporterOutput.json @@ -1,38 +1,38 @@ [ { - "reason" : "Violation Reason", - "character" : 2, + "character" : 1, "file" : "filename", - "rule_id" : "line_length", "line" : 1, + "reason" : "Violation Reason 1", + "rule_id" : "line_length", "severity" : "Warning", "type" : "Line Length" }, { - "reason" : "Violation Reason", - "character" : 2, + "character" : null, "file" : "filename", - "rule_id" : "line_length", "line" : 1, + "reason" : "Violation Reason 2", + "rule_id" : "line_length", "severity" : "Error", "type" : "Line Length" }, { - "reason" : "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array", "character" : 2, - "file" : "filename", - "rule_id" : "syntactic_sugar", + "file" : "${CURRENT_WORKING_DIRECTORY}/path/file.swift", "line" : 1, + "reason" : "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array", + "rule_id" : "syntactic_sugar", "severity" : "Error", "type" : "Syntactic Sugar" }, { - "reason" : "Colons should be next to the identifier when specifying a type and next to the key in dictionary literals", "character" : null, "file" : null, - "rule_id" : "colon", "line" : null, + "reason" : "Colons should be next to the identifier when specifying a type and next to the key in dictionary literals", + "rule_id" : "colon", "severity" : "Error", "type" : "Colon Spacing" } -] \ No newline at end of file +] diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedJunitReporterOutput.xml b/Tests/SwiftLintFrameworkTests/Resources/CannedJunitReporterOutput.xml index 2bb514f06f..b786f1f1f3 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedJunitReporterOutput.xml +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedJunitReporterOutput.xml @@ -2,12 +2,12 @@ - Warning:Line:1 + Warning:Line:1 - Error:Line:1 + Error:Line:1 - + Error:Line:1 diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md b/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md index 6610899238..d74bd35e7f 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedMarkdownReporterOutput.md @@ -1,6 +1,6 @@ file | line | severity | reason | rule_id --- | --- | --- | --- | --- -filename | 1 | :warning: | Line Length: Violation Reason | line_length -filename | 1 | :stop\_sign: | Line Length: Violation Reason | line_length -filename | 1 | :stop\_sign: | Syntactic Sugar: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array | syntactic_sugar +filename | 1 | :warning: | Line Length: Violation Reason 1 | line_length +filename | 1 | :stop\_sign: | Line Length: Violation Reason 2 | line_length +${CURRENT_WORKING_DIRECTORY}/path/file.swift | 1 | :stop\_sign: | Syntactic Sugar: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array | syntactic_sugar | | :stop\_sign: | Colon Spacing: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals | colon \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedRelativePathReporterOutput.txt b/Tests/SwiftLintFrameworkTests/Resources/CannedRelativePathReporterOutput.txt index cab4b2c635..c3b7322b65 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedRelativePathReporterOutput.txt +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedRelativePathReporterOutput.txt @@ -1,4 +1,4 @@ -filename:1:2: warning: Line Length Violation: Violation Reason (line_length) -filename:1:2: error: Line Length Violation: Violation Reason (line_length) -filename:1:2: error: Syntactic Sugar Violation: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) +filename:1:1: warning: Line Length Violation: Violation Reason 1 (line_length) +filename:1:1: error: Line Length Violation: Violation Reason 2 (line_length) +path/file.swift:1:2: error: Syntactic Sugar Violation: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) :1:1: error: Colon Spacing Violation: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedSARIFReporterOutput.json b/Tests/SwiftLintFrameworkTests/Resources/CannedSARIFReporterOutput.json new file mode 100644 index 0000000000..5e02405221 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedSARIFReporterOutput.json @@ -0,0 +1,93 @@ +{ + "$schema" : "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json", + "runs" : [ + { + "results" : [ + { + "level" : "warning", + "locations" : [ + { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "filename" + }, + "region" : { + "startColumn" : 1, + "startLine" : 1 + } + } + } + ], + "message" : { + "text" : "Violation Reason 1" + }, + "ruleId" : "line_length" + }, + { + "level" : "error", + "locations" : [ + { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "filename" + }, + "region" : { + "startColumn" : 1, + "startLine" : 1 + } + } + } + ], + "message" : { + "text" : "Violation Reason 2" + }, + "ruleId" : "line_length" + }, + { + "level" : "error", + "locations" : [ + { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "path/file.swift" + }, + "region" : { + "startColumn" : 2, + "startLine" : 1 + } + } + } + ], + "message" : { + "text" : "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array" + }, + "ruleId" : "syntactic_sugar" + }, + { + "level" : "error", + "locations" : [ + { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "" + } + } + } + ], + "message" : { + "text" : "Colons should be next to the identifier when specifying a type and next to the key in dictionary literals" + }, + "ruleId" : "colon" + } + ], + "tool" : { + "driver" : { + "informationUri" : "https://github.com/realm/SwiftLint/blob/${SWIFTLINT_VERSION}/README.md", + "name" : "SwiftLint", + "semanticVersion" : "${SWIFTLINT_VERSION}" + } + } + } + ], + "version" : "2.1.0" +} \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedSonarQubeReporterOutput.json b/Tests/SwiftLintFrameworkTests/Resources/CannedSonarQubeReporterOutput.json index fb2472e4eb..bb6a1529f2 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedSonarQubeReporterOutput.json +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedSonarQubeReporterOutput.json @@ -1,56 +1,56 @@ { - "issues":[ + "issues" : [ { - "engineId":"SwiftLint", - "primaryLocation":{ - "filePath":"filename", - "message":"Violation Reason", - "textRange":{ - "startLine":1 + "engineId" : "SwiftLint", + "primaryLocation" : { + "filePath" : "filename", + "message" : "Violation Reason 1", + "textRange" : { + "startLine" : 1 } }, - "ruleId":"line_length", - "severity":"MINOR", - "type":"CODE_SMELL" + "ruleId" : "line_length", + "severity" : "MINOR", + "type" : "CODE_SMELL" }, { - "engineId":"SwiftLint", - "primaryLocation":{ - "filePath":"filename", - "message":"Violation Reason", - "textRange":{ - "startLine":1 + "engineId" : "SwiftLint", + "primaryLocation" : { + "filePath" : "filename", + "message" : "Violation Reason 2", + "textRange" : { + "startLine" : 1 } }, - "ruleId":"line_length", - "severity":"MAJOR", - "type":"CODE_SMELL" + "ruleId" : "line_length", + "severity" : "MAJOR", + "type" : "CODE_SMELL" }, { - "engineId":"SwiftLint", - "primaryLocation":{ - "filePath":"filename", - "message":"Shorthand syntactic sugar should be used, i.e. [Int] instead of Array", - "textRange":{ - "startLine":1 + "engineId" : "SwiftLint", + "primaryLocation" : { + "filePath" : "path\/file.swift", + "message" : "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array", + "textRange" : { + "startLine" : 1 } }, - "ruleId":"syntactic_sugar", - "severity":"MAJOR", - "type":"CODE_SMELL" + "ruleId" : "syntactic_sugar", + "severity" : "MAJOR", + "type" : "CODE_SMELL" }, { - "engineId":"SwiftLint", - "primaryLocation":{ - "filePath":"", - "message":"Colons should be next to the identifier when specifying a type and next to the key in dictionary literals", - "textRange":{ - "startLine":1 + "engineId" : "SwiftLint", + "primaryLocation" : { + "filePath" : "", + "message" : "Colons should be next to the identifier when specifying a type and next to the key in dictionary literals", + "textRange" : { + "startLine" : 1 } }, - "ruleId":"colon", - "severity":"MAJOR", - "type":"CODE_SMELL" + "ruleId" : "colon", + "severity" : "MAJOR", + "type" : "CODE_SMELL" } ] -} +} \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedSummaryReporterOutput.txt b/Tests/SwiftLintFrameworkTests/Resources/CannedSummaryReporterOutput.txt index 5771f1c01c..211a0a0cd4 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedSummaryReporterOutput.txt +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedSummaryReporterOutput.txt @@ -6,5 +6,5 @@ | syntactic_sugar | no | yes | no | 0 | 1 | 1 | 1 | | vertical_whitespace_opening_braces | yes | yes | no | 1 | 0 | 1 | 1 | +------------------------------------+--------+-------------+--------+----------+--------+------------------+-----------------+ -| Total | | | | 2 | 3 | 5 | 2 | +| Total | | | | 2 | 3 | 5 | 3 | +------------------------------------+--------+-------------+--------+----------+--------+------------------+-----------------+ diff --git a/Tests/SwiftLintFrameworkTests/Resources/CannedXcodeReporterOutput.txt b/Tests/SwiftLintFrameworkTests/Resources/CannedXcodeReporterOutput.txt index cab4b2c635..553120fb05 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/CannedXcodeReporterOutput.txt +++ b/Tests/SwiftLintFrameworkTests/Resources/CannedXcodeReporterOutput.txt @@ -1,4 +1,4 @@ -filename:1:2: warning: Line Length Violation: Violation Reason (line_length) -filename:1:2: error: Line Length Violation: Violation Reason (line_length) -filename:1:2: error: Syntactic Sugar Violation: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) +filename:1:1: warning: Line Length Violation: Violation Reason 1 (line_length) +filename:1:1: error: Line Length Violation: Violation Reason 2 (line_length) +${CURRENT_WORKING_DIRECTORY}/path/file.swift:1:2: error: Syntactic Sugar Violation: Shorthand syntactic sugar should be used, i.e. [Int] instead of Array (syntactic_sugar) :1:1: error: Colon Spacing Violation: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon) \ No newline at end of file diff --git a/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/BoolExtensionTests.swift b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/BoolExtensionTests.swift index 1360ccaf23..285895ead2 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/BoolExtensionTests.swift +++ b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/BoolExtensionTests.swift @@ -1,7 +1,7 @@ @testable import SomeModule import XCTest -class BoolExtensionTests: SwiftLintTestCase { +final class BoolExtensionTests: SwiftLintTestCase { func testExample() { // some code } diff --git a/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/Multiple.Levels.Deeply.Nested.MyType.swift b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/Multiple.Levels.Deeply.Nested.MyType.swift new file mode 100644 index 0000000000..46bb755a47 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/Multiple.Levels.Deeply.Nested.MyType.swift @@ -0,0 +1,9 @@ +extension Multiple { + enum Levels { + class Deeply { + struct Nested { + actor MyType {} + } + } + } +} diff --git a/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/MyType.swift b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/MyType.swift new file mode 100644 index 0000000000..7c72b6af9d --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/MyType.swift @@ -0,0 +1,3 @@ +enum Nested { + struct MyType {} +} diff --git a/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/Nested.MyType.swift b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/Nested.MyType.swift new file mode 100644 index 0000000000..a57866f72a --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/Resources/FileNameRuleFixtures/Nested.MyType.swift @@ -0,0 +1,4 @@ +enum Nested { + struct MyType { + } +} diff --git a/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/.swiftlint.yml b/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/.swiftlint.yml index fae3e10596..21acf08334 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/.swiftlint.yml +++ b/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/.swiftlint.yml @@ -8,3 +8,5 @@ line_length: 10000000000 reporter: "json" allow_zero_lintable_files: true strict: true +baseline: Baseline.json +write_baseline: Baseline.json diff --git a/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/ChildConfig/Test1/Main/main.yml b/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/ChildConfig/Test1/Main/main.yml index 6dbadd2528..86193fdbf8 100644 --- a/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/ChildConfig/Test1/Main/main.yml +++ b/Tests/SwiftLintFrameworkTests/Resources/ProjectMock/ChildConfig/Test1/Main/main.yml @@ -9,3 +9,4 @@ excluded: line_length: 80 child_config: child1.yml +parent_config: nonExistingParent.yml diff --git a/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift b/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift index b593597c39..a656666d60 100644 --- a/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift +++ b/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift @@ -5,8 +5,8 @@ import XCTest // swiftlint:disable file_length // swiftlint:disable:next type_body_length -class RuleConfigurationDescriptionTests: XCTestCase { - @AutoApply +final class RuleConfigurationDescriptionTests: XCTestCase { + @AutoConfigParser private struct TestConfiguration: RuleConfiguration { typealias Parent = RuleMock // swiftlint:disable:this nesting @@ -20,8 +20,8 @@ class RuleConfigurationDescriptionTests: XCTestCase { var integer = 2 @ConfigurationElement(key: "null") var null: Int? - @ConfigurationElement(key: "double") - var double = 2.1 + @ConfigurationElement(key: "my_double") + var myDouble = 2.1 @ConfigurationElement(key: "severity") var severity = ViolationSeverity.warning @ConfigurationElement( @@ -29,38 +29,39 @@ class RuleConfigurationDescriptionTests: XCTestCase { postprocessor: { list in list = list.map { $0.uppercased() } } ) var list = ["string1", "string2"] - @ConfigurationElement(key: "set") + @ConfigurationElement(key: "set", deprecationNotice: .suggestAlternative(ruleID: "my_rule", name: "other_opt")) var set: Set = [1, 2, 3] - @ConfigurationElement + @ConfigurationElement(inline: true) var severityConfig = SeverityConfiguration(.error) @ConfigurationElement(key: "SEVERITY") var renamedSeverityConfig = SeverityConfiguration(.warning) - @ConfigurationElement - var inlinedSeverityLevels = SeverityLevelsConfiguration(warning: 1, error: 2) + @ConfigurationElement(inline: true) + var inlinedSeverityLevels = SeverityLevelsConfiguration(warning: 1, error: nil) @ConfigurationElement(key: "levels") - var nestedSeverityLevels = SeverityLevelsConfiguration(warning: 3, error: nil) + var nestedSeverityLevels = SeverityLevelsConfiguration(warning: 3, error: 2) - func isEqualTo(_ ruleConfiguration: some RuleConfiguration) -> Bool { false } + func isEqualTo(_: some RuleConfiguration) -> Bool { false } } // swiftlint:disable:next function_body_length - func testDescriptionFromConfiguration() { - let description = RuleConfigurationDescription.from(configuration: TestConfiguration()) + func testDescriptionFromConfiguration() throws { + var configuration = TestConfiguration() + try configuration.apply(configuration: Void()) // Configure to set keys. + let description = RuleConfigurationDescription.from(configuration: configuration) XCTAssertEqual(description.oneLiner(), """ flag: true; \ string: "value"; \ symbol: value; \ integer: 2; \ - double: 2.1; \ + my_double: 2.1; \ severity: warning; \ list: ["STRING1", "STRING2"]; \ set: [1, 2, 3]; \ severity: error; \ SEVERITY: warning; \ warning: 1; \ - error: 2; \ - levels: warning: 3 + levels: warning: 3, error: 2 """) XCTAssertEqual(description.markdown(), """ @@ -103,7 +104,7 @@ class RuleConfigurationDescriptionTests: XCTestCase { - double + my_double 2.1 @@ -159,14 +160,6 @@ class RuleConfigurationDescriptionTests: XCTestCase { - error - - - 2 - - - - levels @@ -183,6 +176,14 @@ class RuleConfigurationDescriptionTests: XCTestCase { 3 + + + error + + + 2 + + @@ -196,16 +197,16 @@ class RuleConfigurationDescriptionTests: XCTestCase { string: "value" symbol: value integer: 2 - double: 2.1 + my_double: 2.1 severity: warning list: ["STRING1", "STRING2"] set: [1, 2, 3] severity: error SEVERITY: warning warning: 1 - error: 2 levels: warning: 3 + error: 2 """) } @@ -220,9 +221,9 @@ class RuleConfigurationDescriptionTests: XCTestCase { @ConfigurationElement(key: "invisible") var invisible = true - mutating func apply(configuration: Any) throws {} + mutating func apply(configuration _: Any) throws { /* conformance for test */ } - func isEqualTo(_ ruleConfiguration: some RuleConfiguration) -> Bool { false } + func isEqualTo(_: some RuleConfiguration) -> Bool { false } } let description = RuleConfigurationDescription.from(configuration: Config()) @@ -465,13 +466,13 @@ class RuleConfigurationDescriptionTests: XCTestCase { "symbol": "new symbol", "integer": 5, "null": 0, - "double": 5.1, + "my_double": 5.1, "severity": "error", "list": ["string3", "string4"], "set": [4, 5, 6], "SEVERITY": "error", "warning": 12, - "levels": ["warning": 6, "error": 7] + "levels": ["warning": 6, "error": 7], ]) XCTAssertFalse(configuration.flag) @@ -479,7 +480,7 @@ class RuleConfigurationDescriptionTests: XCTestCase { XCTAssertEqual(configuration.symbol, try Symbol(fromAny: "new symbol", context: "rule")) XCTAssertEqual(configuration.integer, 5) XCTAssertEqual(configuration.null, 0) - XCTAssertEqual(configuration.double, 5.1) + XCTAssertEqual(configuration.myDouble, 5.1) XCTAssertEqual(configuration.severity, .error) XCTAssertEqual(configuration.list, ["STRING3", "STRING4"]) XCTAssertEqual(configuration.set, [4, 5, 6]) @@ -489,17 +490,35 @@ class RuleConfigurationDescriptionTests: XCTestCase { XCTAssertEqual(configuration.nestedSeverityLevels, SeverityLevelsConfiguration(warning: 6, error: 7)) } + func testDeprecationWarning() throws { + var configuration = TestConfiguration() + + XCTAssertEqual( + try Issue.captureConsole { try configuration.apply(configuration: ["set": [6, 7]]) }, + "warning: Configuration option 'set' in 'my_rule' rule is deprecated. Use the option 'other_opt' instead." + ) + } + + func testNoDeprecationWarningIfNoDeprecatedPropertySet() throws { + var configuration = TestConfiguration() + + XCTAssert(try Issue.captureConsole { try configuration.apply(configuration: ["flag": false]) }.isEmpty) + } + func testInvalidKeys() throws { var configuration = TestConfiguration() - checkError(Issue.invalidConfigurationKeys(ruleID: "RuleMock", keys: ["unknown", "unsupported"])) { - try configuration.apply(configuration: [ - "severity": "error", - "warning": 3, - "unknown": 1, - "unsupported": true - ]) - } + XCTAssertEqual( + try Issue.captureConsole { + try configuration.apply(configuration: [ + "severity": "error", + "warning": 3, + "unknown": 1, + "unsupported": true, + ]) + }, + "warning: Configuration for 'RuleMock' rule contains the invalid key(s) 'unknown', 'unsupported'." + ) } private func description(@RuleConfigurationDescriptionBuilder _ content: () -> RuleConfigurationDescription) diff --git a/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift index 2d2f2a029c..f86fca1e21 100644 --- a/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/RuleConfigurationTests.swift @@ -3,7 +3,7 @@ import SourceKittenFramework @testable import SwiftLintCore import XCTest -class RuleConfigurationTests: SwiftLintTestCase { +final class RuleConfigurationTests: SwiftLintTestCase { private let defaultNestingConfiguration = NestingConfiguration( typeLevel: SeverityLevelsConfiguration(warning: 0), functionLevel: SeverityLevelsConfiguration(warning: 0) @@ -18,7 +18,7 @@ class RuleConfigurationTests: SwiftLintTestCase { "warning": 8, "error": 18 ], "check_nesting_in_closures_and_statements": false, - "always_allow_one_type_in_functions": true + "always_allow_one_type_in_functions": true, ] as [String: any Sendable] var nestingConfig = defaultNestingConfiguration do { @@ -37,11 +37,18 @@ class RuleConfigurationTests: SwiftLintTestCase { func testNestingConfigurationThrowsOnBadConfig() { let config = 17 var nestingConfig = defaultNestingConfiguration - checkError(Issue.invalidConfiguration(ruleID: NestingRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: NestingRule.identifier)) { try nestingConfig.apply(configuration: config) } } + func testSeverityWorksAsOnlyParameter() throws { + var config = AttributesConfiguration() + XCTAssertEqual(config.severity, .warning) + try config.apply(configuration: "error") + XCTAssertEqual(config.severity, .error) + } + func testSeverityConfigurationFromString() { let config = "Warning" let comp = SeverityConfiguration(.warning) @@ -66,18 +73,28 @@ class RuleConfigurationTests: SwiftLintTestCase { } } - func testSeverityConfigurationThrowsOnBadConfig() { + func testSeverityConfigurationThrowsNothingApplied() throws { let config = 17 + var severityConfig = SeverityConfiguration(.error) + checkError(Issue.nothingApplied(ruleID: RuleMock.identifier)) { + try severityConfig.apply(configuration: config) + } + } + + func testSeverityConfigurationThrowsInvalidConfiguration() { + let config = "foo" var severityConfig = SeverityConfiguration(.warning) - checkError(Issue.unknownConfiguration(ruleID: RuleMock.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: RuleMock.identifier)) { try severityConfig.apply(configuration: config) } } func testSeverityLevelConfigParams() { let severityConfig = SeverityLevelsConfiguration(warning: 17, error: 7) - XCTAssertEqual(severityConfig.params, [RuleParameter(severity: .error, value: 7), - RuleParameter(severity: .warning, value: 17)]) + XCTAssertEqual( + severityConfig.params, + [RuleParameter(severity: .error, value: 7), RuleParameter(severity: .warning, value: 17)] + ) } func testSeverityLevelConfigPartialParams() { @@ -100,7 +117,7 @@ class RuleConfigurationTests: SwiftLintTestCase { func testRegexConfigurationThrows() { let config = 17 var regexConfig = RegexConfiguration(identifier: "") - checkError(Issue.unknownConfiguration(ruleID: RuleMock.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: RuleMock.identifier)) { try regexConfig.apply(configuration: config) } } @@ -120,7 +137,7 @@ class RuleConfigurationTests: SwiftLintTestCase { let config = "unknown" var configuration = TrailingWhitespaceConfiguration(ignoresEmptyLines: false, ignoresComments: true) - checkError(Issue.invalidConfiguration(ruleID: TrailingWhitespaceRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: TrailingWhitespaceRule.identifier)) { try configuration.apply(configuration: config) } } @@ -232,7 +249,7 @@ class RuleConfigurationTests: SwiftLintTestCase { let conf2 = [ "severity": "error", "excluded": "viewWillAppear(_:)", - "included": ["*", "testMethod1()", "testMethod2(_:)"] + "included": ["*", "testMethod1()", "testMethod2(_:)"], ] as [String: any Sendable] do { try configuration.apply(configuration: conf2) @@ -249,7 +266,7 @@ class RuleConfigurationTests: SwiftLintTestCase { let conf3 = [ "severity": "warning", "excluded": "*", - "included": ["testMethod1()", "testMethod2(_:)"] + "included": ["testMethod1()", "testMethod2(_:)"], ] as [String: any Sendable] do { try configuration.apply(configuration: conf3) @@ -278,8 +295,8 @@ class RuleConfigurationTests: SwiftLintTestCase { "required", "convenience", "lazy", - "dynamic" - ] + "dynamic", + ], ] try configuration.apply(configuration: config) @@ -294,7 +311,7 @@ class RuleConfigurationTests: SwiftLintTestCase { .required, .convenience, .lazy, - .dynamic + .dynamic, ] XCTAssertEqual(configuration.severityConfiguration.severity, .warning) XCTAssertEqual(configuration.preferredModifierOrder, expected) @@ -304,7 +321,7 @@ class RuleConfigurationTests: SwiftLintTestCase { var configuration = ModifierOrderConfiguration() let config = ["severity": "warning", "preferred_modifier_order": ["specialize"]] as [String: any Sendable] - checkError(Issue.unknownConfiguration(ruleID: ModifierOrderRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: ModifierOrderRule.identifier)) { try configuration.apply(configuration: config) } } @@ -312,7 +329,7 @@ class RuleConfigurationTests: SwiftLintTestCase { func testModifierOrderConfigurationThrowsOnNonModifiableGroup() { var configuration = ModifierOrderConfiguration() let config = ["severity": "warning", "preferred_modifier_order": ["atPrefixed"]] as [String: any Sendable] - checkError(Issue.unknownConfiguration(ruleID: ModifierOrderRule.description.identifier)) { + checkError(Issue.invalidConfiguration(ruleID: ModifierOrderRule.identifier)) { try configuration.apply(configuration: config) } } diff --git a/Tests/SwiftLintFrameworkTests/RuleTests.swift b/Tests/SwiftLintFrameworkTests/RuleTests.swift index de92364c16..a6b0182936 100644 --- a/Tests/SwiftLintFrameworkTests/RuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/RuleTests.swift @@ -10,27 +10,27 @@ struct RuleWithLevelsMock: Rule { kind: .style, deprecatedAliases: ["mock"]) - init() {} + init() { /* conformance for test */ } init(configuration: Any) throws { self.init() try self.configuration.apply(configuration: configuration) } - func validate(file: SwiftLintFile) -> [StyleViolation] { return [] } + func validate(file _: SwiftLintFile) -> [StyleViolation] { [] } } -class RuleTests: SwiftLintTestCase { +final class RuleTests: SwiftLintTestCase { fileprivate struct RuleMock1: Rule { var configuration = SeverityConfiguration(.warning) var configurationDescription: some Documentable { RuleConfigurationOption.noOptions } static let description = RuleDescription(identifier: "RuleMock1", name: "", description: "", kind: .style) - init() {} - init(configuration: Any) throws { self.init() } + init() { /* conformance for test */ } + init(configuration _: Any) throws { self.init() } - func validate(file: SwiftLintFile) -> [StyleViolation] { - return [] + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] } } @@ -40,11 +40,11 @@ class RuleTests: SwiftLintTestCase { static let description = RuleDescription(identifier: "RuleMock2", name: "", description: "", kind: .style) - init() {} - init(configuration: Any) throws { self.init() } + init() { /* conformance for test */ } + init(configuration _: Any) throws { self.init() } - func validate(file: SwiftLintFile) -> [StyleViolation] { - return [] + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] } } @@ -55,13 +55,13 @@ class RuleTests: SwiftLintTestCase { name: "", description: "", kind: .style) - init() {} + init() { /* conformance for test */ } init(configuration: Any) throws { self.init() try self.configuration.apply(configuration: configuration) } - func validate(file: SwiftLintFile) -> [StyleViolation] { return [] } + func validate(file _: SwiftLintFile) -> [StyleViolation] { [] } } func testRuleIsEqualTo() { diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index a0504de814..ef4748f060 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class RulesTests: SwiftLintTestCase { +final class RulesTests: SwiftLintTestCase { func testLeadingWhitespace() { verifyRule(LeadingWhitespaceRule.description, skipDisableCommandTests: true, testMultiByteOffsets: false, testShebang: false) diff --git a/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift b/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift index 4f7484c9fa..4b5b8d3e22 100644 --- a/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift +++ b/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintCore import XCTest -class SourceKitCrashTests: SwiftLintTestCase { +final class SourceKitCrashTests: SwiftLintTestCase { func testAssertHandlerIsNotCalledOnNormalFile() { let file = SwiftLintFile(contents: "A file didn't crash SourceKitService") file.sourcekitdFailed = false @@ -52,7 +52,7 @@ class SourceKitCrashTests: SwiftLintTestCase { file.assertHandler = { XCTFail("If this called, rule's SourceKitFreeRule is not properly configured") } - let configuration = Configuration(rulesMode: .only(allRuleIdentifiers)) + let configuration = Configuration(rulesMode: .onlyConfiguration(allRuleIdentifiers)) let storage = RuleStorage() _ = Linter(file: file, configuration: configuration).collect(into: storage).styleViolations(using: storage) file.sourcekitdFailed = false diff --git a/Tests/SwiftLintFrameworkTests/StatementPositionRuleTests.swift b/Tests/SwiftLintFrameworkTests/StatementPositionRuleTests.swift index a0e7d91c29..b776b05302 100644 --- a/Tests/SwiftLintFrameworkTests/StatementPositionRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/StatementPositionRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class StatementPositionRuleTests: SwiftLintTestCase { +final class StatementPositionRuleTests: SwiftLintTestCase { func testStatementPositionUncuddled() { let configuration = ["statement_mode": "uncuddled_else"] verifyRule(StatementPositionRule.uncuddledDescription, ruleConfiguration: configuration) diff --git a/Tests/SwiftLintFrameworkTests/SwiftLintFileTests.swift b/Tests/SwiftLintFrameworkTests/SwiftLintFileTests.swift index b7b035defb..989b126e68 100644 --- a/Tests/SwiftLintFrameworkTests/SwiftLintFileTests.swift +++ b/Tests/SwiftLintFrameworkTests/SwiftLintFileTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintCore import XCTest -class SwiftLintFileTests: SwiftLintTestCase { +final class SwiftLintFileTests: SwiftLintTestCase { private let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) override func setUp() async throws { diff --git a/Tests/SwiftLintFrameworkTests/SwiftVersionTests.swift b/Tests/SwiftLintFrameworkTests/SwiftVersionTests.swift index 7a6a734808..aff4993cc8 100644 --- a/Tests/SwiftLintFrameworkTests/SwiftVersionTests.swift +++ b/Tests/SwiftLintFrameworkTests/SwiftVersionTests.swift @@ -3,8 +3,16 @@ import XCTest final class SwiftVersionTests: SwiftLintTestCase { func testDetectSwiftVersion() { -#if compiler(>=6.0.0) +#if compiler(>=6.0.2) + let version = "6.0.2" +#elseif compiler(>=6.0.1) + let version = "6.0.1" +#elseif compiler(>=6.0.0) let version = "6.0.0" +#elseif compiler(>=5.10.1) + let version = "5.10.1" +#elseif compiler(>=5.10.0) + let version = "5.10.0" #elseif compiler(>=5.9.2) let version = "5.9.2" #elseif compiler(>=5.9.1) @@ -46,4 +54,52 @@ final class SwiftVersionTests: SwiftLintTestCase { #endif XCTAssertEqual(SwiftVersion.current.rawValue, version) } + + func testCompareBalancedSwiftVersion() { + XCTAssertNotEqual(SwiftVersion(rawValue: "5"), SwiftVersion(rawValue: "6")) + XCTAssertTrue(SwiftVersion(rawValue: "5") < SwiftVersion(rawValue: "6")) + XCTAssertFalse(SwiftVersion(rawValue: "5") > SwiftVersion(rawValue: "6")) + XCTAssertFalse(SwiftVersion(rawValue: "6") < SwiftVersion(rawValue: "5")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "5.1"), SwiftVersion(rawValue: "5.2")) + XCTAssertTrue(SwiftVersion(rawValue: "5.1") < SwiftVersion(rawValue: "5.2")) + XCTAssertFalse(SwiftVersion(rawValue: "5.1") > SwiftVersion(rawValue: "5.2")) + XCTAssertFalse(SwiftVersion(rawValue: "5.2") < SwiftVersion(rawValue: "5.1")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "5.1.1"), SwiftVersion(rawValue: "5.1.2")) + XCTAssertTrue(SwiftVersion(rawValue: "5.1.1") < SwiftVersion(rawValue: "5.1.2")) + XCTAssertFalse(SwiftVersion(rawValue: "5.1.1") > SwiftVersion(rawValue: "5.1.2")) + XCTAssertFalse(SwiftVersion(rawValue: "5.1.2") < SwiftVersion(rawValue: "5.1.1")) + } + + func testCompareUnbalancedSwiftVersion() { + XCTAssertEqual(SwiftVersion(rawValue: "5"), SwiftVersion(rawValue: "5.0")) + XCTAssertFalse(SwiftVersion(rawValue: "5") < SwiftVersion(rawValue: "5.0")) + XCTAssertFalse(SwiftVersion(rawValue: "5") > SwiftVersion(rawValue: "5.0")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "5.9"), SwiftVersion(rawValue: "6")) + XCTAssertTrue(SwiftVersion(rawValue: "5.9") < SwiftVersion(rawValue: "6")) + XCTAssertFalse(SwiftVersion(rawValue: "5.9") > SwiftVersion(rawValue: "6")) + XCTAssertFalse(SwiftVersion(rawValue: "6") < SwiftVersion(rawValue: "5.9")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "5.2"), SwiftVersion(rawValue: "5.10.3")) + XCTAssertTrue(SwiftVersion(rawValue: "5.2") < SwiftVersion(rawValue: "5.10.3")) + XCTAssertFalse(SwiftVersion(rawValue: "5.2") > SwiftVersion(rawValue: "5.10.3")) + XCTAssertFalse(SwiftVersion(rawValue: "5.10.3") < SwiftVersion(rawValue: "5.2")) + } + + func testCompareProblematicSwiftVersion() { + XCTAssertEqual(SwiftVersion(rawValue: "5.010"), SwiftVersion(rawValue: "5.10")) + XCTAssertFalse(SwiftVersion(rawValue: "5.010") < SwiftVersion(rawValue: "5.10")) + XCTAssertFalse(SwiftVersion(rawValue: "5.010") > SwiftVersion(rawValue: "5.10")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "-10"), SwiftVersion(rawValue: "-1")) + XCTAssertTrue(SwiftVersion(rawValue: "-10") < SwiftVersion(rawValue: "-1")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "0"), SwiftVersion(rawValue: "10")) + XCTAssertTrue(SwiftVersion(rawValue: "0") < SwiftVersion(rawValue: "10")) + + XCTAssertNotEqual(SwiftVersion(rawValue: "alpha"), SwiftVersion(rawValue: "beta")) + XCTAssertTrue(SwiftVersion(rawValue: "alpha") < SwiftVersion(rawValue: "beta")) + } } diff --git a/Tests/SwiftLintFrameworkTests/SwitchCaseAlignmentRuleTests.swift b/Tests/SwiftLintFrameworkTests/SwitchCaseAlignmentRuleTests.swift index 80a6c09299..5ec28069b9 100644 --- a/Tests/SwiftLintFrameworkTests/SwitchCaseAlignmentRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/SwitchCaseAlignmentRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class SwitchCaseAlignmentRuleTests: SwiftLintTestCase { +final class SwitchCaseAlignmentRuleTests: SwiftLintTestCase { func testSwitchCaseAlignmentWithoutIndentedCases() { let baseDescription = SwitchCaseAlignmentRule.description let examples = SwitchCaseAlignmentRule.Examples(indentedCases: false) diff --git a/Tests/SwiftLintFrameworkTests/TodoRuleTests.swift b/Tests/SwiftLintFrameworkTests/TodoRuleTests.swift index 2b0b7f311f..8130bc54cc 100644 --- a/Tests/SwiftLintFrameworkTests/TodoRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/TodoRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class TodoRuleTests: SwiftLintTestCase { +final class TodoRuleTests: SwiftLintTestCase { func testTodo() { verifyRule(TodoRule.description, commentDoesntViolate: false) } @@ -41,7 +41,7 @@ class TodoRuleTests: SwiftLintTestCase { } private func violations(_ example: Example, config: Any? = nil) -> [StyleViolation] { - let config = makeConfig(config, TodoRule.description.identifier)! + let config = makeConfig(config, TodoRule.identifier)! return SwiftLintFrameworkTests.violations(example, config: config) } } diff --git a/Tests/SwiftLintFrameworkTests/TrailingClosureConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/TrailingClosureConfigurationTests.swift index 5614353b7b..51ffc479ce 100644 --- a/Tests/SwiftLintFrameworkTests/TrailingClosureConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/TrailingClosureConfigurationTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class TrailingClosureConfigurationTests: SwiftLintTestCase { +final class TrailingClosureConfigurationTests: SwiftLintTestCase { func testDefaultConfiguration() { let config = TrailingClosureConfiguration() XCTAssertEqual(config.severityConfiguration.severity, .warning) @@ -10,8 +10,12 @@ class TrailingClosureConfigurationTests: SwiftLintTestCase { func testApplyingCustomConfiguration() throws { var config = TrailingClosureConfiguration() - try config.apply(configuration: ["severity": "error", - "only_single_muted_parameter": true] as [String: any Sendable]) + try config.apply( + configuration: [ + "severity": "error", + "only_single_muted_parameter": true, + ] as [String: any Sendable] + ) XCTAssertEqual(config.severityConfiguration.severity, .error) XCTAssertTrue(config.onlySingleMutedParameter) } diff --git a/Tests/SwiftLintFrameworkTests/TrailingClosureRuleTests.swift b/Tests/SwiftLintFrameworkTests/TrailingClosureRuleTests.swift index b238c3e5a3..c50697287d 100644 --- a/Tests/SwiftLintFrameworkTests/TrailingClosureRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/TrailingClosureRuleTests.swift @@ -1,13 +1,13 @@ @testable import SwiftLintBuiltInRules -class TrailingClosureRuleTests: SwiftLintTestCase { +final class TrailingClosureRuleTests: SwiftLintTestCase { func testWithOnlySingleMutedParameterEnabled() { let originalDescription = TrailingClosureRule.description let description = originalDescription .with(nonTriggeringExamples: originalDescription.nonTriggeringExamples + [ Example("foo.reduce(0, combine: { $0 + 1 })"), Example("offsets.sorted(by: { $0.offset < $1.offset })"), - Example("foo.something(0, { $0 + 1 })") + Example("foo.something(0, { $0 + 1 })"), ]) .with(triggeringExamples: [Example("foo.map(↓{ $0 + 1 })")]) .with(corrections: [ @@ -23,7 +23,7 @@ class TrailingClosureRuleTests: SwiftLintTestCase { for n in list { n.forEach { print($0) } } - """) + """), ]) verifyRule(description, ruleConfiguration: ["only_single_muted_parameter": true]) diff --git a/Tests/SwiftLintFrameworkTests/TrailingCommaRuleTests.swift b/Tests/SwiftLintFrameworkTests/TrailingCommaRuleTests.swift index 0ad06f4c72..e7b4a8f361 100644 --- a/Tests/SwiftLintFrameworkTests/TrailingCommaRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/TrailingCommaRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class TrailingCommaRuleTests: SwiftLintTestCase { +final class TrailingCommaRuleTests: SwiftLintTestCase { func testTrailingCommaRuleWithDefaultConfiguration() { // Verify TrailingCommaRule with test values for when mandatory_comma is false (default). let triggeringExamples = TrailingCommaRule.description.triggeringExamples + @@ -21,7 +21,7 @@ class TrailingCommaRuleTests: SwiftLintTestCase { Example("struct Bar {\n let foo = [1: 2,\n 2: 3↓]\n}\n"), Example("let foo = [1, 2,\n 3↓] + [4,\n 5, 6↓]\n"), Example("let foo = [1, 2,\n 3↓ ]"), - Example("let foo = [\"אבג\", \"αβγ\",\n\"🇺🇸\"↓]\n") + Example("let foo = [\"אבג\", \"αβγ\",\n\"🇺🇸\"↓]\n"), ] private static let nonTriggeringExamples = [ @@ -38,7 +38,7 @@ class TrailingCommaRuleTests: SwiftLintTestCase { Example("let foo = [1: 2, 2: 3]\n"), Example("let foo = [1: 2, 2: 3 ]\n"), Example("struct Bar {\n let foo = [1: 2, 2: 3]\n}\n"), - Example("let foo = [1, 2, 3] + [4, 5, 6]\n") + Example("let foo = [1, 2, 3] + [4, 5, 6]\n"), ] private static let corrections: [Example: Example] = { @@ -69,7 +69,7 @@ class TrailingCommaRuleTests: SwiftLintTestCase { } private func trailingCommaViolations(_ example: Example, ruleConfiguration: Any? = nil) -> [StyleViolation] { - let config = makeConfig(ruleConfiguration, TrailingCommaRule.description.identifier)! + let config = makeConfig(ruleConfiguration, TrailingCommaRule.identifier)! return violations(example, config: config) } } diff --git a/Tests/SwiftLintFrameworkTests/TrailingWhitespaceRuleTests.swift b/Tests/SwiftLintFrameworkTests/TrailingWhitespaceRuleTests.swift index 171e878480..6de8111141 100644 --- a/Tests/SwiftLintFrameworkTests/TrailingWhitespaceRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/TrailingWhitespaceRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class TrailingWhitespaceRuleTests: SwiftLintTestCase { +final class TrailingWhitespaceRuleTests: SwiftLintTestCase { func testWithIgnoresEmptyLinesEnabled() { // Perform additional tests with the ignores_empty_lines setting enabled. // The set of non-triggering examples is extended by a whitespace-indented empty line @@ -17,7 +17,7 @@ class TrailingWhitespaceRuleTests: SwiftLintTestCase { let baseDescription = TrailingWhitespaceRule.description let triggeringComments = [ Example("// \n"), - Example("let name: String // \n") + Example("let name: String // \n"), ] let nonTriggeringExamples = baseDescription.nonTriggeringExamples .filter { !triggeringComments.contains($0) } diff --git a/Tests/SwiftLintFrameworkTests/TypeContentsOrderRuleTests.swift b/Tests/SwiftLintFrameworkTests/TypeContentsOrderRuleTests.swift index 5722a6f0db..f79efb7ab9 100644 --- a/Tests/SwiftLintFrameworkTests/TypeContentsOrderRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/TypeContentsOrderRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class TypeContentsOrderRuleTests: SwiftLintTestCase { +final class TypeContentsOrderRuleTests: SwiftLintTestCase { // swiftlint:disable:next function_body_length func testTypeContentsOrderReversedOrder() { // Test with reversed `order` entries @@ -8,8 +8,8 @@ class TypeContentsOrderRuleTests: SwiftLintTestCase { Example([ "class TestViewController: UIViewController {", TypeContentsOrderRuleExamples.defaultOrderParts.reversed().joined(separator: "\n\n"), - "}" - ].joined(separator: "\n")) + "}", + ].joined(separator: "\n")), ] let triggeringExamples = [ Example(""" @@ -133,7 +133,7 @@ class TypeContentsOrderRuleTests: SwiftLintTestCase { } } } - """) + """), ] let reversedOrderDescription = TypeContentsOrderRule.description @@ -157,8 +157,8 @@ class TypeContentsOrderRuleTests: SwiftLintTestCase { "type_property", "subtype", ["type_alias", "associated_type"], - "case" - ] as [Any] + "case", + ] as [Any], ] ) } @@ -278,7 +278,7 @@ class TypeContentsOrderRuleTests: SwiftLintTestCase { hasLayoutedView2 = true } } - """) + """), ] let triggeringExamples = [ Example(""" @@ -329,7 +329,7 @@ class TypeContentsOrderRuleTests: SwiftLintTestCase { // some code } } - """) + """), ] let groupedOrderDescription = TypeContentsOrderRule.description @@ -343,8 +343,8 @@ class TypeContentsOrderRuleTests: SwiftLintTestCase { ["type_alias", "associated_type", "subtype"], ["type_property", "instance_property", "ib_inspectable", "ib_outlet"], ["initializer", "type_method", "deinitializer"], - ["view_life_cycle_method", "ib_action", "other_method", "subscript"] - ] + ["view_life_cycle_method", "ib_action", "other_method", "subscript"], + ], ] ) } diff --git a/Tests/SwiftLintFrameworkTests/TypeNameRuleTests.swift b/Tests/SwiftLintFrameworkTests/TypeNameRuleTests.swift index 96270b8e40..2c8819a5ec 100644 --- a/Tests/SwiftLintFrameworkTests/TypeNameRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/TypeNameRuleTests.swift @@ -1,16 +1,16 @@ @testable import SwiftLintBuiltInRules -class TypeNameRuleTests: SwiftLintTestCase { +final class TypeNameRuleTests: SwiftLintTestCase { func testTypeNameWithExcluded() { let baseDescription = TypeNameRule.description let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [ Example("class apple {}"), Example("struct some_apple {}"), - Example("protocol test123 {}") + Example("protocol test123 {}"), ] let triggeringExamples = baseDescription.triggeringExamples + [ Example("enum ap_ple {}"), - Example("typealias appleJuice = Void") + Example("typealias appleJuice = Void"), ] let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples, triggeringExamples: triggeringExamples) @@ -24,7 +24,7 @@ class TypeNameRuleTests: SwiftLintTestCase { Example("struct MyType$ {}"), Example("enum MyType$ {}"), Example("typealias Foo$ = Void"), - Example("protocol Foo {\n associatedtype Bar$\n }") + Example("protocol Foo {\n associatedtype Bar$\n }"), ] let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples) @@ -47,7 +47,7 @@ class TypeNameRuleTests: SwiftLintTestCase { Example("private typealias ↓foo = Void"), Example("class ↓myType {}"), Example("struct ↓myType {}"), - Example("enum ↓myType {}") + Example("enum ↓myType {}"), ] let nonTriggeringExamples = baseDescription.nonTriggeringExamples + triggeringExamplesToRemove.removingViolationMarkers() diff --git a/Tests/SwiftLintFrameworkTests/TypesafeArrayInitRuleTests.swift b/Tests/SwiftLintFrameworkTests/TypesafeArrayInitRuleTests.swift new file mode 100644 index 0000000000..59d0769491 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/TypesafeArrayInitRuleTests.swift @@ -0,0 +1,19 @@ +@testable import SwiftLintBuiltInRules +import XCTest + +final class TypesafeArrayInitRuleTests: SwiftLintTestCase { + func testViolationRuleIdentifier() { + let baseDescription = TypesafeArrayInitRule.description + guard let triggeringExample = baseDescription.triggeringExamples.first else { + XCTFail("No triggering examples found") + return + } + guard let config = makeConfig(nil, baseDescription.identifier) else { + XCTFail("Failed to create configuration") + return + } + let violations = SwiftLintFrameworkTests.violations(triggeringExample, config: config, requiresFileOnDisk: true) + XCTAssertGreaterThanOrEqual(violations.count, 1) + XCTAssertEqual(violations.first?.ruleIdentifier, baseDescription.identifier) + } +} diff --git a/Tests/SwiftLintFrameworkTests/UnneededOverrideRuleTests.swift b/Tests/SwiftLintFrameworkTests/UnneededOverrideRuleTests.swift index 0d731cf32b..8ee55ec026 100644 --- a/Tests/SwiftLintFrameworkTests/UnneededOverrideRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/UnneededOverrideRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class UnneededOverrideRuleTests: SwiftLintTestCase { +final class UnneededOverrideRuleTests: SwiftLintTestCase { func testIncludeAffectInits() { let nonTriggeringExamples = [ Example(""" @@ -22,7 +22,7 @@ class UnneededOverrideRuleTests: SwiftLintTestCase { private override init() { super.init() } - """) + """), ] + UnneededOverrideRuleExamples.nonTriggeringExamples let triggeringExamples = [ @@ -39,7 +39,7 @@ class UnneededOverrideRuleTests: SwiftLintTestCase { super.init(frame: frame) } } - """) + """), ] let corrections = [ @@ -52,7 +52,7 @@ class UnneededOverrideRuleTests: SwiftLintTestCase { """): Example(""" class Foo { } - """) + """), ] let description = UnneededOverrideRule.description diff --git a/Tests/SwiftLintFrameworkTests/UnusedDeclarationConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/UnusedDeclarationConfigurationTests.swift index f677fbe9bf..8b1e4dc492 100644 --- a/Tests/SwiftLintFrameworkTests/UnusedDeclarationConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/UnusedDeclarationConfigurationTests.swift @@ -1,13 +1,13 @@ @testable import SwiftLintBuiltInRules import XCTest -class UnusedDeclarationConfigurationTests: XCTestCase { +final class UnusedDeclarationConfigurationTests: XCTestCase { func testParseConfiguration() throws { var testee = UnusedDeclarationConfiguration() let config = [ "severity": "warning", "include_public_and_open": true, - "related_usrs_to_skip": ["a", "b"] + "related_usrs_to_skip": ["a", "b"], ] as [String: any Sendable] try testee.apply(configuration: config) diff --git a/Tests/SwiftLintFrameworkTests/UnusedOptionalBindingRuleTests.swift b/Tests/SwiftLintFrameworkTests/UnusedOptionalBindingRuleTests.swift index 7f3fe8dc65..e210a3c337 100644 --- a/Tests/SwiftLintFrameworkTests/UnusedOptionalBindingRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/UnusedOptionalBindingRuleTests.swift @@ -1,6 +1,6 @@ @testable import SwiftLintBuiltInRules -class UnusedOptionalBindingRuleTests: SwiftLintTestCase { +final class UnusedOptionalBindingRuleTests: SwiftLintTestCase { func testDefaultConfiguration() { let baseDescription = UnusedOptionalBindingRule.description let triggeringExamples = baseDescription.triggeringExamples + [ diff --git a/Tests/SwiftLintFrameworkTests/VerticalWhitespaceRuleTests.swift b/Tests/SwiftLintFrameworkTests/VerticalWhitespaceRuleTests.swift index b8df57475d..deba0b0844 100644 --- a/Tests/SwiftLintFrameworkTests/VerticalWhitespaceRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/VerticalWhitespaceRuleTests.swift @@ -1,8 +1,8 @@ @testable import SwiftLintBuiltInRules import XCTest -class VerticalWhitespaceRuleTests: SwiftLintTestCase { - private let ruleID = VerticalWhitespaceRule.description.identifier +final class VerticalWhitespaceRuleTests: SwiftLintTestCase { + private let ruleID = VerticalWhitespaceRule.identifier func testAttributesWithMaxEmptyLines() { // Test with custom `max_empty_lines` @@ -21,7 +21,7 @@ class VerticalWhitespaceRuleTests: SwiftLintTestCase { .with(triggeringExamples: []) .with(corrections: [ Example("let b = 0\n\n↓\n↓\n↓\n\nclass AAA {}\n"): Example("let b = 0\n\n\nclass AAA {}\n"), - Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\n\nclass AAA {}\n") + Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\n\nclass AAA {}\n"), ]) verifyRule(maxEmptyLinesDescription, diff --git a/Tests/SwiftLintFrameworkTests/XCTSpecificMatcherRuleTests.swift b/Tests/SwiftLintFrameworkTests/XCTSpecificMatcherRuleTests.swift index 2bf2114589..90c78cfada 100644 --- a/Tests/SwiftLintFrameworkTests/XCTSpecificMatcherRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/XCTSpecificMatcherRuleTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintBuiltInRules import XCTest -class XCTSpecificMatcherRuleTests: SwiftLintTestCase { +final class XCTSpecificMatcherRuleTests: SwiftLintTestCase { func testEqualTrue() { let example = Example("XCTAssertEqual(a, true)") let violations = self.violations(example) @@ -181,7 +181,7 @@ class XCTSpecificMatcherRuleTests: SwiftLintTestCase { } private func violations(_ example: Example) -> [StyleViolation] { - guard let config = makeConfig(nil, XCTSpecificMatcherRule.description.identifier) else { return [] } + guard let config = makeConfig(nil, XCTSpecificMatcherRule.identifier) else { return [] } return SwiftLintFrameworkTests.violations(example, config: config) } diff --git a/Tests/SwiftLintFrameworkTests/XCTestCase+BundlePath.swift b/Tests/SwiftLintFrameworkTests/XCTestCase+BundlePath.swift index 6aedcb8a4e..f20eb5b175 100644 --- a/Tests/SwiftLintFrameworkTests/XCTestCase+BundlePath.swift +++ b/Tests/SwiftLintFrameworkTests/XCTestCase+BundlePath.swift @@ -7,7 +7,7 @@ enum TestResources { return "\(rootProjectDirectory)/Tests/SwiftLintFrameworkTests/Resources" } - return URL(fileURLWithPath: #file, isDirectory: false) + return URL(fileURLWithPath: #filePath, isDirectory: false) .deletingLastPathComponent() .appendingPathComponent("Resources") .path diff --git a/Tests/SwiftLintFrameworkTests/YamlParserTests.swift b/Tests/SwiftLintFrameworkTests/YamlParserTests.swift index 6e4dd56167..4e36027c76 100644 --- a/Tests/SwiftLintFrameworkTests/YamlParserTests.swift +++ b/Tests/SwiftLintFrameworkTests/YamlParserTests.swift @@ -1,7 +1,7 @@ @testable import SwiftLintCore import XCTest -class YamlParserTests: SwiftLintTestCase { +final class YamlParserTests: SwiftLintTestCase { func testParseEmptyString() { XCTAssertEqual((try YamlParser.parse("", env: [:])).count, 0, "Parsing empty YAML string should succeed") @@ -53,4 +53,48 @@ class YamlParserTests: SwiftLintTestCase { _ = try YamlParser.parse("|\na", env: [:]) } } + + func testTreatAllEnvVarsAsStringsWithoutCasting() throws { + let env = [ + "INT": "1", + "FLOAT": "1.0", + "BOOL": "true", + "STRING": "string", + ] + let string = """ + int: ${INT} + float: ${FLOAT} + bool: ${BOOL} + string: ${STRING} + """ + + let result = try YamlParser.parse(string, env: env) + + XCTAssertEqual(result["int"] as? String, "1") + XCTAssertEqual(result["float"] as? String, "1.0") + XCTAssertEqual(result["bool"] as? String, "true") + XCTAssertEqual(result["string"] as? String, "string") + } + + func testRespectCastsOnEnvVars() throws { + let env = [ + "INT": "1", + "FLOAT": "1.0", + "BOOL": "true", + "STRING": "string", + ] + let string = """ + int: !!int ${INT} + float: !!float ${FLOAT} + bool: !!bool ${BOOL} + string: !!str ${STRING} + """ + + let result = try YamlParser.parse(string, env: env) + + XCTAssertEqual(result["int"] as? Int, 1) + XCTAssertEqual(result["float"] as? Double, 1.0) + XCTAssertEqual(result["bool"] as? Bool, true) + XCTAssertEqual(result["string"] as? String, "string") + } } diff --git a/Tests/SwiftLintFrameworkTests/YamlSwiftLintTests.swift b/Tests/SwiftLintFrameworkTests/YamlSwiftLintTests.swift index 8e5d26f050..a001de08d4 100644 --- a/Tests/SwiftLintFrameworkTests/YamlSwiftLintTests.swift +++ b/Tests/SwiftLintFrameworkTests/YamlSwiftLintTests.swift @@ -2,7 +2,7 @@ import Foundation import XCTest import Yams -class YamlSwiftLintTests: SwiftLintTestCase { +final class YamlSwiftLintTests: SwiftLintTestCase { func testFlattenYaml() throws { do { guard let yamlDict = try Yams.load(yaml: try getTestYaml()) as? [String: Any] else { @@ -39,6 +39,6 @@ class YamlSwiftLintTests: SwiftLintTestCase { } private func getTestYaml() throws -> String { - return try String(contentsOfFile: "\(testResourcesPath)/test.yml", encoding: .utf8) + try String(contentsOfFile: "\(testResourcesPath)/test.yml", encoding: .utf8) } } diff --git a/Tests/SwiftLintTestHelpers/TestHelpers.swift b/Tests/SwiftLintTestHelpers/TestHelpers.swift index 7a0c4c5218..06b09ac8b5 100644 --- a/Tests/SwiftLintTestHelpers/TestHelpers.swift +++ b/Tests/SwiftLintTestHelpers/TestHelpers.swift @@ -36,14 +36,14 @@ private extension SwiftLintFile { "-F", frameworks, "-sdk", sdk, - "-j4", path! + "-j4", path!, ] } } public extension String { func stringByAppendingPathComponent(_ pathComponent: String) -> String { - return bridge().appendingPathComponent(pathComponent) + bridge().appendingPathComponent(pathComponent) } } @@ -52,7 +52,7 @@ public let allRuleIdentifiers = Set(RuleRegistry.shared.list.list.keys) public extension Configuration { func applyingConfiguration(from example: Example) -> Configuration { guard let exampleConfiguration = example.configuration, - case let .only(onlyRules) = self.rulesMode, + case let .onlyConfiguration(onlyRules) = self.rulesMode, let firstRule = (onlyRules.first { $0 != "superfluous_disable_command" }), case let configDict: [_: any Sendable] = ["only_rules": onlyRules, firstRule: exampleConfiguration], let typedConfiguration = try? Configuration(dict: configDict) else { return self } @@ -60,7 +60,8 @@ public extension Configuration { } } -public func violations(_ example: Example, config inputConfig: Configuration = Configuration.default, +public func violations(_ example: Example, + config inputConfig: Configuration = Configuration.default, requiresFileOnDisk: Bool = false) -> [StyleViolation] { SwiftLintFile.clearCaches() let config = inputConfig.applyingConfiguration(from: example) @@ -74,20 +75,20 @@ public func violations(_ example: Example, config inputConfig: Configuration = C let file = SwiftLintFile.testFile(withContents: stringStrippingMarkers.code, persistToDisk: true) let storage = RuleStorage() - let collecter = Linter(file: file, configuration: config, compilerArguments: file.makeCompilerArguments()) - let linter = collecter.collect(into: storage) + let collector = Linter(file: file, configuration: config, compilerArguments: file.makeCompilerArguments()) + let linter = collector.collect(into: storage) return linter.styleViolations(using: storage).withoutFiles() } public extension Collection where Element == String { func violations(config: Configuration = Configuration.default, requiresFileOnDisk: Bool = false) -> [StyleViolation] { - return map { SwiftLintFile.testFile(withContents: $0, persistToDisk: requiresFileOnDisk) } + map { SwiftLintFile.testFile(withContents: $0, persistToDisk: requiresFileOnDisk) } .violations(config: config, requiresFileOnDisk: requiresFileOnDisk) } func corrections(config: Configuration = Configuration.default, requiresFileOnDisk: Bool = false) -> [Correction] { - return map { SwiftLintFile.testFile(withContents: $0, persistToDisk: requiresFileOnDisk) } + map { SwiftLintFile.testFile(withContents: $0, persistToDisk: requiresFileOnDisk) } .corrections(config: config, requiresFileOnDisk: requiresFileOnDisk) } } @@ -123,7 +124,7 @@ public extension Collection where Element: SwiftLintFile { private extension Collection where Element == StyleViolation { func withoutFiles() -> [StyleViolation] { - return map { violation in + map { violation in let locationWithoutFile = Location(file: nil, line: violation.location.line, character: violation.location.character) return violation.with(location: locationWithoutFile) @@ -133,7 +134,7 @@ private extension Collection where Element == StyleViolation { private extension Collection where Element == Correction { func withoutFiles() -> [Correction] { - return map { correction in + map { correction in let locationWithoutFile = Location(file: nil, line: correction.location.line, character: correction.location.character) return Correction(ruleDescription: correction.ruleDescription, location: locationWithoutFile) @@ -146,7 +147,7 @@ public extension Collection where Element == Example { /// /// - returns: A new `Array`. func removingViolationMarkers() -> [Element] { - return map { $0.removingViolationMarkers() } + map { $0.removingViolationMarkers() } } } @@ -163,7 +164,7 @@ private func cleanedContentsAndMarkerOffsets(from contents: String) -> (String, } private func render(violations: [StyleViolation], in contents: String) -> String { - var contents = StringView(contents).lines.map { $0.content } + var contents = StringView(contents).lines.map(\.content) for violation in violations.sorted(by: { $0.location > $1.location }) { guard let line = violation.location.line, let character = violation.location.character else { continue } @@ -172,7 +173,8 @@ private func render(violations: [StyleViolation], in contents: String) -> String "\(violation.severity.rawValue): ", "\(violation.ruleName) Violation: ", violation.reason, - " (\(violation.ruleIdentifier))"].joined() + " (\(violation.ruleIdentifier))", + ].joined() if line >= contents.count { contents.append(message) } else { @@ -187,7 +189,7 @@ private func render(violations: [StyleViolation], in contents: String) -> String } private func render(locations: [Location], in contents: String) -> String { - var contents = StringView(contents).lines.map { $0.content } + var contents = StringView(contents).lines.map(\.content) for location in locations.sorted(by: > ) { guard let line = location.line, let character = location.character else { continue } let content = NSMutableString(string: contents[line - 1]) @@ -210,13 +212,17 @@ private extension Configuration { let includeCompilerArguments = self.rules.contains(where: { $0 is any AnalyzerRule }) let compilerArguments = includeCompilerArguments ? file.makeCompilerArguments() : [] let storage = RuleStorage() - let collecter = Linter(file: file, configuration: self, compilerArguments: compilerArguments) - let linter = collecter.collect(into: storage) + let collector = Linter(file: file, configuration: self, compilerArguments: compilerArguments) + let linter = collector.collect(into: storage) let corrections = linter.correct(using: storage).sorted { $0.location < $1.location } if expectedLocations.isEmpty { - XCTAssertEqual( - corrections.count, before.code != expected.code ? 1 : 0, #function + ".expectedLocationsEmpty", - file: before.file, line: before.line) + XCTAssertGreaterThanOrEqual( + corrections.count, + before.code != expected.code ? 1 : 0, + #function + ".expectedLocationsEmpty", + file: before.file, + line: before.line + ) } else { XCTAssertEqual( corrections.count, @@ -259,13 +265,14 @@ private extension Configuration { private extension String { func toStringLiteral() -> String { - return "\"" + replacingOccurrences(of: "\n", with: "\\n") + "\"" + "\"" + replacingOccurrences(of: "\n", with: "\\n") + "\"" } } -public func makeConfig(_ ruleConfiguration: Any?, _ identifier: String, +public func makeConfig(_ ruleConfiguration: Any?, + _ identifier: String, skipDisableCommandTests: Bool = false) -> Configuration? { - let superfluousDisableCommandRuleIdentifier = SuperfluousDisableCommandRule.description.identifier + let superfluousDisableCommandRuleIdentifier = SuperfluousDisableCommandRule.identifier let identifiers: Set = skipDisableCommandTests ? [identifier] : [identifier, superfluousDisableCommandRuleIdentifier] @@ -274,12 +281,12 @@ public func makeConfig(_ ruleConfiguration: Any?, _ identifier: String, return (try? ruleType.init(configuration: ruleConfiguration)).flatMap { configuredRule in let rules = skipDisableCommandTests ? [configuredRule] : [configuredRule, SuperfluousDisableCommandRule()] return Configuration( - rulesMode: .only(identifiers), + rulesMode: .onlyConfiguration(identifiers), allRulesWrapped: rules.map { ($0, false) } ) } } - return Configuration(rulesMode: .only(identifiers)) + return Configuration(rulesMode: .onlyConfiguration(identifiers)) } private func testCorrection(_ correction: (Example, Example), @@ -292,8 +299,8 @@ private func testCorrection(_ correction: (Example, Example), #endif var config = configuration if let correctionConfiguration = correction.0.configuration, - case let .only(onlyRules) = configuration.rulesMode, - let ruleToConfigure = (onlyRules.first { $0 != SuperfluousDisableCommandRule.description.identifier }), + case let .onlyConfiguration(onlyRules) = configuration.rulesMode, + let ruleToConfigure = (onlyRules.first { $0 != SuperfluousDisableCommandRule.identifier }), case let configDict: [_: any Sendable] = ["only_rules": onlyRules, ruleToConfigure: correctionConfiguration], let typedConfiguration = try? Configuration(dict: configDict) { config = configuration.merged(withChild: typedConfiguration, rootDirectory: configuration.rootDirectory) @@ -306,11 +313,11 @@ private func testCorrection(_ correction: (Example, Example), } private func addEmoji(_ example: Example) -> Example { - return example.with(code: "/* 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦 */\n\(example.code)") + example.with(code: "/* 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦 */\n\(example.code)") } private func addShebang(_ example: Example) -> Example { - return example.with(code: "#!/usr/bin/env swift\n\(example.code)") + example.with(code: "#!/usr/bin/env swift\n\(example.code)") } public extension XCTestCase { @@ -325,7 +332,7 @@ public extension XCTestCase { skipDisableCommandTests: Bool = false, testMultiByteOffsets: Bool = true, testShebang: Bool = true, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line) { guard ruleDescription.minSwiftVersion <= .current else { return @@ -349,7 +356,8 @@ public extension XCTestCase { self.verifyLint(ruleDescription, config: config, commentDoesntViolate: commentDoesntViolate, stringDoesntViolate: stringDoesntViolate, skipCommentTests: skipCommentTests, skipStringTests: skipStringTests, disableCommands: disableCommands, - testMultiByteOffsets: testMultiByteOffsets, testShebang: testShebang) + testMultiByteOffsets: testMultiByteOffsets, testShebang: testShebang, + file: file, line: line) self.verifyCorrections(ruleDescription, config: config, disableCommands: disableCommands, testMultiByteOffsets: testMultiByteOffsets) } @@ -363,14 +371,14 @@ public extension XCTestCase { disableCommands: [String] = [], testMultiByteOffsets: Bool = true, testShebang: Bool = true, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line) { func verify(triggers: [Example], nonTriggers: [Example]) { verifyExamples(triggers: triggers, nonTriggers: nonTriggers, configuration: config, - requiresFileOnDisk: ruleDescription.requiresFileOnDisk, file: file, line: line) + requiresFileOnDisk: ruleDescription.requiresFileOnDisk) } func makeViolations(_ example: Example) -> [StyleViolation] { - return violations(example, config: config, requiresFileOnDisk: ruleDescription.requiresFileOnDisk) + violations(example, config: config, requiresFileOnDisk: ruleDescription.requiresFileOnDisk) } let ruleDescription = ruleDescription.focused() @@ -412,18 +420,18 @@ public extension XCTestCase { // Disabled rule doesn't violate and disable command isn't superfluous for command in disableCommands { let disabledTriggers = triggers - .filter { $0.testDisableCommand } + .filter(\.testDisableCommand) .map { $0.with(code: command + $0.code) } for trigger in disabledTriggers { - let violationsPartionedByType = makeViolations(trigger) - .partitioned { $0.ruleIdentifier == SuperfluousDisableCommandRule.description.identifier } + let violationsPartitionedByType = makeViolations(trigger) + .partitioned { $0.ruleIdentifier == SuperfluousDisableCommandRule.identifier } - XCTAssert(violationsPartionedByType.first.isEmpty, + XCTAssert(violationsPartitionedByType.first.isEmpty, "Violation(s) still triggered although rule was disabled", file: trigger.file, line: trigger.line) - XCTAssert(violationsPartionedByType.second.isEmpty, + XCTAssert(violationsPartitionedByType.second.isEmpty, "Disable command was superfluous since no violations(s) triggered", file: trigger.file, line: trigger.line) @@ -431,8 +439,10 @@ public extension XCTestCase { } } - func verifyCorrections(_ ruleDescription: RuleDescription, config: Configuration, - disableCommands: [String], testMultiByteOffsets: Bool, + func verifyCorrections(_ ruleDescription: RuleDescription, + config: Configuration, + disableCommands: [String], + testMultiByteOffsets: Bool, parserDiagnosticsDisabledForTests: Bool = true) { let ruleDescription = ruleDescription.focused() @@ -457,10 +467,10 @@ public extension XCTestCase { } } - private func verifyExamples(triggers: [Example], nonTriggers: [Example], - configuration config: Configuration, requiresFileOnDisk: Bool, - file callSiteFile: StaticString = #file, - line callSiteLine: UInt = #line) { + private func verifyExamples(triggers: [Example], + nonTriggers: [Example], + configuration config: Configuration, + requiresFileOnDisk: Bool) { // Non-triggering examples don't violate for nonTrigger in nonTriggers { let unexpectedViolations = violations(nonTrigger, config: config, @@ -503,7 +513,7 @@ public extension XCTestCase { } // Assert locations missing violation - let violatedLocations = triggerViolations.map { $0.location } + let violatedLocations = triggerViolations.map(\.location) let locationsWithoutViolation = expectedLocations .filter { !violatedLocations.contains($0) } if !locationsWithoutViolation.isEmpty { @@ -527,7 +537,7 @@ public extension XCTestCase { // file and line parameters are first so we can use trailing closure syntax with the closure func checkError( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, _ error: T, closure: () throws -> Void) { @@ -574,6 +584,6 @@ private struct FocusedRuleDescription { private extension RuleDescription { func focused() -> FocusedRuleDescription { - return FocusedRuleDescription(rule: self) + FocusedRuleDescription(rule: self) } } diff --git a/assets/runscript.png b/assets/runscript.png deleted file mode 100644 index 12c35c867d..0000000000 Binary files a/assets/runscript.png and /dev/null differ diff --git a/assets/select-swiftlint-plugin.png b/assets/select-swiftlint-plugin.png deleted file mode 100644 index b219cfaea7..0000000000 Binary files a/assets/select-swiftlint-plugin.png and /dev/null differ diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 60d981f294..6fc1eb3f11 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,38 +4,40 @@ trigger: jobs: - job: Linux pool: - vmImage: 'ubuntu-22.04' + vmImage: 'ubuntu-24.04' strategy: maxParallel: 10 matrix: - swift59: - containerImage: swift:5.9 - container: $[ variables['containerImage'] ] + 'Swift 6': + image: swift:6.0-noble + container: $[ variables['image'] ] + steps: + - script: swift test --parallel -Xswiftc -DDISABLE_FOCUSED_EXAMPLES + displayName: swift test + +- job: macOS + strategy: + maxParallel: 10 + matrix: + '13': + image: 'macOS-13' + xcode: '15.2' + '14': + image: 'macOS-14' + xcode: '15.4' + pool: + vmImage: $(image) + variables: + DEVELOPER_DIR: /Applications/Xcode_$(xcode).app steps: - script: swift test --parallel -Xswiftc -DDISABLE_FOCUSED_EXAMPLES displayName: swift test -# TODO: Re-enable when FB11648454 is fixed -# - job: Xcode -# pool: -# vmImage: 'macOS-12' -# strategy: -# maxParallel: 10 -# matrix: -# xcode14: -# DEVELOPER_DIR: /Applications/Xcode_14.0.1.app -# steps: -# - script: | -# sw_vers -# xcodebuild -version -# displayName: Version Informations -# - script: xcodebuild -scheme swiftlint test -destination "platform=macOS" OTHER_SWIFT_FLAGS="\$(inherited) -D DISABLE_FOCUSED_EXAMPLES" -# displayName: xcodebuild test - job: CocoaPods pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' variables: - DEVELOPER_DIR: /Applications/Xcode_15.0.app + DEVELOPER_DIR: /Applications/Xcode_16.app steps: - script: bundle install --path vendor/bundle displayName: bundle install @@ -44,11 +46,11 @@ jobs: - script: bundle exec pod lib lint --platforms=macos --verbose displayName: pod lib lint -- job: jazzy +- job: Jazzy pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' variables: - DEVELOPER_DIR: /Applications/Xcode_15.0.app + DEVELOPER_DIR: /Applications/Xcode_15.4.app steps: - script: swift run swiftlint generate-docs displayName: Run swiftlint generate-docs diff --git a/bazel/deps.bzl b/bazel/deps.bzl index 13f9e3f6d7..de21f081f1 100644 --- a/bazel/deps.bzl +++ b/bazel/deps.bzl @@ -1,5 +1,4 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -load("@com_github_jpsim_sourcekitten//bazel:repos.bzl", "sourcekitten_repos") def _extra_swift_sources_impl(ctx): ctx.file("WORKSPACE", "") @@ -36,5 +35,3 @@ def swiftlint_deps(): if not native.existing_rule("swiftlint_extra_rules"): extra_swift_sources(name = "swiftlint_extra_rules") - - sourcekitten_repos() diff --git a/bazel/repos.bzl b/bazel/repos.bzl index cbdf471875..447e216d8b 100644 --- a/bazel/repos.bzl +++ b/bazel/repos.bzl @@ -5,16 +5,39 @@ def swiftlint_repos(bzlmod = False): if not bzlmod: http_archive( name = "com_github_jpsim_sourcekitten", - sha256 = "fcc5ea783e6a0b58b3873c3d551c0ff7a146fdd536e66e1d37af13b1f52df3d4", - strip_prefix = "SourceKitten-0.34.1", - url = "https://github.com/jpsim/SourceKitten/releases/download/0.34.1/SourceKitten-0.34.1.tar.gz", + sha256 = "d9c559166f01627826505b0e655b56a59f86938389e1739259e6ce49c9fd95f0", + strip_prefix = "SourceKitten-0.35.0", + url = "https://github.com/jpsim/SourceKitten/releases/download/0.35.0/SourceKitten-0.35.0.tar.gz", ) http_archive( name = "SwiftSyntax", - sha256 = "1cddda9f7d249612e3d75d4caa8fd9534c0621b8a890a7d7524a4689bce644f1", - strip_prefix = "swift-syntax-509.0.0", - url = "https://github.com/apple/swift-syntax/archive/refs/tags/509.0.0.tar.gz", + sha256 = "6572f60ca3c75c2a40f8ccec98c5cd0d3994599a39402d69b433381aaf2c1712", + strip_prefix = "swift-syntax-510.0.2", + url = "https://github.com/swiftlang/swift-syntax/archive/refs/tags/510.0.2.tar.gz", + ) + + http_archive( + name = "sourcekitten_com_github_apple_swift_argument_parser", + url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.3.1.tar.gz", + sha256 = "4d964f874b251abc280ee28f0f187de3c13a6122a9561524f66a10768ca2d837", + build_file = "@com_github_jpsim_sourcekitten//bazel:SwiftArgumentParser.BUILD", + strip_prefix = "swift-argument-parser-1.3.1", + ) + + http_archive( + name = "sourcekitten_com_github_jpsim_yams", + url = "https://github.com/jpsim/Yams/releases/download/5.0.6/Yams-5.0.6.tar.gz", + sha256 = "a81c6b93f5d26bae1b619b7f8babbfe7c8abacf95b85916961d488888df886fb", + strip_prefix = "Yams-5.0.6", + ) + + http_archive( + name = "sourcekitten_com_github_drmohundro_SWXMLHash", + url = "https://github.com/drmohundro/SWXMLHash/archive/refs/tags/7.0.1.tar.gz", + build_file = "@com_github_jpsim_sourcekitten//bazel:SWXMLHash.BUILD", + sha256 = "bafa037a09aa296f180e5613206748db5053b79aa09258c78d093ae9f8102a18", + strip_prefix = "SWXMLHash-7.0.1", ) http_archive( diff --git a/tools/Version.swift.template b/tools/Version.swift.template index bee640a048..685d37697e 100644 --- a/tools/Version.swift.template +++ b/tools/Version.swift.template @@ -1,8 +1,20 @@ /// A type describing the SwiftLint version. -public struct Version { +public struct Version: VersionComparable { /// The string value for this version. public let value: String + /// An alias for `value` required for protocol conformance. + public var rawValue: String { + value + } + /// The current SwiftLint version. - public static let current = Version(value: "__VERSION__") + public static let current = Self(value: "__VERSION__") + + /// Public initializer. + /// + /// - parameter value: The string value for this version. + public init(value: String) { + self.value = value + } } diff --git a/tools/add-preconcurrency-imports.sh b/tools/add-preconcurrency-imports.sh deleted file mode 100755 index acef5af261..0000000000 --- a/tools/add-preconcurrency-imports.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -files=( -"Source/SwiftLintBuiltInRules/Rules/Idiomatic/PrivateOverFilePrivateRule.swift" -"Source/SwiftLintBuiltInRules/Rules/Idiomatic/ToggleBoolRule.swift" -"Source/SwiftLintBuiltInRules/Rules/Style/EmptyEnumArgumentsRule.swift" -"Source/SwiftLintBuiltInRules/Rules/Style/OptionalEnumCaseMatchingRule.swift" -"Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift" -) - -for file in "${files[@]}"; do - sed -i '' -e 's/import SwiftSyntax$/@preconcurrency import SwiftSyntax/g' "$file" -done diff --git a/tools/create-github-release.sh b/tools/create-github-release.sh index 0736a3dd7d..8256ac5a6c 100755 --- a/tools/create-github-release.sh +++ b/tools/create-github-release.sh @@ -12,20 +12,11 @@ release_notes=$(mktemp) # Create GitHub Release release_title="$(sed -n '1s/^## //p' CHANGELOG.md)" -gh release create "$version" --title "$release_title" -F "$release_notes" +gh release create "$version" --title "$release_title" -F "$release_notes" --draft \ + "bazel.tar.gz" \ + "bazel.tar.gz.sha256" \ + "portable_swiftlint.zip" \ + "SwiftLint.pkg" \ + "SwiftLintBinary.artifactbundle.zip" rm "$release_notes" - -# Upload release assets - -files_to_upload=( - "bazel.tar.gz" - "bazel.tar.gz.sha256" - "portable_swiftlint.zip" - "SwiftLint.pkg" - "SwiftLintBinary-macos.artifactbundle.zip" -) - -for file in "${files_to_upload[@]}"; do - gh release upload "$version" "$file" -done diff --git a/tools/info-macos.json.template b/tools/info.json.template similarity index 68% rename from tools/info-macos.json.template rename to tools/info.json.template index ef75578a64..0829f935da 100644 --- a/tools/info-macos.json.template +++ b/tools/info.json.template @@ -8,6 +8,10 @@ { "path": "swiftlint-__VERSION__-macos/bin/swiftlint", "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"] + }, + { + "path": "swiftlint-__VERSION__-linux-gnu/bin/swiftlint", + "supportedTriples": ["x86_64-unknown-linux-gnu"] } ] } diff --git a/tools/oss-check b/tools/oss-check index 1bf0c65e7c..77a70c8365 100755 --- a/tools/oss-check +++ b/tools/oss-check @@ -97,15 +97,12 @@ def fail(str) exit end -def perform(*args) - commands = args - if @options[:verbose] - commands.each do |x| - puts(x) - system(x) - end +def perform(command, dir: nil) + puts command if @options[:verbose] + if dir + Dir.chdir(dir) { perform(command) } else - commands.each { |x| `#{x}` } + system(command) end end @@ -208,24 +205,24 @@ def build(branch) dir = "#{@working_dir}/builds" target = branch == 'main' ? @effective_main_commitish : @options[:branch] if File.directory?(dir) - perform("cd #{dir}; git checkout #{target}") + perform("git checkout #{target}", dir: dir) else perform("git worktree add --detach #{dir} #{target}") end - build_command = "cd #{dir}; bazel build --config=release @SwiftLint//:swiftlint && mv bazel-bin/swiftlint swiftlint-#{branch}" - - perform(build_command) - return if $?.success? + build_command = "bazel build --config=release @SwiftLint//:swiftlint" return_value = nil puts build_command if @options[:verbose] - Open3.popen3(build_command) do |_, stdout, _, wait_thr| + Open3.popen3(build_command, :chdir=>"#{dir}") do |_, stdout, stderr, wait_thr| puts stdout.read.chomp + puts stderr.read.chomp return_value = wait_thr.value end fail "Could not build #{branch}" unless return_value.success? + + perform("mv bazel-bin/swiftlint swiftlint-#{branch}", dir: dir) end def diff_and_report_changes_to_danger diff --git a/tools/update-artifact-bundle.sh b/tools/update-artifact-bundle.sh index 09bac52039..032a423757 100755 --- a/tools/update-artifact-bundle.sh +++ b/tools/update-artifact-bundle.sh @@ -3,13 +3,13 @@ set -euo pipefail readonly version="$1" -readonly artifactbundle="SwiftLintBinary-macos.artifactbundle.zip" +readonly artifactbundle="SwiftLintBinary.artifactbundle.zip" readonly checksum="$(shasum -a 256 "$artifactbundle" | cut -d " " -f1 | xargs)" sed -i '' \ - "s/.*\/releases\/download\/.*/ url: \"https:\/\/github.com\/realm\/SwiftLint\/releases\/download\/$version\/SwiftLintBinary-macos\.artifactbundle\.zip\",/g" \ + "s/.*\/releases\/download\/.*/ url: \"https:\/\/github.com\/realm\/SwiftLint\/releases\/download\/$version\/SwiftLintBinary\.artifactbundle\.zip\",/g" \ Package.swift sed -i '' \ - "s/.*checksum.*/ checksum: \"$checksum\"/g" \ + "s/.*checksum.*/ checksum: \"$checksum\"/g" \ Package.swift