diff --git a/.azure/templates/run-make.yml b/.azure/templates/run-make.yml deleted file mode 100644 index 8861ec66d8..0000000000 --- a/.azure/templates/run-make.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Run the commands in the Makefile for the specified rule. - -parameters: - - name: rule - type: string - -steps: - - script: >- - awk ' - $0 ~ "${{ parameters.rule }}:" { in_rule = 1; next } - in_rule && /^\t/ { print $0 } - in_rule && !/^\t/ { in_rule = 0 } - ' Makefile | while IFS= read -r command; do - eval "$command" - done - displayName: Run `${{ parameters.rule }}` rule diff --git a/.bazelrc b/.bazelrc index 12ae6e95c6..0304307c3c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -13,6 +13,7 @@ build --disk_cache=~/.bazel_cache build --experimental_remote_cache_compression build --remote_build_event_upload=minimal build --nolegacy_important_outputs +build --incompatible_strict_action_env build:release \ --compilation_mode=opt \ diff --git a/.bazelversion b/.bazelversion index 66ce77b7ea..e8be68404b 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.0.0 +7.6.1 diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 826d816d63..d887fc11a2 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -28,6 +28,6 @@ steps: - label: "Registration" commands: - echo "+++ Register Rules and Reporters" - - make --always-make register + - make --always-make bazel_register - echo "+++ Diff Files" - git diff --quiet HEAD diff --git a/.github/actions/bazel-linux-build/action.yml b/.github/actions/bazel-linux-build/action.yml new file mode 100644 index 0000000000..685acc51c4 --- /dev/null +++ b/.github/actions/bazel-linux-build/action.yml @@ -0,0 +1,24 @@ +name: Bazel Linux Build +description: Common steps to build SwiftLint with Bazel on GitHub Linux runners +inputs: + target: + description: The Bazel target to build + default: //:swiftlint +runs: + using: composite + steps: + - name: Create ci.bazelrc file + shell: bash + run: echo "$CI_BAZELRC_FILE_CONTENT" | base64 -d > ci.bazelrc + env: + CI_BAZELRC_FILE_CONTENT: ${{ env.CI_BAZELRC_FILE_CONTENT }} + - name: Apply patch + shell: bash + run: git apply --ignore-whitespace .bcr/patches/no-warnings-as-errors.patch + - name: Build SwiftLint with Bazel + shell: bash + run: | + export PATH="/usr/share/swift/usr/bin:$PATH" + bazel build --config release ${{ inputs.target }} + env: + CC: clang diff --git a/.github/actions/run-make/action.yml b/.github/actions/run-make/action.yml new file mode 100644 index 0000000000..0ff574904e --- /dev/null +++ b/.github/actions/run-make/action.yml @@ -0,0 +1,19 @@ +name: Run Make Rule +description: Runs a specified Makefile rule +inputs: + rule: + description: The Makefile rule to run + required: true + default: build +runs: + using: composite + steps: + - run: | + awk ' + $0 ~ "${{ inputs.rule }}:" { in_rule = 1; next } + in_rule && /^\t/ { print $0 } + in_rule && !/^\t/ { in_rule = 0 } + ' Makefile | while IFS= read -r command; do + eval "$command" + done + shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..cd6188a93f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +You are working on SwiftLint, a linter for Swift code that enforces coding style and conventions. It helps maintain a clean and consistent codebase by identifying and reporting issues in Swift files. It can even automatically fix some of these issues. + +Linting rules are defined in `Source/SwiftLintBuiltInRules/Rules`. If someone mentions a rule by its identifier that is in "snake_case" (e.g., `rule_name`), you can usually find the rule's implementation file named "UpperCamelCaseRule.swift" (e.g., `Rule.swift`) in one of the sub-folders depending on the rule's kind. Specific configurations for rules are located in the `RuleConfigurations` folder, which contains files named as `Configuration.swift` (e.g., `IdentifierNameConfiguration.swift`). + +User-facing changes must be documented in the `CHANGELOG.md` file, which is organized by version. New entries always go into the "Main" section. They give credit to the person who has made the change and they reference the issue which has been fixed by the change. + +All changes need to pass `swift test` as well as running SwiftLint on itself. This is done by running `swift run swiftlint` in the root directory of the project. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..b321e61622 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + bazel_linux: + name: Bazel, Linux, Swift 6.1 # pre-installed + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/bazel-linux-build + name: Build SwiftLint with Bazel + env: + CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} + + plugins_linux: + name: SPM plugins, Linux, Swift ${{ matrix.version }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - image: swift:5.9-focal + version: '5.9' + - image: swift:5.10-noble + version: '5.10' + - image: swift:6.0-noble + version: '6.0' + - image: swift:6.1-noble + version: '6.1' + container: ${{ matrix.image }} + steps: + - uses: actions/checkout@v4 + - name: Build plugins + uses: ./.github/actions/run-make + with: + rule: spm_build_plugins diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..cb12d47d0d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,61 @@ +name: Documentation + +on: + push: + branches: [main] + pull_request: + +jobs: + create-docs: + name: Create + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Build SwiftLint and SourceKitten + uses: ./.github/actions/bazel-linux-build + with: + target: "//:swiftlint @com_github_jpsim_sourcekitten//:sourcekitten" + env: + CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} + - uses: actions/cache@v4 + with: + key: ${{ runner.os }}-swift-spm-${{ hashFiles('Package.resolved') }} + restore-keys: ${{ runner.os }}-swift-spm- + path: .build + - name: Generate documentation + run: | + export PATH="/usr/share/swift/usr/bin:$PATH" + make docs_linux + - name: Validate documentation coverage + run: | + if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then + echo "Undocumented declarations:" + cat docs/undocumented.json + exit 1 + fi + - name: Upload documentation + if: github.event_name == 'push' + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + deploy-docs: + name: Deploy + runs-on: ubuntu-24.04 + needs: create-docs + if: github.event_name == 'push' + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy documentation + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8b2c429a6f..7f72c5a0e8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,8 +2,6 @@ name: Lint on: pull_request: - branches: - - '*' permissions: contents: read @@ -11,27 +9,30 @@ permissions: jobs: lint-swift: - name: Lint Swift + name: Swift runs-on: ubuntu-24.04 # "Noble Numbat" - container: swift:6.1-noble steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/bazel-linux-build + name: Build SwiftLint with Bazel + env: + CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} - name: Lint - run: swift run swiftlint --reporter github-actions-logging --strict 2> /dev/null + run: ./bazel-bin/swiftlint lint --reporter github-actions-logging --strict 2> /dev/null lint-markdown: - name: Lint Markdown + name: Markdown runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Lint - uses: DavidAnson/markdownlint-cli2-action@v19 + uses: DavidAnson/markdownlint-cli2-action@v20 with: globs: | CHANGELOG.md CONTRIBUTING.md README.md lint-actions: - name: Lint Actions + name: Actions runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bddfa6173e..06cd5b851a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,7 @@ jobs: run: | sed 's/__VERSION__/${{ inputs.version }}/g' tools/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift sed -i -e '3s/.*/ version = "${{ inputs.version }}",/' MODULE.bazel + sed -i -e "s/^\(\s*s\.version\s*=\s*'\)[^']*'/\1${{ inputs.version }}'/" SwiftLint.podspec - name: Configure Git author uses: Homebrew/actions/git-user-config@master with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..eb26266e80 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +env: + SKIP_INTEGRATION_TESTS: 'true' + +jobs: + spm_linux: + name: SPM, Linux, Swift 6.1 + runs-on: ubuntu-24.04 + container: swift:6.1-noble + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + key: ${{ runner.os }}-swift-spm-${{ hashFiles('Package.resolved') }} + restore-keys: ${{ runner.os }}-swift-spm- + path: .build + - name: Run tests + uses: ./.github/actions/run-make + with: + rule: spm_test + + spm_macos: + name: SPM, macOS ${{ matrix.macOS }}, Xcode ${{ matrix.xcode }} + runs-on: macos-${{ matrix.macOS }} + strategy: + matrix: + include: + - macOS: '14' + xcode: '15.4' + - macOS: '15' + xcode: '16.4' + env: + DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + key: ${{ runner.os }}-xcode-spm-${{ matrix.xcode }}-${{ hashFiles('Package.resolved') }} + restore-keys: ${{ runner.os }}-xcode-spm-${{ matrix.xcode }}- + path: .build + - name: Run tests + run: make spm_test diff --git a/.gitignore b/.gitignore index ebc2d33e63..e6d5048bee 100644 --- a/.gitignore +++ b/.gitignore @@ -61,9 +61,10 @@ Packages/ # Bundler .bundle/ bundle/ +bin/ # Bazel -bazel-* +/bazel-* /MODULE.bazel.lock # Danger diff --git a/.sourcekit-lsp/config.json b/.sourcekit-lsp/config.json new file mode 100644 index 0000000000..e93a7dc4f6 --- /dev/null +++ b/.sourcekit-lsp/config.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/release/6.1/config.schema.json", + "backgroundIndexing": false +} diff --git a/.swiftlint.yml b/.swiftlint.yml index 063f69e000..5c349312a8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -16,7 +16,6 @@ opt_in_rules: - all disabled_rules: - anonymous_argument_in_multiline_closure - - async_without_await - conditional_returns_on_newline - contrasted_opening_brace - convenience_type @@ -75,6 +74,8 @@ file_name: - RuleConfigurationMacros.swift - SwiftSyntax+SwiftLint.swift - TestHelpers.swift + excluded_paths: + - Tests/GeneratedTests/GeneratedTests_\d\d\.swift final_test_case: *unit_test_configuration function_body_length: 60 identifier_name: @@ -88,7 +89,9 @@ redundant_type_annotation: single_test_class: *unit_test_configuration trailing_comma: mandatory_comma: true -type_body_length: 400 +type_body_length: + warning: 400 + excluded_types: [] unneeded_override: affect_initializers: true unused_import: diff --git a/BUILD b/BUILD index e862723d18..a9466f09d5 100644 --- a/BUILD +++ b/BUILD @@ -7,7 +7,7 @@ load( "swift_library", "universal_swift_compiler_plugin", ) -load("@build_bazel_rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@build_bazel_rules_shell//shell:sh_test.bzl", "sh_test") bool_flag( @@ -99,7 +99,7 @@ swift_library( ":Yams.wrapper", "@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable", ] + select({ - "@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"], + "@platforms//os:linux": ["@swiftlint_com_github_krzyzanowskim_cryptoswift//:CryptoSwift"], "//conditions:default": [":DyldWarningWorkaround"], }), ) @@ -110,7 +110,7 @@ swift_library( module_name = "YamsWrapper", visibility = ["//visibility:private"], deps = [ - "@sourcekitten_com_github_jpsim_yams//:Yams", + "@com_github_jpsim_yams//:Yams", ], ) @@ -154,7 +154,7 @@ swift_library( ":SwiftLintBuiltInRules", ":SwiftLintCore", ":SwiftLintExtraRules", - "@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit", + "@swiftlint_com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit", ], ) @@ -166,7 +166,7 @@ swift_binary( visibility = ["//visibility:public"], deps = [ ":SwiftLintFramework", - "@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser", + "@com_github_apple_swift_argument_parser//:ArgumentParser", "@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable", ], ) @@ -179,7 +179,7 @@ swift_binary( visibility = ["//visibility:public"], deps = [ ":SwiftLintFramework", - "@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser", + "@com_github_apple_swift_argument_parser//:ArgumentParser", ], ) @@ -190,7 +190,7 @@ apple_universal_binary( "x86_64", "arm64", ], - minimum_os_version = "12.0", + minimum_os_version = "13.0", platform_type = "macos", visibility = ["//visibility:public"], ) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b5a96b8b..29ecb8b627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,105 @@ Use the severity levels `off`, `warning` or `error` instead. [kaseken](https://github.com/kaseken) +* SwiftLint now requires macOS 13 or higher to run. + [JP Simard](https://github.com/jpsim) + +* In `SwiftLintBuildToolPlugin`, print the content of the `BUILD_WORKSPACE_DIRECTORY` + environment variable only in debug builds. + [SimplyDanny](https://github.com/SimplyDanny) + [#6135](https://github.com/realm/SwiftLint/issues/6135) + ### Experimental * None. ### Enhancements -* None. +* Add `include_variables` configuration option to `non_optional_string_data_conversion` rule. + When enabled, the rule will trigger on variables, properties, and function calls in addition + to string literals. Defaults to `false` for backward compatibility. + [SimplyDanny](https://github.com/SimplyDanny) + [#6094](https://github.com/realm/SwiftLint/issues/6094) + +* Add Sendable conformance to Rule.Type for building with Swift 6. + [erikkerber](https://github.com/erikkerber) + [#issue_number](https://github.com/realm/SwiftLint/issues/issue_number) + +* Fix false positives for `Actor`-conforming delegate protocols in the + `class_delegate_protocol` rule. + [imsonalbajaj](https://github.com/imsonalbajaj) + [#6054](https://github.com/realm/SwiftLint/issues/6054) + +* Support extensions and protocols in `type_body_length` rule. They can be configured using the + new `excluded_types` option which by default excludes `extension` and `protocol` types. + This means the rule now checks `struct`, `class`, `actor` and `enum` by default. To enable + checking of extensions and protocols, set `excluded_types` to an empty array or exclude other + types as needed. + [SimplyDanny](https://github.com/SimplyDanny) + +* Ignore various assignment operators like `=`, `+=`, `&=`, etc. with right-hand side + ternary expressions otherwise violating the `void_function_in_ternary` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5611](https://github.com/realm/SwiftLint/issues/5611) + +* Rewrite the following rules with SwiftSyntax: + * `accessibility_label_for_image` + * `accessibility_trait_for_button` + * `closure_end_indentation` + * `expiring_todo` + * `file_header` + * `file_length` + * `line_length` + * `trailing_whitespace` + * `vertical_whitespace` + + [JP Simard](https://github.com/jpsim) + [Matt Pennig](https://github.com/pennig) + +* Add `excluded_paths` option to the `file_name` rule. It allows to exclude complete file + paths from analysis. All entries are treated as regular expressions. A single match in + its full path is enough to ignore a file. This is different from the `excluded` option + that only accepts and checks against file names. + [Ueeek](https://github.com/Ueeek) + [#6066](https://github.com/realm/SwiftLint/issues/6066) + +* Fix false positives of `redundant_discardable_let` rule in `@ViewBuilder` functions, + `#Preview` macro bodies and preview providers when `ignore_swiftui_view_bodies` is + enabled. + [kaseken](https://github.com/kaseken) + [#6063](https://github.com/realm/SwiftLint/issues/6063) + +* Improve `multiline_parameters` rule to correctly support + `max_number_of_single_line_parameters` and detect mixed formatting. + [GandaLF2006](https://github.com/GandaLF2006) + +* Add new `prefer_condition_list` rule that triggers when a `guard`/`if`/`while` + condition is composed of multiple expressions connected by the `&&` operator. + It suggests to use a condition list instead, which is more idiomatic. + [SimplyDanny](https://github.com/SimplyDanny) + +* Add `ignore_coding_keys` parameter to `nesting` rule. Setting this to true prevents + `CodingKey` enums from violating the rule. + [braker1nine](https://github.com/braker1nine) + [#5641](https://github.com/realm/SwiftLint/issues/5641) + +* Support deinitializers and subscripts in `function_body_length` rule. + [SimplyDanny](https://github.com/SimplyDanny) +* Add new `allowed_types` option to the `one_declaration_per_file` rule. + [Alfons Hoogervorst](https://github.com/snofla) + [#6072](https://github.com/realm/SwiftLint/issues/6072) ### Bug Fixes -* None. +* Improved error reporting when SwiftLint exits, because of an invalid configuration file + or other error. + [Martin Redington](https://github.com/mildm8nnered) + [#6052](https://github.com/realm/SwiftLint/issues/6052) + +* Keep the default severity levels when neither `warning` nor `error` values are configured. + Ensure especially that the `error` level is not set to `nil` when the `warning` level + isn't set either. + [SimplyDanny](https://github.com/SimplyDanny) ## 0.59.1: Crisp Spring Clean @@ -3847,10 +3935,9 @@ This is the last release to support building with Swift 5.0.x. * API docs for SwiftLintFramework are now available at [realm.github.io/SwiftLint](https://realm.github.io/SwiftLint). `Rules.md` - now redirects to the rules directory in the API docs - [here](https://realm.github.io/SwiftLint/rule-directory.html). Contributors no - longer need to update rule documentation in PRs as this is now done - automatically. The rule documentation now includes the default configuration. + now redirects to the [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) + in the API docs. Contributors no longer need to update rule documentation in PRs as this is + now done automatically. The rule documentation now includes the default configuration. [JP Simard](https://github.com/jpsim) [#1653](https://github.com/realm/SwiftLint/issues/1653) [#1704](https://github.com/realm/SwiftLint/issues/1704) diff --git a/MODULE.bazel b/MODULE.bazel index b18d64a226..c92550eb2f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -5,23 +5,23 @@ module( repo_name = "SwiftLint", ) -bazel_dep(name = "apple_support", version = "1.16.0", repo_name = "build_bazel_apple_support") +bazel_dep(name = "apple_support", version = "1.21.1", 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.20.1", repo_name = "build_bazel_rules_apple") -bazel_dep(name = "rules_cc", version = "0.1.1", repo_name = "build_bazel_rules_cc") +bazel_dep(name = "platforms", version = "0.0.11") +bazel_dep(name = "rules_apple", version = "4.0.1", repo_name = "build_bazel_rules_apple") +bazel_dep(name = "rules_cc", version = "0.1.1") bazel_dep(name = "rules_shell", version = "0.4.0", repo_name = "build_bazel_rules_shell") -bazel_dep(name = "rules_swift", version = "2.8.1", repo_name = "build_bazel_rules_swift") -bazel_dep(name = "sourcekitten", version = "0.37.0", repo_name = "com_github_jpsim_sourcekitten") -bazel_dep(name = "swift_argument_parser", version = "1.3.1.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") -bazel_dep(name = "swift-syntax", version = "601.0.0", repo_name = "SwiftSyntax") -bazel_dep(name = "yams", version = "5.3.0", repo_name = "sourcekitten_com_github_jpsim_yams") +bazel_dep(name = "rules_swift", version = "2.8.1", max_compatibility_level = 3, repo_name = "build_bazel_rules_swift") +bazel_dep(name = "sourcekitten", version = "0.37.2", repo_name = "com_github_jpsim_sourcekitten") +bazel_dep(name = "swift_argument_parser", version = "1.6.1", repo_name = "com_github_apple_swift_argument_parser") +bazel_dep(name = "swift-syntax", version = "601.0.1.1", repo_name = "SwiftSyntax") +bazel_dep(name = "yams", version = "6.0.2", repo_name = "com_github_jpsim_yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") use_repo( swiftlint_repos, - "com_github_johnsundell_collectionconcurrencykit", - "com_github_krzyzanowskim_cryptoswift", + "swiftlint_com_github_johnsundell_collectionconcurrencykit", + "swiftlint_com_github_krzyzanowskim_cryptoswift", "swiftlint_com_github_scottrhoyt_swifty_text_table", ) diff --git a/Makefile b/Makefile index f8d5c8c425..d96690f97b 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,11 @@ register: swift run swiftlint-dev rules register swift run swiftlint-dev reporters register +bazel_register: + bazel build //:swiftlint-dev + ./bazel-bin/swiftlint-dev rules register + ./bazel-bin/swiftlint-dev reporters register + test: clean_xcode $(BUILD_TOOL) $(XCODEFLAGS) test @@ -183,6 +188,12 @@ docs: bundle_install swift run swiftlint generate-docs bundle exec jazzy +docs_linux: bundle_install + bundle binstubs jazzy + ./bazel-bin/swiftlint generate-docs + ./bazel-bin/external/sourcekitten~/sourcekitten doc --spm --module-name SwiftLintCore > doc.json + ./bin/jazzy --sourcekitten-sourcefile doc.json + get_version: @echo "$(VERSION_STRING)" diff --git a/Package.resolved b/Package.resolved index 82bf79fda6..ce663816cd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "729e01bc9b9dab466ac85f21fb9ee2bc1c61b258", - "version" : "1.8.4" + "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f", + "version" : "1.9.0" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "eb6656ed26bdef967ad8d07c27e2eab34dc582f2", - "version" : "0.37.0" + "revision" : "731ffe6a35344a19bab00cdca1c952d5b4fee4d8", + "version" : "0.37.2" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "1103c45ece4f7fe160b8f75b4ea1ee2e5fac1841", - "version" : "601.0.0" + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { - "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", - "version" : "5.3.1" + "revision" : "f4d4d6827d36092d151ad7f6fef1991c1b7192f6", + "version" : "6.0.2" } } ], diff --git a/Package.swift b/Package.swift index 81d6bc12f7..5a9b74765e 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ swiftLintPluginDependencies = [.target(name: "swiftlint")] let package = Package( name: "SwiftLint", - platforms: [.macOS(.v12)], + platforms: [.macOS(.v13)], products: [ .executable(name: "swiftlint", targets: ["swiftlint"]), .library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]), @@ -32,13 +32,13 @@ let package = Package( .plugin(name: "SwiftLintCommandPlugin", targets: ["SwiftLintCommandPlugin"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.2.1")), - .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.0"), - .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMajor(from: "0.37.0")), - .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "5.3.0")), + .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.6.1")), + .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"), + .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMajor(from: "0.37.2")), + .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "6.0.2")), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", .upToNextMajor(from: "0.9.0")), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", .upToNextMajor(from: "0.2.0")), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.4")), + .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.9.0")), ], targets: [ .executableTarget( diff --git a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift index 2005c4dfda..5e70a37c32 100644 --- a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift +++ b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift @@ -6,7 +6,7 @@ struct SwiftLintBuildToolPlugin: BuildToolPlugin { func createBuildCommands( context: PluginContext, target: Target - ) async throws -> [Command] { + ) throws -> [Command] { try makeCommand(executable: context.tool(named: "swiftlint"), swiftFiles: (target as? SourceModuleTarget).flatMap(swiftFiles) ?? [], environment: environment(context: context, target: target), @@ -50,7 +50,9 @@ struct SwiftLintBuildToolPlugin: BuildToolPlugin { return [] } // Outputs the environment to the build log for reference. + #if DEBUG print("Environment:", environment) + #endif let arguments: [String] = [ "lint", "--quiet", diff --git a/README.md b/README.md index 73ec013550..7ea22543f5 100644 --- a/README.md +++ b/README.md @@ -596,8 +596,8 @@ continues to contribute more over time. [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 find an updated list of rules and more information about them in the +[Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html). You can also check the [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules) diff --git a/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift b/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift deleted file mode 100644 index 088917375e..0000000000 --- a/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift +++ /dev/null @@ -1,186 +0,0 @@ -import SourceKittenFramework - -/// Struct to represent SwiftUI ViewModifiers for the purpose of finding modifiers in a substructure. -struct SwiftUIModifier { - /// Name of the modifier. - let name: String - - /// List of arguments to check for in the modifier. - let arguments: [Argument] - - struct Argument { - /// Name of the argument we want to find. For single unnamed arguments, use the empty string. - let name: String - - /// Whether or not the argument is required. If the argument is present, value checks are enforced. - /// Allows for better handling of modifiers with default values for certain arguments where we want - /// to ensure that the default value is used. - let required: Bool - - /// List of possible values for the argument. Typically should just be a list with a single element, - /// but allows for the flexibility of checking for multiple possible values. To only check for the presence - /// of the modifier and not enforce any certain values, pass an empty array. All values are parsed as - /// Strings; for other types (boolean, numeric, optional, etc) types you can check for "true", "5", "nil", etc. - let values: [String] - - /// Success criteria used for matching values (prefix, suffix, substring, exact match, or none). - let matchType: MatchType - - init(name: String, required: Bool = true, values: [String], matchType: MatchType = .exactMatch) { - self.name = name - self.required = required - self.values = values - self.matchType = matchType - } - } - - enum MatchType { - case prefix, suffix, substring, exactMatch - - /// Compares the parsed argument value to a target value for the given match type - /// and returns true is a match is found. - func matches(argumentValue: String, targetValue: String) -> Bool { - switch self { - case .prefix: - return argumentValue.hasPrefix(targetValue) - case .suffix: - return argumentValue.hasSuffix(targetValue) - case .substring: - return argumentValue.contains(targetValue) - case .exactMatch: - return argumentValue == targetValue - } - } - } -} - -/// Extensions for recursively checking SwiftUI code for certain modifiers. -extension SourceKittenDictionary { - /// Call on a SwiftUI View to recursively check the substructure for a certain modifier with certain arguments. - /// - Parameters: - /// - modifiers: A list of `SwiftUIModifier` structs to check for in the view's substructure. - /// In most cases, this can just be a single modifier, but since some modifiers have - /// multiple versions, this enables checking for any modifier from the list. - /// - file: The SwiftLintFile object for the current file, used to extract argument values. - /// - Returns: A boolean value representing whether or not the given modifier with the specified - /// arguments appears in the view's substructure. - func hasModifier(anyOf modifiers: [SwiftUIModifier], in file: SwiftLintFile) -> Bool { - // SwiftUI ViewModifiers are treated as `call` expressions, and we make sure we can get the expression's name. - guard expressionKind == .call, let name else { - return false - } - - // If any modifier from the list matches, return true. - for modifier in modifiers { - // Check for the given modifier name - guard name.hasSuffix(modifier.name) else { - continue - } - - // Check arguments. - var matchesArgs = true - for argument in modifier.arguments { - var foundArg = false - var argValue: String? - - // Check for single unnamed argument. - if argument.name.isEmpty { - foundArg = true - argValue = getSingleUnnamedArgumentValue(in: file) - } else if let parsedArgument = enclosedArguments.first(where: { $0.name == argument.name }) { - foundArg = true - argValue = parsedArgument.getArgumentValue(in: file) - } - - // If argument is not required and we didn't find it, continue. - if !foundArg && !argument.required { - continue - } - - // Otherwise, we must have found an argument with a non-nil value to continue. - guard foundArg, let argumentValue = argValue else { - matchesArgs = false - break - } - - // Argument value can match any of the options given in the argument struct. - if argument.values.isEmpty || argument.values.contains(where: { - argument.matchType.matches(argumentValue: argumentValue, targetValue: $0) - }) { - // Found a match, continue to next argument. - continue - } - // Did not find a match, exit loop over arguments. - matchesArgs = false - break - } - - // Return true if all arguments matched - if matchesArgs { - return true - } - } - - // Recursively check substructure. - // SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like: - // Image("myImage").resizable().accessibility(hidden: true).frame - // --> Image("myImage").resizable().accessibility - // --> Image("myImage").resizable - // --> Image - return substructure.contains(where: { $0.hasModifier(anyOf: modifiers, in: file) }) - } - - // MARK: Sample use cases of `hasModifier` that are used in multiple rules - - /// Whether or not the dictionary represents a SwiftUI View with an `accesibilityHidden(true)` - /// or `accessibility(hidden: true)` modifier. - func hasAccessibilityHiddenModifier(in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityHidden", - arguments: [.init(name: "", values: ["true"])] - ), - SwiftUIModifier( - name: "accessibility", - arguments: [.init(name: "hidden", values: ["true"])] - ), - ], - in: file - ) - } - - /// 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 { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityElement", - arguments: [.init(name: "children", required: false, values: [".ignore"], matchType: .suffix)] - ), - ], - in: file - ) - } - - // MARK: Helpers to extract argument values - - /// Helper to get the value of an argument. - func getArgumentValue(in file: SwiftLintFile) -> String? { - guard expressionKind == .argument, let bodyByteRange else { - return nil - } - - return file.stringView.substringWithByteRange(bodyByteRange) - } - - /// Helper to get the value of a single unnamed argument to a function call. - func getSingleUnnamedArgumentValue(in file: SwiftLintFile) -> String? { - guard expressionKind == .call, let bodyByteRange else { - return nil - } - - return file.stringView.substringWithByteRange(bodyByteRange) - } -} diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 0138cc743e..bea8255b9c 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -148,6 +148,7 @@ public let builtInRules: [any Rule.Type] = [ OverrideInExtensionRule.self, PatternMatchingKeywordsRule.self, PeriodSpacingRule.self, + PreferConditionListRule.self, PreferKeyPathRule.self, PreferNimbleRule.self, PreferSelfInStaticReferencesRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift index 40bd172131..d1dff3d374 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift @@ -34,11 +34,11 @@ private extension DiscouragedObjectLiteralRule { return } - if !configuration.imageLiteral && identifierText == "imageLiteral" { + if !configuration.imageLiteral, identifierText == "imageLiteral" { return } - if !configuration.colorLiteral && identifierText == "colorLiteral" { + if !configuration.colorLiteral, identifierText == "colorLiteral" { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift index 18f65633b0..336a8243c1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift @@ -238,11 +238,11 @@ private extension ExprSyntax { return true } if let expr = self.as(MemberAccessExprSyntax.self), - expr.description.split(separator: ".").allSatisfy(\.startsWithUppercase) { + expr.description.split(separator: ".").allSatisfy(\.startsWithUppercase) { return true } if let expr = self.as(GenericSpecializationExprSyntax.self)?.expression.as(DeclReferenceExprSyntax.self), - expr.baseName.text.startsWithUppercase { + expr.baseName.text.startsWithUppercase { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift index 892ca60313..0a5d60e549 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift @@ -13,9 +13,9 @@ struct FileNameNoSpaceRule: OptInRule, SourceKitFreeRule { func validate(file: SwiftLintFile) -> [StyleViolation] { guard let filePath = file.path, - case let fileName = filePath.bridge().lastPathComponent, - !configuration.excluded.contains(fileName), - fileName.rangeOfCharacter(from: .whitespaces) != nil else { + case let fileName = filePath.bridge().lastPathComponent, + !configuration.excluded.contains(fileName), + fileName.rangeOfCharacter(from: .whitespaces) != nil else { return [] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift index 6c6ab9cf21..f4651b9ef0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift @@ -12,25 +12,25 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { func validate(file: SwiftLintFile) -> [StyleViolation] { guard let filePath = file.path, - case let fileName = filePath.bridge().lastPathComponent, - !configuration.excluded.contains(fileName) else { + !configuration.shouldExclude(filePath: filePath) else { return [] } let prefixRegex = regex("\\A(?:\(configuration.prefixPattern))") let suffixRegex = regex("(?:\(configuration.suffixPattern))\\z") + let fileName = filePath.bridge().lastPathComponent var typeInFileName = fileName.bridge().deletingPathExtension // Process prefix if let match = prefixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange), - let range = typeInFileName.nsrangeToIndexRange(match.range) { + let range = typeInFileName.nsrangeToIndexRange(match.range) { typeInFileName.removeSubrange(range) } // Process suffix if let match = suffixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange), - let range = typeInFileName.nsrangeToIndexRange(match.range) { + let range = typeInFileName.nsrangeToIndexRange(match.range) { typeInFileName.removeSubrange(range) } @@ -38,10 +38,10 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { let allDeclaredTypeNames = TypeNameCollectingVisitor( requireFullyQualifiedNames: configuration.requireFullyQualifiedNames ) - .walk(tree: file.syntaxTree, handler: \.names) - .map { - $0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator) - } + .walk(tree: file.syntaxTree, handler: \.names) + .map { + $0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator) + } guard allDeclaredTypeNames.isNotEmpty, !allDeclaredTypeNames.contains(typeInFileName) else { return [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift index 91502f3403..3001e30cee 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift @@ -67,7 +67,7 @@ private extension GenericTypeNameRule { ) ) } else if let caseCheckSeverity = configuration.validatesStartWithLowercase.severity, - !String(name[name.startIndex]).isUppercase() { + !String(name[name.startIndex]).isUppercase() { violations.append( ReasonedRuleViolation( position: node.positionAfterSkippingLeadingTrivia, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift index b68a8914fc..bd75545237 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift @@ -62,8 +62,8 @@ private extension ObjectLiteralRule { private func isColorInit(node: FunctionCallExprSyntax, name: String) -> Bool { guard inits(forClasses: ["UIColor", "NSColor"]).contains(name), case let argumentsNames = node.arguments.compactMap(\.label?.text), - argumentsNames == ["red", "green", "blue", "alpha"] || argumentsNames == ["white", "alpha"] else { - return false + argumentsNames == ["red", "green", "blue", "alpha"] || argumentsNames == ["white", "alpha"] else { + return false } return node.arguments.allSatisfy(\.expression.canBeExpressedAsColorLiteralParams) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index 784a4e3456..c8702ce80c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -2,8 +2,7 @@ import SwiftSyntax @SwiftSyntaxRule(optIn: true) struct OneDeclarationPerFileRule: Rule { - var configuration = SeverityConfiguration(.warning) - + var configuration = OneDeclarationPerFileConfiguration() static let description = RuleDescription( identifier: "one_declaration_per_file", name: "One Declaration per File", @@ -22,6 +21,18 @@ struct OneDeclarationPerFileRule: Rule { struct N {} } """), + Example(""" + enum Foo { + } + struct Bar { + } + """, + configuration: ["ignored_types": ["enum", "struct"]]), + Example(""" + struct Foo {} + struct Bar {} + """, + configuration: ["ignored_types": ["struct"]]), ], triggeringExamples: [ Example(""" @@ -36,15 +47,26 @@ struct OneDeclarationPerFileRule: Rule { struct Foo {} ↓struct Bar {} """), + Example(""" + struct Foo {} + ↓enum Bar {} + """, + configuration: ["ignored_types": ["protocol"]]), ] ) } private extension OneDeclarationPerFileRule { final class Visitor: ViolationsSyntaxVisitor { + private let allowedTypes: Set private var declarationVisited = false override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + override init(configuration: OneDeclarationPerFileConfiguration, file: SwiftLintFile) { + allowedTypes = Set(configuration.allowedTypes.map(\.rawValue)) + super.init(configuration: configuration, file: file) + } + override func visitPost(_ node: ActorDeclSyntax) { appendViolationIfNeeded(node: node.actorKeyword) } @@ -66,10 +88,10 @@ private extension OneDeclarationPerFileRule { } func appendViolationIfNeeded(node: TokenSyntax) { - if declarationVisited { + defer { declarationVisited = true } + if declarationVisited, !allowedTypes.contains(node.text) { violations.append(node.positionAfterSkippingLeadingTrivia) } - declarationVisited = true } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift new file mode 100644 index 0000000000..959e76b34b --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift @@ -0,0 +1,211 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule(foldExpressions: true, explicitRewriter: true, optIn: true) +struct PreferConditionListRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "prefer_condition_list", + name: "Prefer Condition List", + description: "Prefer a condition list over chaining conditions with '&&'", + rationale: """ + Instead of chaining conditions with `&&`, use a condition list to separate conditions with commas, that is, + use + + ``` + if a, b {} + ``` + + instead of + + ``` + if a && b {} + ``` + + Using a condition list improves readability and makes it easier to add or remove conditions in the future. + It also allows for better formatting and alignment of conditions. All in all, it's the idiomatic way to + write conditions in Swift. + + Since function calls with trailing closures trigger a warning in the Swift compiler when used in + conditions, this rule makes sure to wrap such expressions in parentheses when transforming them to + condition list elements. The scope of the parentheses is limited to the function call itself. + """, + kind: .idiomatic, + nonTriggeringExamples: [ + Example("if a, b {}"), + Example("guard a || b && c {}"), + Example("if a && b || c {}"), + Example("let result = a && b"), + Example("repeat {} while a && b"), + Example("if (f {}) {}"), + Example("if f {} {}"), + ], + triggeringExamples: [ + Example("if a ↓&& b {}"), + Example("if a ↓&& b ↓&& c {}"), + Example("while a ↓&& b {}"), + Example("guard a ↓&& b {}"), + Example("guard (a || b) ↓&& c {}"), + Example("if a ↓&& (b && c) {}"), + Example("guard a ↓&& b ↓&& c else {}"), + Example("if (a ↓&& b) {}"), + Example("if (a ↓&& f {}) {}"), + ], + corrections: [ + Example("if a && b {}"): + Example("if a, b {}"), + Example(""" + if a && + b {} + """): Example(""" + if a, + b {} + """), + Example("guard a && b && c else {}"): + Example("guard a, b, c else {}"), + Example("while a && b {}"): + Example("while a, b {}"), + Example("if a && b || c {}"): + Example("if a && b || c {}"), + Example("if (a && b) {}"): + Example("if a, b {}"), + Example("if a && (b && c) {}"): + Example("if a, b, c {}"), + Example("if (a && b) && c {}"): + Example("if a, b, c {}"), + Example("if (a && b), c {}"): + Example("if a, b, c {}"), + Example("guard (a || b) ↓&& c {}"): + Example("guard a || b, c {}"), + Example("if a && (b || c) {}"): + Example("if a, b || c {}"), + Example("if (a ↓&& f {}) {}"): + Example("if a, (f {}) {}"), + Example("if a ↓&& (b || f {}) {}"): + Example("if a, b || (f {}) {}"), + Example("if a ↓&& !f {} {}"): + Example("if a, !(f {}) {}"), + ] + ) +} + +private extension PreferConditionListRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ConditionElementSyntax) { + if case let .expression(expr) = node.condition { + collectViolations(for: expr) + } + } + + private func collectViolations(for expr: ExprSyntax) { + if let opExpr = expr.unwrap.as(InfixOperatorExprSyntax.self), + let opToken = opExpr.operator.as(BinaryOperatorExprSyntax.self)?.operator, + opToken.text == "&&" { + violations.append(opToken.positionAfterSkippingLeadingTrivia) + collectViolations(for: opExpr.leftOperand) // Expressions are left-recursive. + } + } + } + + private final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: ConditionElementListSyntax) -> ConditionElementListSyntax { + var elements = Array(node) + var modifiedIndices = Set() + var index = 0 + + while index < elements.count { + let element = elements[index] + guard case let .expression(expr) = element.condition else { + index += 1 + continue + } + if let opExpr = expr.as(InfixOperatorExprSyntax.self), + let opToken = opExpr.operator.as(BinaryOperatorExprSyntax.self)?.operator, + opToken.text == "&&" { + numberOfCorrections += 1 + + elements[index] = ConditionElementSyntax( + condition: .expression(opExpr.leftOperand.with(\.trailingTrivia, [])), + trailingComma: .commaToken(), + trailingTrivia: opToken.trailingTrivia + ) + modifiedIndices.insert(index) + + elements.insert( + ConditionElementSyntax( + condition: .expression(opExpr.rightOperand.with(\.trailingTrivia, [])), + trailingComma: index == elements.count - 1 ? nil : .commaToken(), + trailingTrivia: .space + ), + at: index + 1 + ) + modifiedIndices.insert(index + 1) + // Don't increment the index to re-evaluate `elements[index]`. + } else if expr.is(TupleExprSyntax.self) { + // Unwrap parenthesized expression and repeat the loop for the inner expression (i.e. without + // incrementing the index). + let unwrappedExpr = expr.unwrap + elements[index] = element.with(\.condition, .expression(unwrappedExpr)) + if unwrappedExpr != expr { + modifiedIndices.insert(index) + } + } else { + index += 1 + } + } + for (index, element) in elements.enumerated() where modifiedIndices.contains(index) { + if case let .expression(expr) = element.condition { + // If the expression contains function calls with trailing closures, we need to wrap them in + // parentheses. That might not be exactly how the author created the expression, but it is + // necessary to ensure no compiler warning appears after the transformations. + elements[index] = element.with( + \.condition, + .expression(ParenthesizedTrailingClosureRewriter().visit(expr)) + .with(\.leadingTrivia, expr.leadingTrivia) + .with(\.trailingTrivia, expr.trailingTrivia) + ) + } + } + return super.visit(ConditionElementListSyntax(elements)) + } + } +} + +private extension ExprSyntax { + var unwrap: ExprSyntax { + `as`(TupleExprSyntax.self)?.elements.onlyElement?.expression + .with(\.leadingTrivia, leadingTrivia) + .with(\.trailingTrivia, trailingTrivia) + ?? self + } +} + +private final class ParenthesizedTrailingClosureRewriter: SyntaxRewriter { + override func visitAny(_ node: Syntax) -> Syntax? { + if let opToken = node.as(InfixOperatorExprSyntax.self)?.operator.as(BinaryOperatorExprSyntax.self)?.operator, + ["&&", "||"].contains(opToken.text) { + nil + } else if let opToken = node.as(PrefixOperatorExprSyntax.self)?.operator, + ["!"].contains(opToken.text) { + nil + } else if node.is(FunctionCallExprSyntax.self) { + nil + } else { + node + } + } + + override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { + if node.trailingClosure != nil || node.additionalTrailingClosures.isNotEmpty { + return ExprSyntax(TupleExprSyntax( + elements: LabeledExprListSyntax([ + LabeledExprSyntax(label: nil, expression: node.with(\.trailingTrivia, [])) + ]) + )) + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + } + return super.visit(node) + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift index 6263405e0b..ed18f588c8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift @@ -63,7 +63,7 @@ private extension Syntax { return true } if let variableDecl = self.as(VariableDeclSyntax.self), - variableDecl.bindings.allSatisfy({ $0.accessorBlock == nil }) { + variableDecl.bindings.allSatisfy({ $0.accessorBlock == nil }) { return true } return false @@ -93,12 +93,12 @@ private extension AttributeListSyntax { return nil } if parent?.isFunctionOrStoredProperty == true, - let parentClassDecl = parent?.parent?.parent?.parent?.parent?.as(ClassDeclSyntax.self), - parentClassDecl.attributes.contains(attributeNamed: "objcMembers") { + let parentClassDecl = parent?.parent?.parent?.parent?.parent?.as(ClassDeclSyntax.self), + parentClassDecl.attributes.contains(attributeNamed: "objcMembers") { return parent?.functionOrVariableModifiers?.containsPrivateOrFileprivate() == true ? nil : objcAttribute } if let parentExtensionDecl = parent?.parent?.parent?.parent?.parent?.as(ExtensionDeclSyntax.self), - parentExtensionDecl.attributes.objCAttribute != nil { + parentExtensionDecl.attributes.objCAttribute != nil { return objcAttribute } return nil @@ -111,7 +111,7 @@ extension RedundantObjcAttributeRule { let nsCharSet = CharacterSet.whitespacesAndNewlines.bridge() let nsContent = file.contents.bridge() while nsCharSet - .characterIsMember(nsContent.character(at: violationRange.upperBound + whitespaceAndNewlineOffset)) { + .characterIsMember(nsContent.character(at: violationRange.upperBound + whitespaceAndNewlineOffset)) { whitespaceAndNewlineOffset += 1 } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift index dd91da179b..8ed486f525 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift @@ -73,7 +73,7 @@ struct RedundantVoidReturnRule: Rule { private extension RedundantVoidReturnRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: ReturnClauseSyntax) { - if !configuration.includeClosures && node.parent?.is(ClosureSignatureSyntax.self) == true { + if !configuration.includeClosures, node.parent?.is(ClosureSignatureSyntax.self) == true { return } @@ -116,7 +116,7 @@ private extension ReturnClauseSyntax { return false } if let simpleReturnType = type.as(IdentifierTypeSyntax.self) { - return simpleReturnType.typeName == "Void" + return simpleReturnType.typeName == "Void" } if let tupleReturnType = type.as(TupleTypeSyntax.self) { return tupleReturnType.elements.isEmpty diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift index 7237a45514..13f605fda4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift @@ -90,7 +90,7 @@ private extension TypeNameRule { ) } if let caseCheckSeverity = nameConfiguration.validatesStartWithLowercase.severity, - name.first?.isLowercase == true { + name.first?.isLowercase == true { return ReasonedRuleViolation( position: identifier.positionAfterSkippingLeadingTrivia, reason: "Type name '\(name)' should start with an uppercase character", diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift index df9786f33d..1be10e9f3f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift @@ -50,9 +50,9 @@ struct UnneededBreakInSwitchRule: Rule { ], corrections: [ embedInSwitch("something()\n ↓break") - : embedInSwitch("something()"), + : embedInSwitch("something()"), embedInSwitch("something()\n ↓break // line comment") - : embedInSwitch("something()\n // line comment"), + : embedInSwitch("something()\n // line comment"), embedInSwitch(""" something() ↓break @@ -60,14 +60,14 @@ struct UnneededBreakInSwitchRule: Rule { block comment */ """) - : embedInSwitch(""" + : embedInSwitch(""" something() /* block comment */ """), embedInSwitch("something()\n ↓break /// doc line comment") - : embedInSwitch("something()\n /// doc line comment"), + : embedInSwitch("something()\n /// doc line comment"), embedInSwitch(""" something() ↓break @@ -75,16 +75,16 @@ struct UnneededBreakInSwitchRule: Rule { /// doc block comment /// """) - : embedInSwitch(""" + : embedInSwitch(""" something() /// /// doc block comment /// """), embedInSwitch("something()\n ↓break", case: "default") - : embedInSwitch("something()", 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/UnusedEnumeratedRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift index da2c628ba9..60e6b62956 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift @@ -243,9 +243,9 @@ private extension FunctionCallExprSyntax { var enumeratedPosition: AbsolutePosition? { if let memberAccess = calledExpression.as(MemberAccessExprSyntax.self), - memberAccess.base != nil, - memberAccess.declName.baseName.text == "enumerated", - hasNoArguments { + memberAccess.base != nil, + memberAccess.declName.baseName.text == "enumerated", + hasNoArguments { return memberAccess.declName.positionAfterSkippingLeadingTrivia } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift index 8d6cc648c5..27f7c3ad57 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift @@ -11,7 +11,6 @@ struct VoidFunctionInTernaryConditionRule: Rule { kind: .idiomatic, minSwiftVersion: .fiveDotOne, nonTriggeringExamples: [ - Example("let result = success ? foo() : bar()"), Example(""" if success { askQuestion() @@ -60,6 +59,14 @@ struct VoidFunctionInTernaryConditionRule: Rule { subscript(index: Int) -> Int { index == 0 ? defaultValue() : compute(index) """), + Example(""" + var a = b ? c() : d() + a += b ? c() : d() + a -= b ? c() : d() + a *= b ? c() : d() + a &<<= b ? c() : d() + a &-= b ? c() : d() + """), ], triggeringExamples: [ Example("success ↓? askQuestion() : exit()"), @@ -144,18 +151,43 @@ private extension VoidFunctionInTernaryConditionRule { private extension ExprListSyntax { var containsAssignment: Bool { - children(viewMode: .sourceAccurate).contains(where: { $0.is(AssignmentExprSyntax.self) }) + children(viewMode: .sourceAccurate).contains { + if let binOp = $0.as(BinaryOperatorExprSyntax.self) { + // https://developer.apple.com/documentation/swift/operator-declarations + return [ + "*=", + "/=", + "%=", + "+=", + "-=", + "<<=", + ">>=", + "&=", + "|=", + "^=", + "&*=", + "&+=", + "&-=", + "&<<=", + "&>>=", + ".&=", + ".|=", + ".^=", + ].contains(binOp.operator.text) + } + return $0.is(AssignmentExprSyntax.self) + } } } private extension CodeBlockItemSyntax { var isImplicitReturn: Bool { - isClosureImplictReturn || isFunctionImplicitReturn || + isClosureImplicitReturn || isFunctionImplicitReturn || isVariableImplicitReturn || isSubscriptImplicitReturn || - isAcessorImplicitReturn + isAccessorImplicitReturn } - var isClosureImplictReturn: Bool { + var isClosureImplicitReturn: Bool { guard let parent = parent?.as(CodeBlockItemListSyntax.self), let grandparent = parent.parent else { return false @@ -191,7 +223,7 @@ private extension CodeBlockItemSyntax { return parent.children(viewMode: .sourceAccurate).count == 1 && subscriptDecl.allowsImplicitReturns } - var isAcessorImplicitReturn: Bool { + var isAccessorImplicitReturn: Bool { guard let parent = parent?.as(CodeBlockItemListSyntax.self), parent.parent?.parent?.as(AccessorDeclSyntax.self) != nil else { return false @@ -202,9 +234,9 @@ private extension CodeBlockItemSyntax { } private extension FunctionSignatureSyntax { - var allowsImplicitReturns: Bool { - returnClause?.allowsImplicitReturns ?? false - } + var allowsImplicitReturns: Bool { + returnClause?.allowsImplicitReturns ?? false + } } private extension SubscriptDeclSyntax { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift index ac41e13867..2a4903fe9e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift @@ -25,7 +25,7 @@ private extension XCTSpecificMatcherRule { reason: "Prefer the specific matcher '\(suggestion)' instead" )) } else if configuration.matchers.contains(.oneArgumentAsserts), - let suggestion = OneArgXCTAssert.violations(in: node) { + let suggestion = OneArgXCTAssert.violations(in: node) { violations.append(ReasonedRuleViolation( position: node.positionAfterSkippingLeadingTrivia, reason: "Prefer the specific matcher '\(suggestion)' instead" diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift index 57cf76b26e..281059ba66 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift @@ -1,6 +1,7 @@ -import SourceKittenFramework +import SwiftSyntax -struct AccessibilityLabelForImageRule: ASTRule, OptInRule { +@SwiftSyntaxRule +struct AccessibilityLabelForImageRule: Rule, OptInRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -24,121 +25,259 @@ struct AccessibilityLabelForImageRule: ASTRule, OptInRule { nonTriggeringExamples: AccessibilityLabelForImageRuleExamples.nonTriggeringExamples, triggeringExamples: AccessibilityLabelForImageRuleExamples.triggeringExamples ) +} - // MARK: AST Rule +private extension AccessibilityLabelForImageRule { + final class Visitor: ViolationsSyntaxVisitor { + private var isInViewStruct = false - func validate(file: SwiftLintFile, - kind: SwiftDeclarationKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - // Only proceed to check View structs. - guard kind == .struct, - dictionary.inheritedTypes.contains("View"), - dictionary.substructure.isNotEmpty else { - return [] + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + isInViewStruct = node.isViewStruct + return .visitChildren } - return findImageViolations(file: file, substructure: dictionary.substructure) - } + override func visitPost(_: StructDeclSyntax) { + isInViewStruct = false + } - /// Recursively check a file for image violations, and return all such violations. - private func findImageViolations(file: SwiftLintFile, substructure: [SourceKittenDictionary]) -> [StyleViolation] { - var violations = [StyleViolation]() - for dictionary in substructure { - guard let offset: ByteCount = dictionary.offset else { - continue - } + override func visitPost(_ node: FunctionCallExprSyntax) { + // Only check Image calls within View structs + guard isInViewStruct else { return } - // If it's image, and does not hide from accessibility or provide a label, it's a violation. - if dictionary.isImage { - if dictionary.isDecorativeOrLabeledImage || - dictionary.hasAccessibilityHiddenModifier(in: file) || - dictionary.hasAccessibilityLabelModifier(in: file) { - continue - } + // Only process direct Image calls + guard node.isDirectImageCall else { return } - violations.append( - StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: offset)) + // Use centralized exemption logic + if !AccessibilityDeterminator.isExempt(node) { + let violation = ReasonedRuleViolation( + position: node.positionAfterSkippingLeadingTrivia, + reason: """ + Images that provide context should have an accessibility label or should be \ + explicitly hidden from accessibility + """, + severity: configuration.severity ) + violations.append(violation) } + } + } +} - // If dictionary did not represent an Image, recursively check substructure, - // unless it's a container that hides its children from accessibility or is labeled. - else if dictionary.substructure.isNotEmpty { - if dictionary.hasAccessibilityHiddenModifier(in: file) || - dictionary.hasAccessibilityElementChildrenIgnoreModifier(in: file) || - dictionary.hasAccessibilityLabelModifier(in: file) { - continue - } +// MARK: Accessibility Exemption Logic - violations.append(contentsOf: findImageViolations(file: file, substructure: dictionary.substructure)) - } +private struct AccessibilityDeterminator { + /// Maximum depth to search up the syntax tree for exemptions + static let maxSearchDepth = 20 + + /// Determines if an Image call is exempt from requiring accessibility treatment + static func isExempt(_ imageCall: FunctionCallExprSyntax) -> Bool { + // 1. Check for decorative or labeled initializers (e.g., Image(decorative:)) + if imageCall.isDecorativeOrLabeledImage { + return true } - return violations + // 2. Check the parent hierarchy for exemptions + return imageCall.isExemptedByAncestors() } } -// MARK: SourceKittenDictionary extensions +// MARK: SwiftSyntax extensions -private extension SourceKittenDictionary { - /// Whether or not the dictionary represents a SwiftUI Image. - /// Currently only accounts for SwiftUI image literals and not instance variables. - var isImage: Bool { - // Image literals will be reported as calls to the initializer. - guard expressionKind == .call else { - return false +private extension StructDeclSyntax { + /// Whether this struct conforms to View protocol + var isViewStruct: Bool { + guard let inheritanceClause else { return false } + + return inheritanceClause.inheritedTypes.contains { inheritedType in + inheritedType.type.as(IdentifierTypeSyntax.self)?.name.text == "View" } + } +} - if name == "Image" || name == "SwiftUI.Image" { - return true +private extension FunctionCallExprSyntax { + /// Check if this is a direct Image call (not a modifier) + var isDirectImageCall: Bool { + // Check for direct Image call + if let identifierExpr = calledExpression.as(DeclReferenceExprSyntax.self) { + return identifierExpr.baseName.text == "Image" + } + + // Check for SwiftUI.Image call + if let memberAccessExpr = calledExpression.as(MemberAccessExprSyntax.self), + let baseIdentifier = memberAccessExpr.base?.as(DeclReferenceExprSyntax.self) { + return baseIdentifier.baseName.text == "SwiftUI" && + memberAccessExpr.declName.baseName.text == "Image" } - // Recursively check substructure. - // SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like: - // Image(decorative: "myImage").resizable().frame - // --> Image(decorative: "myImage").resizable - // --> Image - return substructure.contains(where: \.isImage) + return false } - /// Whether or not the dictionary represents a SwiftUI Image using the `Image(decorative:)` constructor (hides - /// from a11y), or the `Image(_:label:)` constructors (which provide labels). + /// Whether this is Image(decorative:) or Image(_:label:) var isDecorativeOrLabeledImage: Bool { - guard isImage else { + arguments.contains { arg in + let label = arg.label?.text + return label == "decorative" || label == "label" + } + } + + /// Walks up the syntax tree to find accessibility exemptions with depth limit + func isExemptedByAncestors() -> Bool { + var currentNode: Syntax? = Syntax(self) + var depth = 0 + + while let node = currentNode, depth < AccessibilityDeterminator.maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + // Check function calls for exempting patterns + guard let funcCall = node.as(FunctionCallExprSyntax.self) else { continue } + + // Check for accessibility modifiers + if let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) { + let modifierName = memberAccess.declName.baseName.text + + if funcCall.isDirectAccessibilityModifier(modifierName) || + funcCall.isContainerExemptingModifier(modifierName) { + return true + } + } + + // Check for inherently exempting containers + if funcCall.isInherentlyExemptingContainer() { + return true + } + + // Check container views with accessibility modifiers + if funcCall.isContainerView(), funcCall.hasAccessibilityModifiersInChain() { + return true + } + + // Stop early at statement boundaries for performance + if node.parent?.is(StmtSyntax.self) == true { + break + } + } + + return false + } + + /// Check if this function call represents a container view + func isContainerView() -> Bool { + guard let identifierExpr = calledExpression.as(DeclReferenceExprSyntax.self) else { return false } + let containerNames: Set = ["VStack", "HStack", "ZStack", "Group", "LazyVStack", "LazyHStack"] + return containerNames.contains(identifierExpr.baseName.text) + } + + /// Check if this container has accessibility modifiers in its modifier chain + func hasAccessibilityModifiersInChain() -> Bool { + var currentNode: Syntax? = Syntax(self) + var depth = 0 + + while let node = currentNode, depth < AccessibilityDeterminator.maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + guard let funcCall = node.as(FunctionCallExprSyntax.self), + let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) else { continue } + + let modifierName = memberAccess.declName.baseName.text + + if funcCall.isDirectAccessibilityModifier(modifierName) || + funcCall.isContainerExemptingModifier(modifierName) { + return true + } + + // Stop at statement boundaries + if node.parent?.is(StmtSyntax.self) == true { + break + } + } + + return false + } + + /// Checks for modifiers like .accessibilityLabel(...) or .accessibilityHidden(true) + func isDirectAccessibilityModifier(_ name: String) -> Bool { + switch name { + case "accessibilityHidden": + return arguments.first?.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + case "accessibilityLabel", "accessibilityValue", "accessibilityHint": + return true + case "accessibility": + return arguments.contains { arg in + guard let label = arg.label?.text else { return false } + if ["label", "value", "hint"].contains(label) { return true } + if label == "hidden" { + return arg.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + } + return false + } + default: return false } + } + + /// Checks for modifiers that make a container exempt its children from individual accessibility + func isContainerExemptingModifier(_ name: String) -> Bool { + guard name == "accessibilityElement" else { return false } - // Check for Image(decorative:) or Image(_:label:) constructor. - if expressionKind == .call && - enclosedArguments.contains(where: { ["decorative", "label"].contains($0.name) }) { + // Check for .accessibilityElement(children: .ignore) which exempts children + if let childrenArg = arguments.first(where: { $0.label?.text == "children" }) { + let childrenValue = childrenArg.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text + return childrenValue == "ignore" // Only .ignore exempts individual children + } + + // .accessibilityElement() with no arguments defaults to behavior that exempts children + return arguments.isEmpty + } + + /// Checks for container views that provide their own accessibility context + func isInherentlyExemptingContainer() -> Bool { + guard let identifier = calledExpression.as(DeclReferenceExprSyntax.self) else { return false } + let containerName = identifier.baseName.text + + // NavigationLink automatically exempts children + if containerName == "NavigationLink" { return true } - // Recursively check substructure. - // SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like: - // Image(decorative: "myImage").resizable().frame - // --> Image(decorative: "myImage").resizable - // --> Image - return substructure.contains(where: \.isDecorativeOrLabeledImage) + // Button exempts children if it has accessibility treatment + if containerName == "Button" { + return hasDirectAccessibilityTreatment() + } + + return false } - /// Whether or not the dictionary represents a SwiftUI View with an `accesibilityLabel(_:)` - /// or `accessibility(label:)` modifier. - func hasAccessibilityLabelModifier(in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityLabel", - arguments: [.init(name: "", values: [])] - ), - SwiftUIModifier( - name: "accessibility", - arguments: [.init(name: "label", values: [])] - ), - ], - in: file - ) + /// Check if this container has direct accessibility treatment + private func hasDirectAccessibilityTreatment() -> Bool { + var currentNode: Syntax? = Syntax(self) + var depth = 0 + + while let node = currentNode, depth < AccessibilityDeterminator.maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + guard let funcCall = node.as(FunctionCallExprSyntax.self), + let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) else { continue } + + let modifierName = memberAccess.declName.baseName.text + if funcCall.isDirectAccessibilityModifier(modifierName) { + return true + } + + // Stop at statement boundaries + if node.parent?.is(StmtSyntax.self) == true { + break + } + } + + return false } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift index ede1e90e84..24355db396 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift @@ -1,3 +1,6 @@ +// swiftlint:disable file_length + +// swiftlint:disable:next type_body_length internal struct AccessibilityLabelForImageRuleExamples { static let nonTriggeringExamples = [ Example(""" @@ -149,6 +152,112 @@ internal struct AccessibilityLabelForImageRuleExamples { } } """), + // MARK: - SwiftSyntax Migration Regression Tests + // These examples would have been false positives with the SourceKit implementation + // but now correctly pass with the SwiftSyntax implementation + Example(""" + struct MyView: View { + var body: some View { + NavigationLink("Go to Details") { + DetailView() + } label: { + HStack { + Image(systemName: "arrow.right") + Text("Navigate Here") + } + } + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + Button("Save Changes") { + saveAction() + } label: { + Label("Save", systemImage: "square.and.arrow.down") + } + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + Button(action: performAction) { + HStack { + Image(uiImage: UIImage(systemName: "star") ?? UIImage()) + Text("Favorite") + } + } + .accessibilityLabel("Add to Favorites") + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + VStack { + Image(systemName: "wifi") + Image("network-icon") + Text("Network Status") + }.accessibilityElement(children: .ignore) + .accessibilityLabel("Connected to WiFi") + } + } + """), + Example(""" + struct MyView: View { + let statusImage: UIImage + var body: some View { + HStack { + Image(uiImage: statusImage) + .foregroundColor(.green) + Text("System Status") + }.accessibilityElement(children: .ignore) + .accessibilityLabel("System is operational") + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + NavigationLink(destination: SettingsView()) { + HStack { + Image(nsImage: NSImage(named: "gear") ?? NSImage()) + Text("Preferences") + Spacer() + Image(systemName: "chevron.right") + } + } + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + Button { + toggleState() + } label: { + Image(systemName: isEnabled ? "eye" : "eye.slash") + .foregroundColor(isEnabled ? .blue : .gray) + } + .accessibilityLabel(isEnabled ? "Hide content" : "Show content") + } + } + """), + Example(""" + struct CustomCard: View { + var body: some View { + VStack { + Image("card-background") + Image(systemName: "checkmark.circle") + Text("Task Complete") + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Task completed successfully") + } + } + """), ] static let triggeringExamples = [ @@ -265,5 +374,66 @@ internal struct AccessibilityLabelForImageRuleExamples { } } """), + // MARK: - SwiftSyntax Migration Detection Improvements + // These violations would have been missed by the SourceKit implementation + // but are now correctly detected by SwiftSyntax + Example(""" + struct StatusView: View { + let statusIcon: UIImage + var body: some View { + HStack { + ↓Image(uiImage: statusIcon) + .foregroundColor(.green) + Text("Status") + } + } + } + """), + Example(""" + struct PreferencesView: View { + var body: some View { + VStack { + ↓Image(nsImage: NSImage(named: "gear") ?? NSImage()) + .resizable() + .frame(width: 24, height: 24) + Text("Settings") + } + } + } + """), + Example(""" + struct FaviconView: View { + let favicon: UIImage? + var body: some View { + ↓Image(uiImage: favicon ?? UIImage()) + .aspectRatio(contentMode: .fit) + .frame(width: 16, height: 16) + } + } + """), + Example(""" + struct IconGrid: View { + var body: some View { + HStack { + ↓Image(uiImage: loadedImage) + .resizable() + ↓Image(systemName: "star.fill") + .foregroundColor(.yellow) + }.accessibilityElement(children: .combine) + } + } + """), + Example(""" + struct CardView: View { + var body: some View { + VStack { + ↓Image(uiImage: backgroundImage) + .resizable() + .aspectRatio(contentMode: .fill) + Text("Card Content") + }.accessibilityElement(children: .contain) + } + } + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift index 006783a2d3..abe85e3d8e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift @@ -1,6 +1,7 @@ -import SourceKittenFramework +import SwiftSyntax -struct AccessibilityTraitForButtonRule: ASTRule, OptInRule { +@SwiftSyntaxRule(optIn: true) +struct AccessibilityTraitForButtonRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -25,119 +26,251 @@ struct AccessibilityTraitForButtonRule: ASTRule, OptInRule { nonTriggeringExamples: AccessibilityTraitForButtonRuleExamples.nonTriggeringExamples, triggeringExamples: AccessibilityTraitForButtonRuleExamples.triggeringExamples ) +} - // MARK: AST Rule +private extension AccessibilityTraitForButtonRule { + final class Visitor: ViolationsSyntaxVisitor { + private var isInViewStruct = false - func validate(file: SwiftLintFile, - kind: SwiftDeclarationKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - // Only proceed to check View structs. - guard kind == .struct, - dictionary.inheritedTypes.contains("View"), - dictionary.substructure.isNotEmpty else { - return [] + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + isInViewStruct = node.isViewStruct + return .visitChildren } - return findButtonTraitViolations(file: file, substructure: dictionary.substructure) - } + override func visitPost(_: StructDeclSyntax) { + isInViewStruct = false + } - /// Recursively check a file for image violations, and return all such violations. - private func findButtonTraitViolations( - file: SwiftLintFile, - substructure: [SourceKittenDictionary] - ) -> [StyleViolation] { - var violations = [StyleViolation]() - for dictionary in substructure { - guard let offset: ByteCount = dictionary.offset else { - continue + override func visitPost(_ node: FunctionCallExprSyntax) { + guard isInViewStruct, node.isSingleTapGestureModifier() else { + return } - // If it has a tap gesture and does not have a button or link trait, it's a violation. - // Also allowing ones that are hidden from accessibility, though that's not recommended. - if dictionary.hasOnSingleTapModifier(in: file) { - if dictionary.hasAccessibilityTrait(".isButton", in: file) || - dictionary.hasAccessibilityTrait(".isLink", in: file) || - dictionary.hasAccessibilityHiddenModifier(in: file) { - continue - } - + // The 'node' is the tap gesture modifier itself. + // Check if this node, any preceding modifiers in the chain, or an encapsulating Button/Link provide + // exemption. + if !AccessibilityButtonTraitDeterminator.isExempt(tapGestureNode: node) { violations.append( - StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: offset)) + ReasonedRuleViolation( + // Position of .onTapGesture etc. + position: node.calledExpression.positionAfterSkippingLeadingTrivia, + reason: AccessibilityTraitForButtonRule.description.description, + severity: configuration.severity + ) ) } + } + } +} + +private struct AccessibilityButtonTraitDeterminator { + static let maxSearchDepth = 20 // Limit for traversing up the syntax tree + + static func isExempt(tapGestureNode: FunctionCallExprSyntax) -> Bool { + // 1. Check if accessibility traits are present anywhere in the same modifier chain + if hasAccessibilityTraitsInChain(tapGestureNode: tapGestureNode) { + return true + } + + // 2. Check if the view (to which the gesture is applied) is part of an inherently exempting container + return isInsideInherentlyExemptingContainer(startingFrom: tapGestureNode) + } + + private static func hasAccessibilityTraitsInChain(tapGestureNode: FunctionCallExprSyntax) -> Bool { + // Check both directions: before the tap gesture (backwards in chain) and after (ancestors in tree) + + // 1. Check backwards in the modifier chain (modifiers applied before the tap gesture) + if hasAccessibilityTraitsBackwards(from: tapGestureNode) { + return true + } + + // 2. Check forwards by looking at parent nodes (modifiers applied after the tap gesture) + return hasAccessibilityTraitsForwards(from: tapGestureNode) + } + + private static func hasAccessibilityTraitsBackwards(from tapGestureNode: FunctionCallExprSyntax) -> Bool { + var current: ExprSyntax? = ExprSyntax(tapGestureNode) + var depth = 0 + + while let currentExpr = current, depth < maxSearchDepth { + defer { depth += 1 } + + if let funcCall = currentExpr.as(FunctionCallExprSyntax.self) { + // Check if this modifier provides accessibility traits + if funcCall.providesButtonOrLinkTrait() || funcCall.isAccessibilityHiddenTrue() { + return true + } + + // Move to the previous modifier in the chain (the base of the member access) + if let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) { + current = memberAccess.base + } else { + // Reached the end of the chain (e.g., Text(...)) + break + } + } else { + break + } + } + + return false + } + + private static func hasAccessibilityTraitsForwards(from tapGestureNode: FunctionCallExprSyntax) -> Bool { + // Look at parent nodes to see if accessibility traits are applied after the tap gesture + var currentNode: Syntax? = Syntax(tapGestureNode).parent + var depth = 0 + + while let node = currentNode, depth < maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } - // If dictionary did not represent a View with a tap gesture, recursively check substructure, - // unless it's a container that hides its children from accessibility. - else if dictionary.substructure.isNotEmpty { - if dictionary.hasAccessibilityHiddenModifier(in: file) || - dictionary.hasAccessibilityElementChildrenIgnoreModifier(in: file) { - continue + // Check if this parent node is a function call with accessibility traits + if let funcCall = node.as(FunctionCallExprSyntax.self) { + if funcCall.providesButtonOrLinkTrait() || funcCall.isAccessibilityHiddenTrue() { + return true } + } - violations.append( - contentsOf: findButtonTraitViolations(file: file, substructure: dictionary.substructure) - ) + // Stop at certain boundaries + if node.is(StmtSyntax.self) || node.is(StructDeclSyntax.self) { + break } } - return violations + return false + } + + private static func isInsideInherentlyExemptingContainer(startingFrom node: FunctionCallExprSyntax) -> Bool { + var currentNode: Syntax? = Syntax(node) + var depth = 0 + + while let currentSyntaxNode = currentNode, depth < maxSearchDepth { + defer { + currentNode = currentSyntaxNode.parent + depth += 1 + } + + if let funcCall = currentSyntaxNode.as(FunctionCallExprSyntax.self), + let identifier = funcCall.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text, + ["Button", "Link"].contains(identifier) { + return true + } + + // Stop if we reach a new View declaration or similar boundary + if currentSyntaxNode.is(StructDeclSyntax.self) || + currentSyntaxNode.is(ClassDeclSyntax.self) || + currentSyntaxNode.is(EnumDeclSyntax.self) { + break + } + } + return false + } + + /// Helper to check if an expression (part of a gesture modifier argument) is TapGesture(count: 1) or TapGesture() + fileprivate static func isSingleTapGestureInstance(expression: ExprSyntax) -> Bool { + var currentExpr: ExprSyntax? = expression + + // Traverse down if it's a chain of gesture modifiers like .onEnded to find the base gesture. + while let memberCall = currentExpr?.as(FunctionCallExprSyntax.self), // e.g. TapGesture().onEnded() + let memberAccess = memberCall.calledExpression.as(MemberAccessExprSyntax.self), + memberAccess.base != nil { // Ensure it's a chain like x.method() + currentExpr = memberAccess.base + } + + guard let gestureCall = currentExpr?.as(FunctionCallExprSyntax.self), + let gestureName = gestureCall.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text, + gestureName == "TapGesture" else { + return false + } + + // Check count argument: TapGesture() or TapGesture(count: 1) + if gestureCall.arguments.isEmpty { return true } // Default count is 1 + + if let countArg = gestureCall.arguments.first(where: { $0.label?.text == "count" }) { + return countArg.expression.as(IntegerLiteralExprSyntax.self)?.literal.text == "1" + } + + // If 'count' is not specified, it defaults to 1. + return !gestureCall.arguments.contains(where: { $0.label?.text == "count" }) } } -// MARK: SourceKittenDictionary extensions - -private extension SourceKittenDictionary { - /// Whether or not the dictionary represents a SwiftUI View with a tap gesture where the `count` argument is 1. - /// A single tap can be represented by an `onTapGesture` modifier with a count of 1 (default value is 1), - /// 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 { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "onTapGesture", - arguments: [.init(name: "count", required: false, values: ["1"])] - ), - SwiftUIModifier( - name: "gesture", - arguments: [ - .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) - ] - ), - SwiftUIModifier( - name: "simultaneousGesture", - arguments: [ - .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) - ] - ), - SwiftUIModifier( - name: "highPriorityGesture", - arguments: [ - .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) - ] - ), - ], - in: file - ) +private extension StructDeclSyntax { + var isViewStruct: Bool { + guard let inheritanceClause else { return false } + return inheritanceClause.inheritedTypes.contains { inheritedType in + inheritedType.type.as(IdentifierTypeSyntax.self)?.name.text == "View" + } } +} + +private extension FunctionCallExprSyntax { + func isSingleTapGestureModifier() -> Bool { + guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false } + let name = calledExpr.declName.baseName.text + + if name == "onTapGesture" { + if arguments.isEmpty { return true } // Default count is 1 + if let countArg = arguments.first(where: { $0.label?.text == "count" }) { + return countArg.expression.as(IntegerLiteralExprSyntax.self)?.literal.text == "1" + } + // If 'count' is not specified, it defaults to 1 (other args like 'perform' might be present) + return !arguments.contains(where: { $0.label?.text == "count" }) + } + + if ["gesture", "simultaneousGesture", "highPriorityGesture"].contains(name) { + guard let firstArgExpression = arguments.first?.expression else { return false } + return AccessibilityButtonTraitDeterminator.isSingleTapGestureInstance(expression: firstArgExpression) + } + return false + } + + func providesButtonOrLinkTrait() -> Bool { + guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false } + let name = calledExpr.declName.baseName.text + + if name == "accessibilityAddTraits" { + guard let firstArgExpr = arguments.first?.expression else { return false } + return Self.expressionContainsButtonOrLinkTrait(firstArgExpr) + } + + if name == "accessibility" { + guard let addTraitsArg = arguments.first(where: { $0.label?.text == "addTraits" }) else { return false } + return Self.expressionContainsButtonOrLinkTrait(addTraitsArg.expression) + } + + return false + } + + private static func expressionContainsButtonOrLinkTrait(_ expression: ExprSyntax) -> Bool { + if let memberAccess = expression.as(MemberAccessExprSyntax.self) { + let traitName = memberAccess.declName.baseName.text + return traitName == "isButton" || traitName == "isLink" + } + if let arrayExpr = expression.as(ArrayExprSyntax.self) { + return arrayExpr.elements.contains { element in + self.expressionContainsButtonOrLinkTrait(element.expression) + } + } + return false + } + + func isAccessibilityHiddenTrue() -> Bool { + guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false } + let name = calledExpr.declName.baseName.text + + if name == "accessibilityHidden" { + return arguments.first?.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + } + + if name == "accessibility" { + guard let hiddenArg = arguments.first(where: { $0.label?.text == "hidden" }) else { return false } + return hiddenArg.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + } - /// 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 { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityAddTraits", - arguments: [.init(name: "", values: [trait], matchType: .substring)] - ), - SwiftUIModifier( - name: "accessibility", - arguments: [.init(name: "addTraits", values: [trait], matchType: .substring)] - ), - ], - in: file - ) + return false } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift index a461e9a0e1..28ac98b22d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift @@ -131,7 +131,7 @@ private extension ClosureSignatureSyntax { return list.onlyElement?.name.text } if let clause = parameterClause?.as(ClosureParameterClauseSyntax.self), clause.parameters.count == 1, - clause.parameters.first?.secondName == nil { + clause.parameters.first?.secondName == nil { return clause.parameters.first?.firstName.text } return nil diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift index 0268d68813..579e2afc0f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift @@ -121,7 +121,7 @@ private extension AsyncWithoutAwaitRule { override func visitPost(_ node: VariableDeclSyntax) { if node.bindingSpecifier.tokenKind == .keyword(.let), - node.modifiers.contains(keyword: .async) { + node.modifiers.contains(keyword: .async) { functionScopes.modifyLast { $0.containsAwait = true } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift index 6df51d512a..ba6a1631bf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift @@ -61,7 +61,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { """), Example("// swiftlint:disable all"), ].skipWrappingInCommentTests().skipDisableCommandTests() - ) + ) func validate(file: SwiftLintFile) -> [StyleViolation] { var violations: [StyleViolation] = [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift index 32cb208480..e2dc375a36 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift @@ -45,6 +45,7 @@ struct ClassDelegateProtocolRule: Rule { Example("protocol FooDelegate where Self: Foo & BarDelegate & Bar {}"), Example("protocol FooDelegate where Self: AnyObject {}"), Example("protocol FooDelegate where Self: NSObjectProtocol {}"), + Example("protocol FooDelegate: Actor {}"), ], triggeringExamples: [ Example("↓protocol FooDelegate {}"), @@ -107,7 +108,8 @@ private extension ProtocolDeclSyntax { private extension TypeSyntax { func isObjectOrDelegate() -> Bool { if let typeName = `as`(IdentifierTypeSyntax.self)?.typeName { - return typeName == "AnyObject" || typeName == "NSObjectProtocol" || typeName.hasSuffix("Delegate") + let objectTypes = ["AnyObject", "NSObjectProtocol", "Actor"] + return objectTypes.contains(typeName) || typeName.hasSuffix("Delegate") } if let combined = `as`(CompositionTypeSyntax.self) { return combined.elements.contains { $0.type.isObjectOrDelegate() } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift index 0227dcf8fe..b74f089317 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift @@ -42,7 +42,7 @@ private extension CompilerProtocolInitRule { let argumentsNames = arguments.map(\.text) for compilerProtocol in ExpressibleByCompiler.allProtocols { guard compilerProtocol.initCallNames.contains(name), - compilerProtocol.match(arguments: argumentsNames) else { + compilerProtocol.match(arguments: argumentsNames) else { continue } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift index 45a18ed6a7..c392de8a49 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift @@ -105,12 +105,12 @@ private extension DeploymentTargetRule { violationType: AvailabilityType) -> String? { guard let platform = DeploymentTargetConfiguration.Platform(rawValue: platform.text), let minVersion = platformToConfiguredMinVersion[platform.rawValue] else { - return nil + return nil } guard let version = try? Version(platform: platform, value: versionString), - version <= minVersion else { - return nil + version <= minVersion else { + return nil } return """ diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift index 60883d23ad..0ac6e82a4b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift @@ -23,7 +23,7 @@ private extension EmptyXCTestMethodRule { } override func visitPost(_ node: FunctionDeclSyntax) { - if (node.modifiers.contains(keyword: .override) || node.isTestMethod) && node.hasEmptyBody { + if node.modifiers.contains(keyword: .override) || node.isTestMethod, node.hasEmptyBody { violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift index c64c93c681..49cf8c43d3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift @@ -1,7 +1,8 @@ import Foundation -import SourceKittenFramework +import SwiftSyntax -struct ExpiringTodoRule: OptInRule { +@SwiftSyntaxRule(optIn: true) +struct ExpiringTodoRule: Rule { enum ExpiryViolationLevel { case approachingExpiry case expired @@ -10,11 +11,11 @@ struct ExpiringTodoRule: OptInRule { var reason: String { switch self { case .approachingExpiry: - return "TODO/FIXME is approaching its expiry and should be resolved soon" + "TODO/FIXME is approaching its expiry and should be resolved soon" case .expired: - return "TODO/FIXME has expired and must be resolved" + "TODO/FIXME has expired and must be resolved" case .badFormatting: - return "Expiring TODO/FIXME is incorrectly formatted" + "Expiring TODO/FIXME is incorrectly formatted" } } } @@ -45,73 +46,142 @@ struct ExpiringTodoRule: OptInRule { ) var configuration = ExpiringTodoConfiguration() +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - let regex = #""" - \b(?:TODO|FIXME)(?::|\b)(?:(?!\b(?:TODO|FIXME)(?::|\b)).)*?\# - \\#(configuration.dateDelimiters.opening)\# - (\d{1,4}\\#(configuration.dateSeparator)\d{1,4}\\#(configuration.dateSeparator)\d{1,4})\# - \\#(configuration.dateDelimiters.closing) - """# - - return file.matchesAndSyntaxKinds(matching: regex).compactMap { checkingResult, syntaxKinds in - guard - syntaxKinds.allSatisfy(\.isCommentLike), - checkingResult.numberOfRanges > 1, - case let range = checkingResult.range(at: 1), - let violationLevel = violationLevel(for: expiryDate(file: file, range: range)), - let severity = severity(for: violationLevel) else { - return nil +private extension ExpiringTodoRule { + final class Visitor: ViolationsSyntaxVisitor { + private lazy var regex: NSRegularExpression = { + let pattern = #""" + \b(?:TODO|FIXME)(?::|\b)(?:(?!\b(?:TODO|FIXME)(?::|\b)).)*?\# + \\#(configuration.dateDelimiters.opening)\# + (\d{1,4}\\#(configuration.dateSeparator)\d{1,4}\\#(configuration.dateSeparator)\d{1,4})\# + \\#(configuration.dateDelimiters.closing) + """# + return SwiftLintCore.regex(pattern) + }() + + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + // Process each comment individually + for token in node.tokens(viewMode: .sourceAccurate) { + processTrivia( + token.leadingTrivia, + baseOffset: token.position.utf8Offset + ) + processTrivia( + token.trailingTrivia, + baseOffset: token.endPositionBeforeTrailingTrivia.utf8Offset + ) } - return StyleViolation( - ruleDescription: Self.description, - severity: severity, - location: Location(file: file, characterOffset: range.location), - reason: violationLevel.reason - ) + return .skipChildren } - } - private func expiryDate(file: SwiftLintFile, range: NSRange) -> Date? { - let expiryDateString = file.contents.bridge() - .substring(with: range) - .trimmingCharacters(in: .whitespacesAndNewlines) + private func processTrivia(_ trivia: Trivia, baseOffset: Int) { + var triviaOffset = baseOffset - let formatter = DateFormatter() - formatter.calendar = .current - formatter.dateFormat = configuration.dateFormat + for (index, piece) in trivia.enumerated() { + defer { triviaOffset += piece.sourceLength.utf8Length } - return formatter.date(from: expiryDateString) - } + guard let commentText = piece.commentText else { continue } + + // Handle multiline comments by checking consecutive line comments + if piece.isLineComment { + var combinedText = commentText + let currentOffset = triviaOffset + + // Look ahead for consecutive line comments + let remainingTrivia = trivia.dropFirst(index + 1) - private func severity(for violationLevel: ExpiryViolationLevel) -> ViolationSeverity? { - switch violationLevel { - case .approachingExpiry: - return configuration.approachingExpirySeverity.severity - case .expired: - return configuration.expiredSeverity.severity - case .badFormatting: - return configuration.badFormattingSeverity.severity + for nextPiece in remainingTrivia { + if case .lineComment(let nextText) = nextPiece { + // Check if it's a continuation (starts with //) + if nextText.hasPrefix("//") { + combinedText += "\n" + nextText + } else { + break + } + } else if !nextPiece.isWhitespace { + break + } + } + + processComment(combinedText, offset: currentOffset) + } else { + processComment(commentText, offset: triviaOffset) + } + } + } + + private func processComment(_ commentText: String, offset: Int) { + let matches = regex.matches(in: commentText, options: [], range: commentText.fullNSRange) + let nsStringComment = commentText.bridge() + + for match in matches { + guard match.numberOfRanges > 1 else { continue } + + // Get the date capture group (second capture group, index 1) + let dateRange = match.range(at: 1) + guard dateRange.location != NSNotFound else { continue } + + let matchOffset = offset + dateRange.location + let matchPosition = AbsolutePosition(utf8Offset: matchOffset) + + let dateString = nsStringComment.substring(with: dateRange) + .trimmingCharacters(in: .whitespacesAndNewlines) + + if let violationLevel = getViolationLevel(for: parseDate(dateString: dateString)), + let severity = getSeverity(for: violationLevel) { + let violation = ReasonedRuleViolation( + position: matchPosition, + reason: violationLevel.reason, + severity: severity + ) + violations.append(violation) + } + } } - } - private func violationLevel(for expiryDate: Date?) -> ExpiryViolationLevel? { - guard let expiryDate else { - return .badFormatting + private func parseDate(dateString: String) -> Date? { + let formatter = DateFormatter() + formatter.calendar = .current + formatter.dateFormat = configuration.dateFormat + return formatter.date(from: dateString) } - guard expiryDate.isAfterToday else { - return .expired + + private func getSeverity(for violationLevel: ExpiryViolationLevel) -> ViolationSeverity? { + switch violationLevel { + case .approachingExpiry: + configuration.approachingExpirySeverity.severity + case .expired: + configuration.expiredSeverity.severity + case .badFormatting: + configuration.badFormattingSeverity.severity + } } - guard let approachingDate = Calendar.current.date( - byAdding: .day, - value: -configuration.approachingExpiryThreshold, - to: expiryDate) else { + + private func getViolationLevel(for expiryDate: Date?) -> ExpiryViolationLevel? { + guard let expiryDate else { + return .badFormatting + } + + guard expiryDate.isAfterToday else { + return .expired + } + + let approachingDate = Calendar.current.date( + byAdding: .day, + value: -configuration.approachingExpiryThreshold, + to: expiryDate + ) + + guard let approachingDate else { return nil + } + + return approachingDate.isAfterToday ? + nil : + .approachingExpiry } - return approachingDate.isAfterToday ? - nil : - .approachingExpiry } } @@ -121,9 +191,23 @@ private extension Date { } } -private extension SyntaxKind { - /// Returns if the syntax kind is comment-like. - var isCommentLike: Bool { - Self.commentKinds.contains(self) - } +private extension TriviaPiece { + var isLineComment: Bool { + switch self { + case .lineComment, .docLineComment: + true + default: + false + } + } + + var commentText: String? { + switch self { + case .lineComment(let text), .blockComment(let text), + .docLineComment(let text), .docBlockComment(let text): + text + default: + nil + } + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift index d198393897..a81026a6b0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift @@ -78,7 +78,6 @@ private extension TokenSyntax { ].map(basePattern).joined(separator: "|")) + capturingGroup(hyphenOrEmpty) private static let anySpace = " *" - private static let nonSpaceOrTwoOrMoreSpace = "(?: {2,})?" private static let anyText = "(?:\\S.*)" diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift index 2a8c744f1e..49ea44bb53 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift @@ -220,9 +220,9 @@ private extension SyntaxProtocol { return false } let ifConfigDecl = itemList - .parent?.as(IfConfigClauseSyntax.self)? - .parent?.as(IfConfigClauseListSyntax.self)? - .parent?.as(IfConfigDeclSyntax.self) + .parent?.as(IfConfigClauseSyntax.self)? + .parent?.as(IfConfigClauseListSyntax.self)? + .parent?.as(IfConfigDeclSyntax.self) if let ifConfigDecl { return ifConfigDecl.hasDocComment } @@ -303,19 +303,19 @@ private extension Stack { 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 - } + 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/NonOptionalStringDataConversionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift index 5141717370..bb690ef813 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift @@ -1,18 +1,35 @@ +import SwiftLintCore import SwiftSyntax @SwiftSyntaxRule struct NonOptionalStringDataConversionRule: Rule { - var configuration = SeverityConfiguration(.warning) + var configuration = NonOptionalStringDataConversionConfiguration() + + private static let variablesIncluded = ["include_variables": true] + static let description = RuleDescription( identifier: "non_optional_string_data_conversion", 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("Data(\"foo\".utf8)"), + Example("Data(string.utf8)"), + Example("\"foo\".data(using: .ascii)"), + Example("string.data(using: .unicode)"), + Example("Data(\"foo\".utf8)", configuration: variablesIncluded), + Example("Data(string.utf8)", configuration: variablesIncluded), + Example("\"foo\".data(using: .ascii)", configuration: variablesIncluded), + Example("string.data(using: .unicode)", configuration: variablesIncluded), ], triggeringExamples: [ - Example("\"foo\".data(using: .utf8)") + Example("↓\"foo\".data(using: .utf8)"), + Example("↓\"foo\".data(using: .utf8)", configuration: variablesIncluded), + Example("↓string.data(using: .utf8)", configuration: variablesIncluded), + Example("↓property.data(using: .utf8)", configuration: variablesIncluded), + Example("↓obj.property.data(using: .utf8)", configuration: variablesIncluded), + Example("↓getString().data(using: .utf8)", configuration: variablesIncluded), + Example("↓getValue()?.data(using: .utf8)", configuration: variablesIncluded), ] ) } @@ -20,12 +37,13 @@ struct NonOptionalStringDataConversionRule: Rule { private extension NonOptionalStringDataConversionRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: MemberAccessExprSyntax) { - if node.base?.is(StringLiteralExprSyntax.self) == true, - node.declName.baseName.text == "data", + if node.declName.baseName.text == "data", let parent = node.parent?.as(FunctionCallExprSyntax.self), let argument = parent.arguments.onlyElement, argument.label?.text == "using", - argument.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true { + argument.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true, + let base = node.base, + base.is(StringLiteralExprSyntax.self) || configuration.includeVariables { violations.append(node.positionAfterSkippingLeadingTrivia) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift index 428b79721e..d67dbfb0ae 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift @@ -78,7 +78,7 @@ private extension OrphanedDocCommentRule { switch piece { case .docLineComment(let comment), .docBlockComment(let comment): // These patterns are often used for "file header" style comments - if !comment.hasPrefix("////") && !comment.hasPrefix("/***") { + if !comment.hasPrefix("////"), !comment.hasPrefix("/***") { if isOrphanedDocComment(with: &iterator) { let utf8Length = pieces[.. [StyleViolation] { // is it a call to a restricted method? - guard - kind == .call, - let name = dictionary.name, - let kindName = QuickCallKind(rawValue: name), - QuickCallKind.restrictiveKinds.contains(kindName) - else { return [] } + guard kind == .call, + let name = dictionary.name, + let kindName = QuickCallKind(rawValue: name), + QuickCallKind.restrictiveKinds.contains(kindName) else { + return [] + } return violationOffsets(in: dictionary.enclosedArguments).map { StyleViolation(ruleDescription: Self.description, @@ -73,29 +73,30 @@ struct QuickDiscouragedCallRule: OptInRule { } private func toViolationOffsets(dictionary: SourceKittenDictionary) -> [ByteCount] { - guard - dictionary.kind != nil, - let offset = dictionary.offset - else { return [] } + guard dictionary.kind != nil, + let offset = dictionary.offset else { + return [] + } if dictionary.expressionKind == .call, - let name = dictionary.name, QuickCallKind(rawValue: name) == nil { + let name = dictionary.name, QuickCallKind(rawValue: name) == nil { return [offset] } - guard dictionary.expressionKind != .call else { return [] } + guard dictionary.expressionKind != .call else { + return [] + } return dictionary.substructure.compactMap(toViolationOffset) } private func toViolationOffset(dictionary: SourceKittenDictionary) -> ByteCount? { - guard - let name = dictionary.name, - let offset = dictionary.offset, - dictionary.expressionKind == .call, - QuickCallKind(rawValue: name) == nil - else { return nil } - + guard let name = dictionary.name, + let offset = dictionary.offset, + dictionary.expressionKind == .call, + QuickCallKind(rawValue: name) == nil else { + return nil + } return offset } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift index 4ef5f9b115..c0749e278a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift @@ -57,10 +57,10 @@ private extension TriviaPiece { for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] { switch self { case - .blockComment(let comment), - .lineComment(let comment), - .docBlockComment(let comment), - .docLineComment(let comment): + .blockComment(let comment), + .lineComment(let comment), + .docBlockComment(let comment), + .docLineComment(let comment): // Construct a regex string considering only keywords. let searchKeywords = todoKeywords.map(\.rawValue).joined(separator: "|") diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift index fb353a8ac6..db265b8a1f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift @@ -52,14 +52,14 @@ struct TypesafeArrayInitRule: AnalyzerRule { private static let parentRule = ArrayInitRule() 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 - """), + 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] { diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift index c5a6b7fc7c..9a21737b6e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift @@ -25,7 +25,7 @@ private extension UnneededOverrideRule { } override func visitPost(_ node: InitializerDeclSyntax) { - if configuration.affectInits && node.isUnneededOverride { + if configuration.affectInits, node.isUnneededOverride { self.violations.append(node.positionAfterSkippingLeadingTrivia) } } @@ -96,7 +96,7 @@ private extension OverridableDecl { } guard let call = extractFunctionCallSyntax(statement.item), - let member = call.calledExpression.as(MemberAccessExprSyntax.self), + let member = call.calledExpression.as(MemberAccessExprSyntax.self), member.base?.is(SuperExprSyntax.self) == true, member.declName.baseName.text == name else { return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift index 616725f30a..93934ed401 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift @@ -84,8 +84,7 @@ private extension SwiftLintFile { func index(compilerArguments: [String]) -> SourceKittenDictionary? { path .flatMap { path in - try? Request.index(file: path, arguments: compilerArguments) - .send() + try? Request.index(file: path, arguments: compilerArguments).send() } .map(SourceKittenDictionary.init) } @@ -162,9 +161,9 @@ private extension SwiftLintFile { // Skip CodingKeys as they are used for Codable generation if kind == .enum, - indexEntity.name == "CodingKeys", - case let allRelatedUSRs = indexEntity.traverseEntitiesDepthFirst(traverseBlock: { $1.usr }), - allRelatedUSRs.contains("s:s9CodingKeyP") { + indexEntity.name == "CodingKeys", + case let allRelatedUSRs = indexEntity.traverseEntitiesDepthFirst(traverseBlock: { $1.usr }), + allRelatedUSRs.contains("s:s9CodingKeyP") { return nil } @@ -195,7 +194,7 @@ private extension SwiftLintFile { // with "related names", which appears to be similarly named declarations (i.e. overloads) that are // programmatically unrelated to the current cursor-info declaration. Those similarly named declarations // aren't in `key.related` so confirm that that one is also populated. - if cursorInfo?.value["key.related_decls"] != nil && indexEntity.value["key.related"] != nil { + if cursorInfo?.value["key.related_decls"] != nil, indexEntity.value["key.related"] != nil { return nil } @@ -264,8 +263,8 @@ private extension SourceKittenDictionary { func propertyAtOffset(_ offset: ByteCount, property: KeyPath) -> T? { if let nameOffset, - nameOffset == offset, - let field = self[keyPath: property] { + nameOffset == offset, + let field = self[keyPath: property] { return field } for child in substructure { diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift index c6b47b9c4e..59aefbb84c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift @@ -100,7 +100,7 @@ private extension SwiftLintFile { var unusedImports = imports.subtracting(usrFragments).subtracting(configuration.alwaysKeepImports) // Certain Swift attributes requires importing Foundation. - if unusedImports.contains("Foundation") && containsAttributesRequiringFoundation() { + if unusedImports.contains("Foundation"), containsAttributesRequiringFoundation() { unusedImports.remove("Foundation") } @@ -179,7 +179,7 @@ private extension SwiftLintFile { if nextIsModuleImport { nextIsModuleImport = false if let importedModule = cursorInfo.moduleName, - cursorInfo.kind == "source.lang.swift.ref.module" { + cursorInfo.kind == "source.lang.swift.ref.module" { imports.insert(importedModule) continue } @@ -207,7 +207,7 @@ private extension SwiftLintFile { // Operators are omitted in the editor.open request and thus have to be looked up by the indexsource request func operatorImports(arguments: [String], processedTokenOffsets: Set) -> Set { guard let index = (try? Request.index(file: path!, arguments: arguments).sendIfNotDisabled()) - .map(SourceKittenDictionary.init) else { + .map(SourceKittenDictionary.init) else { Issue.indexingError(path: path, ruleID: UnusedImportRule.identifier).print() return [] } @@ -230,7 +230,7 @@ private extension SwiftLintFile { file: path!, offset: ByteCount(offset), arguments: arguments ) guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()) - .map(SourceKittenDictionary.init) else { + .map(SourceKittenDictionary.init) else { Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.identifier).print() continue } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift index a3073f47b5..0c75a4f8c6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift @@ -142,7 +142,7 @@ private extension UnusedSetterValueRule { let visitor = NewValueUsageVisitor(variableName: variableName) if !visitor.walk(tree: node, handler: \.isVariableUsed) { if Syntax(node).closestVariableOrSubscript()?.modifiers?.contains(keyword: .override) == true, - let body = node.body, body.statements.isEmpty { + let body = node.body, body.statements.isEmpty { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift index 709ec62e7e..5f0562130f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift @@ -74,8 +74,8 @@ private extension WeakDelegateRule { override func visitPost(_ node: VariableDeclSyntax) { guard node.hasDelegateSuffix, node.weakOrUnownedModifier == nil, - !node.hasComputedBody, - !node.containsIgnoredAttribute, + !node.hasComputedBody, + !node.containsIgnoredAttribute, let parent = node.parent, Syntax(parent).enclosingClass() != nil else { return diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift index 8289db7f38..546620edc3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift @@ -21,7 +21,7 @@ struct YodaConditionRule: Rule { 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 {}"), @@ -72,26 +72,26 @@ private extension YodaConditionRule { guard let operatorIndex = children.index(of: comparisonOperator) else { continue } - let rhsIdx = children.index(operatorIndex, offsetBy: 1) + let rhsIdx = children.index(after: operatorIndex) if children[rhsIdx].isLiteral { - let afterRhsIndex = children.index(after: rhsIdx) - guard children.endIndex != rhsIdx, afterRhsIndex != nil else { + guard children.endIndex != children.index(after: rhsIdx) else { // This is already the end of the expression. continue } + let afterRhsIndex = children.index(after: rhsIdx) if children[afterRhsIndex].isLogicalBinaryOperator { // Next token is an operator with weaker binding. Thus, the literal is unique on the // right-hand side of the comparison operator. continue } } - let lhsIdx = children.index(operatorIndex, offsetBy: -1) + let lhsIdx = children.index(before: operatorIndex) let lhs = children[lhsIdx] if lhs.isLiteral, children.startIndex == lhsIdx || children[children.index(before: lhsIdx)].isLogicalBinaryOperator { - // Literal is at the very beginning of the expression or the previous token is an operator with - // weaker binding. Thus, the literal is unique on the left-hand side of the comparison operator. - violations.append(lhs.positionAfterSkippingLeadingTrivia) + // Literal is at the very beginning of the expression or the previous token is an operator with + // weaker binding. Thus, the literal is unique on the left-hand side of the comparison operator. + violations.append(lhs.positionAfterSkippingLeadingTrivia) } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift index 5e322e5182..97d316d870 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift @@ -1,5 +1,9 @@ -struct ClosureBodyLengthRule: OptInRule, SwiftSyntaxRule { +import SwiftSyntax + +@SwiftSyntaxRule(optIn: true) +struct ClosureBodyLengthRule: Rule { private static let defaultWarningThreshold = 30 + var configuration = SeverityLevelsConfiguration(warning: Self.defaultWarningThreshold, error: 100) static let description = RuleDescription( @@ -15,8 +19,17 @@ struct ClosureBodyLengthRule: OptInRule, SwiftSyntaxRule { nonTriggeringExamples: ClosureBodyLengthRuleExamples.nonTriggeringExamples, triggeringExamples: ClosureBodyLengthRuleExamples.triggeringExamples ) +} - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - BodyLengthRuleVisitor(kind: .closure, file: file, configuration: configuration) +private extension ClosureBodyLengthRule { + final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: ClosureExprSyntax) { + registerViolations( + leftBrace: node.leftBrace, + rightBrace: node.rightBrace, + violationNode: node.leftBrace, + objectName: "Closure" + ) + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift index 6679fec0bf..e2e7b5ee06 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift @@ -1,5 +1,6 @@ -import SourceKittenFramework +import SwiftSyntax +@SwiftSyntaxRule struct FileLengthRule: Rule { var configuration = FileLengthConfiguration() @@ -17,38 +18,85 @@ struct FileLengthRule: Rule { Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()), ].skipWrappingInCommentTests() ) +} + +private extension FileLengthRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: SourceFileSyntax) { + let lineCount = configuration.ignoreCommentOnlyLines ? countNonCommentLines(in: node) : file.lines.count + + let severity: ViolationSeverity, upperBound: Int + if let error = configuration.severityConfiguration.error, lineCount > error { + severity = .error + upperBound = error + } else if lineCount > configuration.severityConfiguration.warning { + severity = .warning + upperBound = configuration.severityConfiguration.warning + } else { + return + } + + let reason = "File should contain \(upperBound) lines or less" + + (configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") + + ": currently contains \(lineCount)" + + // Position violation at the start of the last line to avoid boundary issues + let lastLine = file.lines.last + let lastLineStartOffset = lastLine?.byteRange.location ?? 0 + let violationPosition = AbsolutePosition(utf8Offset: lastLineStartOffset.value) - func validate(file: SwiftLintFile) -> [StyleViolation] { - func lineCountWithoutComments() -> Int { - let commentKinds = SyntaxKind.commentKinds - return file.syntaxKindsByLines.filter { kinds in - !Set(kinds).isSubset(of: commentKinds) - }.count + let violation = ReasonedRuleViolation( + position: violationPosition, + reason: reason, + severity: severity + ) + violations.append(violation) } - var lineCount = file.lines.count - let hasViolation = configuration.severityConfiguration.params.contains { - $0.value < lineCount + private func countNonCommentLines(in node: SourceFileSyntax) -> Int { + var linesWithActualContent = Set() + + for token in node.tokens(viewMode: .sourceAccurate) { + addTokenContentLines(token, to: &linesWithActualContent) + + // Process leading trivia + addTriviaLines(token.leadingTrivia, startingAt: token.position, to: &linesWithActualContent) + } + return linesWithActualContent.count } - if hasViolation && configuration.ignoreCommentOnlyLines { - lineCount = lineCountWithoutComments() + private func addTokenContentLines(_ token: TokenSyntax, to lines: inout Set) { + // Skip tokens whose text is empty or only whitespace + // (e.g., EOF token, or an unlikely malformed token). + guard !token.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } + + let startLocation = locationConverter.location(for: token.positionAfterSkippingLeadingTrivia) + let endLocation = locationConverter.location(for: token.endPositionBeforeTrailingTrivia) + + addLinesInRange(from: startLocation.line, to: endLocation.line, to: &lines) } - for parameter in configuration.severityConfiguration.params where lineCount > parameter.value { - 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 - ), - ] + private func addTriviaLines( + _ trivia: Trivia, + startingAt startPosition: AbsolutePosition, + to lines: inout Set + ) { + var currentPosition = startPosition + for piece in trivia { + if !piece.isComment, !piece.isWhitespace { + let startLocation = locationConverter.location(for: currentPosition) + let endLocation = locationConverter.location(for: currentPosition + piece.sourceLength) + addLinesInRange(from: startLocation.line, to: endLocation.line, to: &lines) + } + currentPosition += piece.sourceLength + } } - return [] + private func addLinesInRange(from startLine: Int, to endLine: Int, to lines: inout Set) { + guard startLine > 0, startLine <= endLine else { return } + for line in startLine...endLine { + lines.insert(line) + } + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index dc6eb5fe9f..1393a61563 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -1,16 +1,168 @@ -import SwiftLintCore +import SwiftSyntax -struct FunctionBodyLengthRule: SwiftSyntaxRule { +@SwiftSyntaxRule +struct FunctionBodyLengthRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 50, error: 100) + private static let testConfig = ["warning": 2] + static let description = RuleDescription( identifier: "function_body_length", name: "Function Body Length", description: "Function bodies should not span too many lines", - kind: .metrics + kind: .metrics, + nonTriggeringExamples: [ + Example("func f() {}", configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + let y = 1 + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + // comments + // will + // be + // ignored + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + // empty lines will be ignored + + + } + """, configuration: Self.testConfig), + ], + + triggeringExamples: [ + Example(""" + ↓func f() { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓deinit { + let x = 0 + let y = 1 + let z = 2 + } + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓init() { + let x = 0 + let y = 1 + let z = 2 + } + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓subscript() -> Int { + let x = 0 + let y = 1 + return x + y + } + } + """, configuration: Self.testConfig), + Example(""" + struct S { + subscript() -> Int { + ↓get { + let x = 0 + let y = 1 + return x + y + } + ↓set { + let x = 0 + let y = 1 + let z = 2 + } + ↓willSet { + let x = 0 + let y = 1 + let z = 2 + } + } + } + """, configuration: Self.testConfig), + ] ) +} + +private extension FunctionBodyLengthRule { + final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: DeinitializerDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.deinitKeyword, + objectName: "Deinitializer" + ) + } + } + + override func visitPost(_ node: FunctionDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.funcKeyword, + objectName: "Function" + ) + } + } + + override func visitPost(_ node: InitializerDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.initKeyword, + objectName: "Initializer" + ) + } + } - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - BodyLengthRuleVisitor(kind: .function, file: file, configuration: configuration) + override func visitPost(_ node: SubscriptDeclSyntax) { + guard let body = node.accessorBlock else { + return + } + if case .getter = body.accessors { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.subscriptKeyword, + objectName: "Subscript" + ) + } + if case let .accessors(accessors) = body.accessors { + for accessor in accessors { + guard let body = accessor.body else { + continue + } + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: accessor.accessorSpecifier, + objectName: "Accessor" + ) + } + } + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift index b3196aefc8..35d31abd8d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift @@ -1,6 +1,7 @@ import SwiftSyntax -struct LargeTupleRule: SourceKitFreeRule { +@SwiftSyntaxRule +struct LargeTupleRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 2, error: 3) static let description = RuleDescription( @@ -8,32 +9,74 @@ struct LargeTupleRule: SourceKitFreeRule { name: "Large Tuple", description: "Tuples shouldn't have too many members. Create a custom type instead.", kind: .metrics, - nonTriggeringExamples: LargeTupleRuleExamples.nonTriggeringExamples, - triggeringExamples: LargeTupleRuleExamples.triggeringExamples + nonTriggeringExamples: [ + Example("let foo: (Int, Int)"), + Example("let foo: (start: Int, end: Int)"), + Example("let foo: (Int, (Int, String))"), + Example("func foo() -> (Int, Int)"), + Example("func foo() -> (Int, Int) {}"), + Example("func foo(bar: String) -> (Int, Int)"), + Example("func foo(bar: String) -> (Int, Int) {}"), + Example("func foo() throws -> (Int, Int)"), + Example("func foo() throws -> (Int, Int) {}"), + Example("let foo: (Int, Int, Int) -> Void"), + Example("let foo: (Int, Int, Int) throws -> Void"), + Example("func foo(bar: (Int, String, Float) -> Void)"), + Example("func foo(bar: (Int, String, Float) throws -> Void)"), + Example("var completionHandler: ((_ data: Data?, _ resp: URLResponse?, _ e: NSError?) -> Void)!"), + Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), + Example("func getGenericTypeAndInt() -> (Type, Int)?"), + Example("func foo() async -> (Int, Int)"), + Example("func foo() async -> (Int, Int) {}"), + Example("func foo(bar: String) async -> (Int, Int)"), + Example("func foo(bar: String) async -> (Int, Int) {}"), + Example("func foo() async throws -> (Int, Int)"), + Example("func foo() async throws -> (Int, Int) {}"), + Example("let foo: (Int, Int, Int) async -> Void"), + Example("let foo: (Int, Int, Int) async throws -> Void"), + 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)?"), + ], + triggeringExamples: [ + Example("let foo: ↓(Int, Int, Int)"), + Example("let foo: ↓(start: Int, end: Int, value: String)"), + Example("let foo: (Int, ↓(Int, Int, Int))"), + Example("func foo(bar: ↓(Int, Int, Int))"), + Example("func foo() -> ↓(Int, Int, Int)"), + Example("func foo() -> ↓(Int, Int, Int) {}"), + Example("func foo(bar: String) -> ↓(Int, Int, Int)"), + Example("func foo(bar: String) -> ↓(Int, Int, Int) {}"), + Example("func foo() throws -> ↓(Int, Int, Int)"), + Example("func foo() throws -> ↓(Int, Int, Int) {}"), + Example("func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}"), + Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), + Example("func foo(bar: ↓(Int, Int, Int)) async"), + Example("func foo() async -> ↓(Int, Int, Int)"), + Example("func foo() async -> ↓(Int, Int, Int) {}"), + Example("func foo(bar: String) async -> ↓(Int, Int, Int)"), + Example("func foo(bar: String) async -> ↓(Int, Int, Int) {}"), + 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)?"), + ] ) - - func validate(file: SwiftLintFile) -> [StyleViolation] { - LargeTupleRuleVisitor(viewMode: .sourceAccurate) - .walk(file: file, handler: \.violationPositions) - .sorted(by: { $0.position < $1.position }) - .compactMap { position, size in - for parameter in configuration.params where size > parameter.value { - let reason = "Tuples should have at most \(configuration.warning) members" - return StyleViolation(ruleDescription: Self.description, - severity: parameter.severity, - location: Location(file: file, position: position), - reason: reason) - } - - return nil - } - } } -private final class LargeTupleRuleVisitor: SyntaxVisitor { - private(set) var violationPositions: [(position: AbsolutePosition, memberCount: Int)] = [] - - override func visitPost(_ node: TupleTypeSyntax) { - violationPositions.append((node.positionAfterSkippingLeadingTrivia, node.elements.count)) +private extension LargeTupleRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: TupleTypeSyntax) { + let memberCount = node.elements.count + for parameter in configuration.params where memberCount > parameter.value { + violations.append(.init( + position: node.positionAfterSkippingLeadingTrivia, + reason: "Tuples should have at most \(configuration.warning) members", + severity: parameter.severity + )) + return + } + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift deleted file mode 100644 index ec604610ee..0000000000 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift +++ /dev/null @@ -1,56 +0,0 @@ -struct LargeTupleRuleExamples { - static let nonTriggeringExamples: [Example] = [ - Example("let foo: (Int, Int)"), - Example("let foo: (start: Int, end: Int)"), - Example("let foo: (Int, (Int, String))"), - Example("func foo() -> (Int, Int)"), - Example("func foo() -> (Int, Int) {}"), - Example("func foo(bar: String) -> (Int, Int)"), - Example("func foo(bar: String) -> (Int, Int) {}"), - Example("func foo() throws -> (Int, Int)"), - Example("func foo() throws -> (Int, Int) {}"), - Example("let foo: (Int, Int, Int) -> Void"), - Example("let foo: (Int, Int, Int) throws -> Void"), - Example("func foo(bar: (Int, String, Float) -> Void)"), - Example("func foo(bar: (Int, String, Float) throws -> Void)"), - Example("var completionHandler: ((_ data: Data?, _ resp: URLResponse?, _ e: NSError?) -> Void)!"), - Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), - Example("func getGenericTypeAndInt() -> (Type, Int)?"), - Example("func foo() async -> (Int, Int)"), - Example("func foo() async -> (Int, Int) {}"), - Example("func foo(bar: String) async -> (Int, Int)"), - Example("func foo(bar: String) async -> (Int, Int) {}"), - Example("func foo() async throws -> (Int, Int)"), - Example("func foo() async throws -> (Int, Int) {}"), - Example("let foo: (Int, Int, Int) async -> Void"), - Example("let foo: (Int, Int, Int) async throws -> Void"), - 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)?"), - ] - - static let triggeringExamples: [Example] = [ - Example("let foo: ↓(Int, Int, Int)"), - Example("let foo: ↓(start: Int, end: Int, value: String)"), - Example("let foo: (Int, ↓(Int, Int, Int))"), - Example("func foo(bar: ↓(Int, Int, Int))"), - Example("func foo() -> ↓(Int, Int, Int)"), - Example("func foo() -> ↓(Int, Int, Int) {}"), - Example("func foo(bar: String) -> ↓(Int, Int, Int)"), - Example("func foo(bar: String) -> ↓(Int, Int, Int) {}"), - Example("func foo() throws -> ↓(Int, Int, Int)"), - Example("func foo() throws -> ↓(Int, Int, Int) {}"), - Example("func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}"), - Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), - Example("func foo(bar: ↓(Int, Int, Int)) async"), - Example("func foo() async -> ↓(Int, Int, Int)"), - Example("func foo() async -> ↓(Int, Int, Int) {}"), - Example("func foo(bar: String) async -> ↓(Int, Int, Int)"), - Example("func foo(bar: String) async -> ↓(Int, Int, Int) {}"), - 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)?"), - ] -} diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift index 38035d0992..b9caa3525e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift @@ -1,13 +1,11 @@ import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax +@SwiftSyntaxRule struct LineLengthRule: Rule { var configuration = LineLengthConfiguration() - private let commentKinds = SyntaxKind.commentKinds - private let nonCommentKinds = SyntaxKind.allKinds.subtracting(SyntaxKind.commentKinds) - private let functionKinds = SwiftDeclarationKind.functionKinds - static let description = RuleDescription( identifier: "line_length", name: "Line Length", @@ -24,149 +22,219 @@ struct LineLengthRule: Rule { Example(String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 121) + ""), ].skipWrappingInCommentTests().skipWrappingInStringTests() ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - let minValue = configuration.params.map(\.value).min() ?? .max - let swiftDeclarationKindsByLine = Lazy(file.swiftDeclarationKindsByLine() ?? []) - let syntaxKindsByLine = Lazy(file.syntaxKindsByLine() ?? []) - - return file.lines.compactMap { line in - // `line.content.count` <= `line.range.length` is true. - // So, `check line.range.length` is larger than minimum parameter value. - // for avoiding using heavy `line.content.count`. - if line.range.length < minValue { - return nil - } +private extension LineLengthRule { + final class Visitor: ViolationsSyntaxVisitor { + // To store line numbers that should be ignored based on configuration + private var functionDeclarationLines = Set() + private var commentOnlyLines = Set() + private var interpolatedStringLines = Set() + private var multilineStringLines = Set() - if configuration.ignoresFunctionDeclarations && - lineHasKinds(line: line, - kinds: functionKinds, - kindsByLine: swiftDeclarationKindsByLine.value) { - return nil + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + // Populate functionDeclarationLines if ignores_function_declarations is true + if configuration.ignoresFunctionDeclarations { + let funcVisitor = FunctionLineVisitor(locationConverter: locationConverter) + functionDeclarationLines = funcVisitor.walk(tree: node, handler: \.lines) } - if configuration.ignoresComments && - lineHasKinds(line: line, - kinds: commentKinds, - kindsByLine: syntaxKindsByLine.value) && - !lineHasKinds(line: line, - kinds: nonCommentKinds, - kindsByLine: syntaxKindsByLine.value) { - return nil + // Populate multilineStringLines if ignores_multiline_strings is true + if configuration.ignoresMultilineStrings { + let stringVisitor = MultilineStringLiteralVisitor(locationConverter: locationConverter) + multilineStringLines = stringVisitor.walk(tree: node, handler: \.linesSpanned) } - if configuration.ignoresInterpolatedStrings && - lineHasKinds(line: line, - kinds: [.stringInterpolationAnchor], - kindsByLine: syntaxKindsByLine.value) { - return nil + // Populate interpolatedStringLines if ignores_interpolated_strings is true + if configuration.ignoresInterpolatedStrings { + let interpVisitor = InterpolatedStringLineVisitor(locationConverter: locationConverter) + interpolatedStringLines = interpVisitor.walk(tree: node, handler: \.lines) } - if configuration.ignoresMultilineStrings && - lineIsMultilineString(line, file: file, syntaxKindsByLine: syntaxKindsByLine.value) { - return nil + // Populate commentOnlyLines if ignores_comments is true + if configuration.ignoresComments { + commentOnlyLines = findCommentOnlyLines(in: node, file: file, locationConverter: locationConverter) } - for pattern in configuration.excludedLinesPatterns where line.containsMatchingPattern(pattern) { - return nil + return .skipChildren // We'll do the main processing in visitPost + } + + override func visitPost(_: SourceFileSyntax) { + let minLengthThreshold = configuration.params.map(\.value).min() ?? .max + + for line in file.lines { + // Quick check to skip very short lines before expensive stripping + // `line.content.count` <= `line.range.length` is true. + // So, check `line.range.length` is larger than minimum parameter value + // for avoiding using heavy `line.content.count`. + if line.range.length < minLengthThreshold { + continue + } + + // Apply ignore configurations + if configuration.ignoresFunctionDeclarations, functionDeclarationLines.contains(line.index) { + continue + } + if configuration.ignoresComments, commentOnlyLines.contains(line.index) { + continue + } + if configuration.ignoresInterpolatedStrings, interpolatedStringLines.contains(line.index) { + continue + } + if configuration.ignoresMultilineStrings, multilineStringLines.contains(line.index) { + continue + } + if configuration.excludedLinesPatterns.contains(where: { + regex($0).firstMatch(in: line.content, range: line.content.fullNSRange) != nil + }) { + continue + } + + // String stripping logic + var strippedString = line.content + if configuration.ignoresURLs { + strippedString = strippedString.strippingURLs + } + strippedString = stripLiterals(fromSourceString: strippedString, withDelimiter: "#colorLiteral") + strippedString = stripLiterals(fromSourceString: strippedString, withDelimiter: "#imageLiteral") + + let length = strippedString.count // Character count for reporting + + // Check against configured length limits + for param in configuration.params where length > param.value { + let reason = "Line should be \(param.value) characters or less; " + + "currently it has \(length) characters" + // Position the violation at the start of the line, consistent with original behavior + violations.append(ReasonedRuleViolation( + position: locationConverter.position(ofLine: line.index, column: 1), // Start of the line + reason: reason, + severity: param.severity + )) + break // Only report one violation (the most severe one reached) per line + } } + } - var strippedString = line.content - if configuration.ignoresURLs { - strippedString = strippedString.strippingURLs + // Strip color and image literals from the source string + private func stripLiterals(fromSourceString sourceString: String, + withDelimiter delimiter: String) -> String { + var modifiedString = sourceString + while modifiedString.contains("\(delimiter)(") { + if let rangeStart = modifiedString.range(of: "\(delimiter)("), + let rangeEnd = modifiedString.range(of: ")", options: .literal, + range: rangeStart.lowerBound.. param.value { - let reason = "Line should be \(param.value) characters or less; currently it has \(length) characters" - return StyleViolation(ruleDescription: Self.description, - severity: param.severity, - location: Location(file: file.path, line: line.index), - reason: reason) + return modifiedString + } + + private func findCommentOnlyLines( + in node: SourceFileSyntax, + file: SwiftLintFile, + locationConverter: SourceLocationConverter + ) -> Set { + var commentOnlyLines = Set() + + // For each line, check if it contains only comments and whitespace + for line in file.lines { + let lineContent = line.content.trimmingCharacters(in: .whitespaces) + + // Skip empty lines + if lineContent.isEmpty { continue } + + // Check if line starts with comment markers + if lineContent.hasPrefix("//") || lineContent.hasPrefix("/*") || + (lineContent.hasPrefix("*/") && lineContent.count == 2) { + // Now verify using SwiftSyntax that this line doesn't contain any tokens + var hasNonCommentContent = false + + for token in node.tokens(viewMode: .sourceAccurate) { + if token.tokenKind == .endOfFile { continue } + + let tokenLine = locationConverter.location(for: token.position).line + if tokenLine == line.index { + hasNonCommentContent = true + break + } + } + + if !hasNonCommentContent { + commentOnlyLines.insert(line.index) + } + } } - return nil + + return commentOnlyLines } } +} - /// Checks if the given line is part of a multiline string - /// - Example: - /// ``` - /// let a = """ - /// - /// """ - /// ``` - private func lineIsMultilineString(_ line: Line, file: SwiftLintFile, syntaxKindsByLine: [[SyntaxKind]]) -> Bool { - // contents of multiline strings only include one string element per line - guard syntaxKindsByLine[line.index] == [.string] else { return false } - - // find the trailing delimiter `"""` in order to make sure we're not in a list of concatenated strings - let lastStringLineIndex = syntaxKindsByLine.dropFirst(line.index + 1).firstIndex(where: { $0 != [.string] }) - guard let lastStringLineIndex else { - return file.lines.last?.content.trimmingCharacters(in: .whitespaces).hasPrefix("\"\"\"") == true - } +// MARK: - Helper Visitors for Pre-computation - // lines include leading empty element - // check last string line for single `"""` - // and if it fails, check the next line contains more than just a string for `"""; let a = 1` - return file.lines[lastStringLineIndex - 1].content.trimmingCharacters(in: .whitespaces).hasPrefix("\"\"\"") || - file.lines[lastStringLineIndex - 2].content.trimmingCharacters(in: .whitespaces).hasPrefix("\"\"\"") +// Visitor to find lines spanned by function declarations +private final class FunctionLineVisitor: SyntaxVisitor { + let locationConverter: SourceLocationConverter + var lines = Set() + + init(locationConverter: SourceLocationConverter) { + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } - /// Takes a string and replaces any literals specified by the `delimiter` parameter with `#` - /// - /// - parameter sourceString: Original string, possibly containing literals - /// - parameter delimiter: Delimiter of the literal - /// (characters before the parentheses, e.g. `#colorLiteral`) - /// - /// - returns: sourceString with the given literals replaced by `#` - private func stripLiterals(fromSourceString sourceString: String, - withDelimiter delimiter: String) -> String { - var modifiedString = sourceString - - // While copy of content contains literal, replace with a single character - while modifiedString.contains("\(delimiter)(") { - if let rangeStart = modifiedString.range(of: "\(delimiter)("), - let rangeEnd = modifiedString.range(of: ")", - options: .literal, - range: - rangeStart.lowerBound..() + + init(locationConverter: SourceLocationConverter) { + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } - private func lineHasKinds(line: Line, kinds: Set, kindsByLine: [[Kind]]) -> Bool { - let index = line.index - if index >= kindsByLine.count { - return false + override func visitPost(_ node: ExpressionSegmentSyntax) { + // ExpressionSegmentSyntax is the interpolation inside a string + let startLocation = locationConverter.location(for: node.positionAfterSkippingLeadingTrivia) + let endLocation = locationConverter.location(for: node.endPositionBeforeTrailingTrivia) + for line in startLocation.line...endLocation.line { + lines.insert(line) } - return !kinds.isDisjoint(with: kindsByLine[index]) } } -private extension Line { - func containsMatchingPattern(_ pattern: String) -> Bool { - regex(pattern).firstMatch(in: content, range: content.fullNSRange) != nil +// Visitor to find line ranges covered by multiline string literals +private final class MultilineStringLiteralVisitor: SyntaxVisitor { + let locationConverter: SourceLocationConverter + var linesSpanned = Set() + + init(locationConverter: SourceLocationConverter) { + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } -} -// extracted from https://forums.swift.org/t/pitch-declaring-local-variables-as-lazy/9287/3 -private class Lazy { - private var computation: () -> Result - fileprivate private(set) lazy var value: Result = computation() + override func visitPost(_ node: StringLiteralExprSyntax) { + guard node.openingQuote.tokenKind == .multilineStringQuote || + (node.openingPounds != nil && node.openingQuote.tokenKind == .stringQuote) else { + return + } + + let startLocation = locationConverter.location(for: node.positionAfterSkippingLeadingTrivia) + let endLocation = locationConverter.location(for: node.endPositionBeforeTrailingTrivia) - init(_ computation: @escaping @autoclosure () -> Result) { - self.computation = computation + for line in startLocation.line...endLocation.line { + linesSpanned.insert(line) + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift index 7eb1af8163..0f55a1c624 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift @@ -64,7 +64,14 @@ private extension NestingRule { } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.enumKeyword) + // if current defines coding keys and we're ignoring coding keys, then skip nesting rule + // push another level on and proceed to visit children + if configuration.ignoreCodingKeys, node.definesCodingKeys { + levels.push(levels.lastIsFunction) + } else { + validate(forFunction: false, triggeringToken: node.enumKeyword) + } + return .visitChildren } @@ -149,9 +156,10 @@ private extension NestingRule { let targetLevel = forFunction ? configuration.functionLevel : configuration.typeLevel // if parent is function and current is not function types, then skip nesting rule. - if configuration.alwaysAllowOneTypeInFunctions && inFunction && !forFunction { + if configuration.alwaysAllowOneTypeInFunctions, inFunction, !forFunction { return } + guard let severity = configuration.severity(with: targetLevel, for: level) else { return } let targetName = forFunction ? "Functions" : "Types" diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift index d94c196068..17c2122514 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift @@ -8,6 +8,7 @@ internal struct NestingRuleExamples { + nonTriggeringClosureAndStatementExamples + nonTriggeringProtocolExamples + nonTriggeringMixedExamples + + nonTriggeringExamplesIgnoreCodingKeys private static let nonTriggeringTypeExamples = detectingTypes.flatMap { type -> [Example] in @@ -228,6 +229,21 @@ internal struct NestingRuleExamples { """), ] } + + private static let nonTriggeringExamplesIgnoreCodingKeys: [Example] = [ + Example( + """ + struct Outer { + struct Inner { + enum CodingKeys: String, CodingKey { + case id + } + } + } + """, + configuration: ["ignore_coding_keys": true] + ), + ] } extension NestingRuleExamples { @@ -236,6 +252,8 @@ extension NestingRuleExamples { + triggeringClosureAndStatementExamples + triggeringProtocolExamples + triggeringMixedExamples + + triggeringExamplesCodingKeys + + triggeringExamplesIgnoreCodingKeys private static let triggeringTypeExamples = detectingTypes.flatMap { type -> [Example] in @@ -499,4 +517,42 @@ extension NestingRuleExamples { """), ] } + + private static let triggeringExamplesCodingKeys: [Example] = [ + Example(""" + struct Outer { + struct Inner { + ↓enum CodingKeys: String, CodingKey { + case id + } + } + } + """), + ] + + private static let triggeringExamplesIgnoreCodingKeys: [Example] = [ + Example( + """ + struct Outer { + struct Inner { + ↓enum Example: String, CodingKey { + case id + } + } + } + """, + configuration: ["ignore_coding_keys": true] + ), + Example( + """ + struct Outer { + enum CodingKeys: String, CodingKey { + case id + ↓struct S {} + } + } + """, + configuration: ["ignore_coding_keys": true] + ), + ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index 08c1318a04..47aa4f0211 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -1,39 +1,149 @@ -import SwiftLintCore - -private func wrapExample( - prefix: String = "", - _ type: String, - _ template: String, - _ count: Int, - _ add: String = "", - file: StaticString = #filePath, - line: UInt = #line) -> Example { - Example("\(prefix)\(type) Abc {\n" + - repeatElement(template, count: count).joined() + "\(add)}\n", file: file, line: line) -} +import SwiftSyntax + +@SwiftSyntaxRule +struct TypeBodyLengthRule: Rule { + var configuration = TypeBodyLengthConfiguration() -struct TypeBodyLengthRule: SwiftSyntaxRule { - var configuration = SeverityLevelsConfiguration(warning: 250, error: 350) + private static let testConfig = ["warning": 2] as [String: any Sendable] + private static let testConfigWithAllTypes = testConfig.merging( + ["excluded_types": [] as [String]], + uniquingKeysWith: { $1 } + ) static let description = RuleDescription( identifier: "type_body_length", name: "Type Body Length", description: "Type bodies should not span too many lines", kind: .metrics, - nonTriggeringExamples: ["class", "struct", "enum", "actor"].flatMap({ type in - [ - 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*/"), - ] - }), - triggeringExamples: ["class", "struct", "enum", "actor"].map({ type in - wrapExample(prefix: "↓", type, "let abc = 0\n", 251) - }) + nonTriggeringExamples: [ + Example("actor A {}", configuration: testConfig), + Example("class C {}", configuration: testConfig), + Example("enum E {}", configuration: testConfig), + Example("extension E {}", configuration: testConfigWithAllTypes), + Example("protocol P {}", configuration: testConfigWithAllTypes), + Example("struct S {}", configuration: testConfig), + Example(""" + actor A { + let x = 0 + } + """, configuration: testConfig), + Example(""" + class C { + let x = 0 + // comments + // will + // be + // ignored + } + """, configuration: testConfig), + Example(""" + enum E { + let x = 0 + // empty lines will be ignored + + + } + """, configuration: testConfig), + Example(""" + protocol P { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + ], + triggeringExamples: [ + Example(""" + ↓actor A { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + Example(""" + ↓class C { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + Example(""" + ↓enum E { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + Example(""" + ↓extension E { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfigWithAllTypes), + Example(""" + ↓protocol P { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfigWithAllTypes), + Example(""" + ↓struct S { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + ] ) +} + +private extension TypeBodyLengthRule { + final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: ActorDeclSyntax) { + if !configuration.excludedTypes.contains(.actor) { + collectViolation(node) + } + } + + override func visitPost(_ node: ClassDeclSyntax) { + if !configuration.excludedTypes.contains(.class) { + collectViolation(node) + } + } + + override func visitPost(_ node: EnumDeclSyntax) { + if !configuration.excludedTypes.contains(.enum) { + collectViolation(node) + } + } + + override func visitPost(_ node: ExtensionDeclSyntax) { + if !configuration.excludedTypes.contains(.extension) { + collectViolation(node) + } + } + + override func visitPost(_ node: ProtocolDeclSyntax) { + if !configuration.excludedTypes.contains(.protocol) { + collectViolation(node) + } + } + + override func visitPost(_ node: StructDeclSyntax) { + if !configuration.excludedTypes.contains(.struct) { + collectViolation(node) + } + } - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - BodyLengthRuleVisitor(kind: .type, file: file, configuration: configuration) + private func collectViolation(_ node: some DeclGroupSyntax) { + registerViolations( + leftBrace: node.memberBlock.leftBrace, + rightBrace: node.memberBlock.rightBrace, + violationNode: node.introducer, + objectName: node.introducer.text.capitalized + ) + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift index 6436937ebc..2b21b5b129 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift @@ -148,7 +148,7 @@ private extension EmptyCountRule { private extension ExprSyntax { func countCallPosition(onlyAfterDot: Bool) -> AbsolutePosition? { if let expr = self.as(MemberAccessExprSyntax.self) { - if expr.declName.argumentNames == nil && expr.declName.baseName.tokenKind == .identifier("count") { + if expr.declName.argumentNames == nil, expr.declName.baseName.tokenKind == .identifier("count") { return expr.declName.baseName.positionAfterSkippingLeadingTrivia } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift index e3ae47c1ca..824178a569 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift @@ -56,8 +56,8 @@ private extension ExprSyntax { return true } if let functionCall = self.as(FunctionCallExprSyntax.self), - let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), - calledExpression.baseName.text == "NSPredicate" { + let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), + calledExpression.baseName.text == "NSPredicate" { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift index f053764d5e..b8dafbcef4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift @@ -52,8 +52,8 @@ private extension ExprSyntax { return true } if let functionCall = self.as(FunctionCallExprSyntax.self), - let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), - calledExpression.baseName.text == "NSPredicate" { + let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), + calledExpression.baseName.text == "NSPredicate" { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift index a179edf276..a4ba9e4898 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift @@ -152,8 +152,8 @@ private extension ExprSyntax { return identifierExpr.isCopyOnWriteType } if let memberAccesExpr = expr.calledExpression.as(MemberAccessExprSyntax.self), - memberAccesExpr.declName.baseName.text == "init", - let identifierExpr = memberAccesExpr.base?.identifierExpr { + memberAccesExpr.declName.baseName.text == "init", + let identifierExpr = memberAccesExpr.base?.identifierExpr { return identifierExpr.isCopyOnWriteType } if expr.calledExpression.isCopyOnWriteType { @@ -162,7 +162,7 @@ private extension ExprSyntax { } return false - } + } var identifierExpr: DeclReferenceExprSyntax? { if let identifierExpr = self.as(DeclReferenceExprSyntax.self) { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift index 6c7cd87ee1..e164c5dc00 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift @@ -52,10 +52,6 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { self.init(platform: platform, major: major, minor: minor, 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 { @@ -106,14 +102,14 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { var parameterDescription: RuleConfigurationDescription? { let targets = Dictionary(uniqueKeysWithValues: [ - iOSDeploymentTarget, - iOSAppExtensionDeploymentTarget, - macOSDeploymentTarget, - macOSAppExtensionDeploymentTarget, - watchOSDeploymentTarget, - watchOSAppExtensionDeploymentTarget, - tvOSDeploymentTarget, - tvOSAppExtensionDeploymentTarget, + iOSDeploymentTarget, + iOSAppExtensionDeploymentTarget, + macOSDeploymentTarget, + macOSAppExtensionDeploymentTarget, + watchOSDeploymentTarget, + watchOSAppExtensionDeploymentTarget, + tvOSDeploymentTarget, + tvOSAppExtensionDeploymentTarget, ].map { ($0.platform.configurationKey, $0) }) severityConfiguration for (platform, target) in targets.sorted(by: { $0.key < $1.key }) { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift index e23e265ffd..e48ee1e6af 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift @@ -1,5 +1,4 @@ import Foundation -import SourceKittenFramework import SwiftLintCore struct FileHeaderConfiguration: SeverityBasedRuleConfiguration { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift index 7aa8be61b5..1dca2b089a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift @@ -7,7 +7,9 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) @ConfigurationElement(key: "excluded") - private(set) var excluded: Set = ["main.swift", "LinuxMain.swift"] + private(set) var excluded = Set(["main.swift", "LinuxMain.swift"]) + @ConfigurationElement(key: "excluded_paths") + private(set) var excludedPaths = Set() @ConfigurationElement(key: "prefix_pattern") private(set) var prefixPattern = "" @ConfigurationElement(key: "suffix_pattern") @@ -17,3 +19,15 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "require_fully_qualified_names") private(set) var requireFullyQualifiedNames = false } + +extension FileNameConfiguration { + func shouldExclude(filePath: String) -> Bool { + let fileName = filePath.bridge().lastPathComponent + if excluded.contains(fileName) { + return true + } + return excludedPaths.contains { + $0.regex.firstMatch(in: filePath, range: filePath.fullNSRange) != nil + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift index 750b1793ef..2600c45ab6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift @@ -15,6 +15,8 @@ struct NestingConfiguration: RuleConfiguration { private(set) var alwaysAllowOneTypeInFunctions = false @ConfigurationElement(key: "ignore_typealiases_and_associatedtypes") private(set) var ignoreTypealiasesAndAssociatedtypes = false + @ConfigurationElement(key: "ignore_coding_keys") + private(set) var ignoreCodingKeys = false func severity(with config: Severity, for level: Int) -> ViolationSeverity? { if let error = config.error, level > error { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift new file mode 100644 index 0000000000..ef21f1ac39 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift @@ -0,0 +1,12 @@ +import SwiftLintCore + +@AutoConfigParser +struct NonOptionalStringDataConversionConfiguration: SeverityBasedRuleConfiguration { + // swiftlint:disable:previous type_name + typealias Parent = NonOptionalStringDataConversionRule + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "include_variables") + private(set) var includeVariables = false +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift new file mode 100644 index 0000000000..d9d18f6632 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift @@ -0,0 +1,26 @@ +import SwiftLintCore + +@AutoConfigParser +struct OneDeclarationPerFileConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = OneDeclarationPerFileRule + + @AcceptableByConfigurationElement + enum IgnoredType: String, CaseIterable { + case `actor` + case `class` + case `enum` + case `protocol` + case `struct` + static let all = Set(allCases) + } + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + + @ConfigurationElement(key: "ignored_types") + private(set) var ignoredTypes: [IgnoredType] = [] + + var allowedTypes: Set { + Set(self.ignoredTypes) + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift index 1c4a3f12bc..8316b97980 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift @@ -45,7 +45,7 @@ struct OverriddenSuperCallConfiguration: SeverityBasedRuleConfiguration { var resolvedMethodNames: [String] { var names: [String] = [] - if included.contains("*") && !excluded.contains("*") { + if included.contains("*"), !excluded.contains("*") { names += Self.defaultIncluded } names += included.filter { $0 != "*" } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift index cd06da7cf1..a828f7ed92 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift @@ -24,7 +24,7 @@ struct ProhibitedSuperConfiguration: SeverityBasedRuleConfiguration { var resolvedMethodNames: [String] { var names = [String]() - if included.contains("*") && !excluded.contains("*") { + if included.contains("*"), !excluded.contains("*") { names += Self.methodNames } names += included.filter { $0 != "*" } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift new file mode 100644 index 0000000000..c8454724f1 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift @@ -0,0 +1,25 @@ +import SwiftLintCore + +@AcceptableByConfigurationElement +enum TypeBodyLengthCheckType: String, CaseIterable, Comparable { + case `actor` = "actor" + case `class` = "class" + case `enum` = "enum" + case `extension` = "extension" + case `protocol` = "protocol" + case `struct` = "struct" + + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +@AutoConfigParser +struct TypeBodyLengthConfiguration: SeverityLevelsBasedRuleConfiguration { + typealias Parent = TypeBodyLengthRule + + @ConfigurationElement(inline: true) + private(set) var severityConfiguration = SeverityLevelsConfiguration(warning: 250, error: 350) + @ConfigurationElement(key: "excluded_types") + private(set) var excludedTypes = Set([.extension, .protocol]) +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift index aed18138ff..d58dce5cd3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift @@ -10,9 +10,9 @@ struct TransitiveModuleConfiguration: Equatable, AcceptableByConfi 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] + Set(configurationDict.keys) == ["module", "allowed_transitive_imports"], + let importedModule = configurationDict["module"] as? String, + let transitivelyImportedModules = configurationDict["allowed_transitive_imports"] as? [String] else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift index 2bc989d73f..676a524510 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift @@ -4,8 +4,17 @@ import SwiftLintCore struct VerticalWhitespaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = VerticalWhitespaceRule + static let defaultDescriptionReason = "Limit vertical whitespace to a single empty line" + @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) @ConfigurationElement(key: "max_empty_lines") private(set) var maxEmptyLines = 1 + + var configuredDescriptionReason: String { + guard maxEmptyLines == 1 else { + return "Limit vertical whitespace to maximum \(maxEmptyLines) empty lines" + } + return Self.defaultDescriptionReason + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift index be3ad3b7dc..bcdb3330c0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift @@ -86,7 +86,7 @@ private extension AttributesRule { } let hasMultipleNewlines = node.children(viewMode: .sourceAccurate).enumerated().contains { index, element in - if index > 0 && element.leadingTrivia.hasMultipleNewlines == true { + if index > 0, element.leadingTrivia.hasMultipleNewlines == true { return true } return element.trailingTrivia.hasMultipleNewlines == true @@ -162,7 +162,7 @@ private struct RuleHelper { linesWithAttributes.contains(attributeStartLine) linesWithAttributes.insert(attributeStartLine) if hasViolation { - if attributesWithArgumentsAlwaysOnNewLine && shouldBeOnSameLine { + if attributesWithArgumentsAlwaysOnNewLine, shouldBeOnSameLine { return .argumentsAlwaysOnNewLineViolation } return .violation diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift index 9782d141af..9c87e4e9f6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift @@ -1,7 +1,8 @@ -import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax -struct ClosureEndIndentationRule: Rule, OptInRule { +@SwiftSyntaxRule(correctable: true, optIn: true) +struct ClosureEndIndentationRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -13,334 +14,183 @@ struct ClosureEndIndentationRule: Rule, OptInRule { triggeringExamples: ClosureEndIndentationRuleExamples.triggeringExamples, corrections: ClosureEndIndentationRuleExamples.corrections ) - - fileprivate static let notWhitespace = regex("[^\\s]") - - func validate(file: SwiftLintFile) -> [StyleViolation] { - violations(in: file).map { violation in - styleViolation(for: violation, in: file) - } - } - - private func styleViolation(for violation: Violation, in file: SwiftLintFile) -> StyleViolation { - let reason = "Closure end should have the same indentation as the line that started it; " + - "expected \(violation.indentationRanges.expected.length), " + - "got \(violation.indentationRanges.actual.length)" - - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: violation.endOffset), - reason: reason) - } } -extension ClosureEndIndentationRule: CorrectableRule { - func correct(file: SwiftLintFile) -> Int { - let allViolations = violations(in: file).reversed().filter { violation in - guard let nsRange = file.stringView.byteRangeToNSRange(violation.range) else { - return false +private extension ClosureEndIndentationRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ClosureExprSyntax) { + // Get locations of opening and closing braces + let leftBraceLocation = locationConverter.location( + for: node.leftBrace.positionAfterSkippingLeadingTrivia + ) + let rightBracePositionAfterTrivia = node.rightBrace.positionAfterSkippingLeadingTrivia + let rightBraceLocation = locationConverter.location(for: rightBracePositionAfterTrivia) + + // Only interested in multi-line closures + let leftBraceLine = leftBraceLocation.line + let rightBraceLine = rightBraceLocation.line + guard rightBraceLine > leftBraceLine else { + return } - return file.ruleEnabled(violatingRanges: [nsRange], for: self).isNotEmpty - } - - guard allViolations.isNotEmpty else { - return 0 - } - - var correctedContents = file.contents - var correctedLocations: [Int] = [] - - let actualLookup = actualViolationLookup(for: allViolations) - - for violation in allViolations { - let expected = actualLookup(violation).indentationRanges.expected - let actual = violation.indentationRanges.actual - if correct(contents: &correctedContents, expected: expected, actual: actual) { - correctedLocations.append(actual.location) + // Find the position that the closing brace should align with + guard let anchorPosition = findAnchorPosition(for: node) else { + return } - } - - var numberOfCorrections = correctedLocations.count - file.write(correctedContents) - - // Re-correct to catch cascading indentation from the first round. - numberOfCorrections += correct(file: file) - - return numberOfCorrections - } - - private func correct(contents: inout String, expected: NSRange, actual: NSRange) -> Bool { - guard let actualIndices = contents.nsrangeToIndexRange(actual) else { - return false - } - - let regex = Self.notWhitespace - if regex.firstMatch(in: contents, options: [], range: actual) != nil { - var correction = "\n" - correction.append(contents.substring(from: expected.location, length: expected.length)) - contents.insert(contentsOf: correction, at: actualIndices.upperBound) - } else { - let correction = contents.substring(from: expected.location, length: expected.length) - contents = contents.replacingCharacters(in: actualIndices, with: correction) - } - - return true - } - - private func actualViolationLookup(for violations: [Violation]) -> (Violation) -> Violation { - let lookup = violations.reduce(into: [NSRange: Violation](), { result, violation in - result[violation.indentationRanges.actual] = violation - }) - - func actualViolation(for violation: Violation) -> Violation { - guard let actual = lookup[violation.indentationRanges.expected] else { return violation } - return actualViolation(for: actual) - } - - return actualViolation - } -} - -extension ClosureEndIndentationRule { - fileprivate struct Violation { - var indentationRanges: (expected: NSRange, actual: NSRange) - var endOffset: ByteCount - var range: ByteRange - } - - fileprivate func violations(in file: SwiftLintFile) -> [Violation] { - 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, - dictionary: SourceKittenDictionary) -> [Violation] { - guard kind == .call else { - return [] - } - - var violations = validateArguments(in: file, dictionary: dictionary) - - if let callViolation = validateCall(in: file, dictionary: dictionary) { - violations.append(callViolation) - } - - return violations - } - - private func hasTrailingClosure(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> Bool { - guard - let byteRange = dictionary.byteRange, - let text = file.stringView.substringWithByteRange(byteRange) - else { - return false - } - return !text.hasSuffix(")") - } - - private func validateCall(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> Violation? { - let contents = file.stringView - guard let offset = dictionary.offset, - let length = dictionary.length, - let bodyLength = dictionary.bodyLength, - let nameOffset = dictionary.nameOffset, - let nameLength = dictionary.nameLength, - bodyLength > 0, - case let endOffset = offset + length - 1, - case let closingBraceByteRange = ByteRange(location: endOffset, length: 1), - contents.substringWithByteRange(closingBraceByteRange) == "}", - let startOffset = startOffset(forDictionary: dictionary, file: file), - let (startLine, _) = contents.lineAndCharacter(forByteOffset: startOffset), - let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), - case let nameEndPosition = nameOffset + nameLength, - let (bodyOffsetLine, _) = contents.lineAndCharacter(forByteOffset: nameEndPosition), - startLine != endLine, bodyOffsetLine != endLine, - !containsSingleLineClosure(dictionary: dictionary, endPosition: endOffset, file: file) - else { - return nil - } - - let range = file.lines[startLine - 1].range - let regex = Self.notWhitespace - let actual = endPosition - 1 - guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range, - case let expected = match.location - range.location, - expected != actual - else { - return nil - } - - var expectedRange = range - expectedRange.length = expected - - var actualRange = file.lines[endLine - 1].range - actualRange.length = actual - - return Violation(indentationRanges: (expected: expectedRange, actual: actualRange), - endOffset: endOffset, - range: ByteRange(location: offset, length: length)) - } - - private func validateArguments(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> [Violation] { - guard isFirstArgumentOnNewline(dictionary, file: file) else { - return [] - } - - var closureArguments = filterClosureArguments(dictionary.enclosedArguments, file: file) - - if hasTrailingClosure(in: file, dictionary: dictionary), closureArguments.isNotEmpty { - closureArguments.removeLast() - } - - return closureArguments.compactMap { dictionary in - validateClosureArgument(in: file, dictionary: dictionary) - } - } - - private func validateClosureArgument(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> Violation? { - let contents = file.stringView - guard let offset = dictionary.offset, - let length = dictionary.length, - let bodyLength = dictionary.bodyLength, - let nameOffset = dictionary.nameOffset, - let nameLength = dictionary.nameLength, - bodyLength > 0, - case let endOffset = offset + length - 1, - case let closingBraceByteRange = ByteRange(location: endOffset, length: 1), - contents.substringWithByteRange(closingBraceByteRange) == "}", - let startOffset = dictionary.offset, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: startOffset), - let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), - case let nameEndPosition = nameOffset + nameLength, - let (bodyOffsetLine, _) = contents.lineAndCharacter(forByteOffset: nameEndPosition), - startLine != endLine, bodyOffsetLine != endLine, - !isSingleLineClosure(dictionary: dictionary, endPosition: endOffset, file: file) - else { - return nil + let anchorLocation = locationConverter.location(for: anchorPosition) + let anchorLineNumber = anchorLocation.line + + // Calculate expected indentation + let expectedIndentationColumn = getFirstNonWhitespaceColumn(onLine: anchorLineNumber) - 1 + + // Calculate actual indentation of the closing brace + let actualIndentationColumn = rightBraceLocation.column - 1 + + if actualIndentationColumn != expectedIndentationColumn { + // Check if there's leading trivia on the right brace that ends with a newline and only whitespace + // after it. + let leadingTriviaEndsWithNewline = node.rightBrace.leadingTrivia + .reversed() + .drop(while: \.isSpaceOrTab) + .first + .map(\.isNewline) ?? false + + let (correctionStartPosition, correctionPartBeforeIndentation) = + if leadingTriviaEndsWithNewline { + // If there's already a newline, we just need to fix the indentation. + // The range to replace is the trivia before the brace. + ( + locationConverter.position(ofLine: rightBraceLocation.line, column: 1), + "" + ) + } else { + // If no newline, we need to add one. The replacement will be inserted + // after the previous token and before the closing brace. + ( + node.rightBrace.positionAfterSkippingLeadingTrivia, + "\n" + ) + } + + let reason = "expected \(expectedIndentationColumn), got \(actualIndentationColumn)" + violations.append( + ReasonedRuleViolation( + position: node.rightBrace.positionAfterSkippingLeadingTrivia, + reason: reason, + severity: configuration.severity, + correction: .init( + start: correctionStartPosition, + end: node.rightBrace.positionAfterSkippingLeadingTrivia, + replacement: correctionPartBeforeIndentation + + String(repeating: " ", count: max(0, expectedIndentationColumn)) + ) + ) + ) + } } - let range = file.lines[startLine - 1].range - let regex = Self.notWhitespace - let actual = endPosition - 1 - guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range, - case let expected = match.location - range.location, - expected != actual - else { - return nil - } + /// Finds the position of a token that the closure's closing brace should be aligned with. + private func findAnchorPosition(for closureNode: ClosureExprSyntax) -> AbsolutePosition? { + guard let parent = closureNode.parent else { + return nil + } - var expectedRange = range - expectedRange.length = expected + // Case: Trailing closure. e.g., `list.map { ... }` + if let functionCall = parent.as(FunctionCallExprSyntax.self), + closureNode.id == functionCall.trailingClosure?.id { + return anchor(for: ExprSyntax(functionCall)) + } - var actualRange = file.lines[endLine - 1].range - actualRange.length = actual + // Case: Closure as a labeled argument. e.g., `function(label: { ... })` + if let labeledExpr = parent.as(LabeledExprSyntax.self) { + // Check if this is part of a function call where the first argument is on a new line + if let argList = labeledExpr.parent?.as(LabeledExprListSyntax.self), + let functionCall = argList.parent?.as(FunctionCallExprSyntax.self), + let firstArg = argList.first, + let leftParen = functionCall.leftParen { + // Get the location of the opening paren and first argument + let leftParenLocation = locationConverter.location( + for: leftParen.positionAfterSkippingLeadingTrivia + ) + let firstArgLocation = locationConverter.location( + for: firstArg.positionAfterSkippingLeadingTrivia + ) + + // If first argument is on the same line as the opening paren, don't apply the rule + if leftParenLocation.line == firstArgLocation.line { + return nil + } + } + + // The anchor is the start of the argument expression (including the label). + if let label = labeledExpr.label { + return label.positionAfterSkippingLeadingTrivia + } + return labeledExpr.positionAfterSkippingLeadingTrivia + } - return Violation(indentationRanges: (expected: expectedRange, actual: actualRange), - endOffset: endOffset, - range: ByteRange(location: offset, length: length)) - } + // Case: Multiple trailing closures. e.g., `function { ... } another: { ... }` + if let multipleTrailingClosure = parent.as(MultipleTrailingClosureElementSyntax.self) { + // The anchor is the label of the specific trailing closure. + return multipleTrailingClosure.label.positionAfterSkippingLeadingTrivia + } - private func startOffset(forDictionary dictionary: SourceKittenDictionary, file: SwiftLintFile) -> ByteCount? { - guard let nameByteRange = dictionary.nameByteRange else { - return nil - } + // For closures on new lines after function calls + if let exprList = parent.as(ExprListSyntax.self), + exprList.count == 1, + exprList.parent?.as(FunctionCallExprSyntax.self) != nil { + // This is a closure on its own line after a function call like: + // foo(abc, 123) + // { _ in } + return closureNode.positionAfterSkippingLeadingTrivia + } - let newLineRegex = regex("\n(\\s*\\}?\\.)") - let contents = file.stringView - guard let range = contents.byteRangeToNSRange(nameByteRange), - let match = newLineRegex.matches(in: file.contents, options: [], range: range).last?.range(at: 1), - let methodByteRange = contents.NSRangeToByteRange(start: match.location, length: match.length) - else { - return nameByteRange.location + // Fallback for other cases (e.g., closure in an array literal). + // The anchor is the start of the parent syntax node. + return closureNode.positionAfterSkippingLeadingTrivia } - return methodByteRange.location - } + /// Recursively traverses a chain of expressions (e.g., member access or function calls) + /// to find the token that begins the statement. This is the token that the closure's + /// closing brace should ultimately be aligned with. + /// - Parameter expression: The expression to find the anchor for. + /// - Returns: The absolute position of the anchor token. + private func anchor(for expression: ExprSyntax) -> AbsolutePosition { + if let memberAccess = expression.as(MemberAccessExprSyntax.self), let base = memberAccess.base { + let baseAnchor = anchor(for: base) - private func isSingleLineClosure(dictionary: SourceKittenDictionary, - endPosition: ByteCount, - file: SwiftLintFile) -> Bool { - let contents = file.stringView + let memberStartPosition = memberAccess.period.positionAfterSkippingLeadingTrivia + let baseEndPosition = base.endPositionBeforeTrailingTrivia - guard let start = dictionary.bodyOffset, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: start), - let (endLine, _) = contents.lineAndCharacter(forByteOffset: endPosition) else { - return false - } - - return startLine == endLine - } + let memberStartLocation = locationConverter.location(for: memberStartPosition) + let baseEndLocation = locationConverter.location(for: baseEndPosition) - private func containsSingleLineClosure(dictionary: SourceKittenDictionary, - endPosition: ByteCount, - file: SwiftLintFile) -> Bool { - let contents = file.stringView - - guard let closure = trailingClosure(dictionary: dictionary, file: file), - let start = closure.bodyOffset, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: start), - let (endLine, _) = contents.lineAndCharacter(forByteOffset: endPosition) else { - return false - } + if memberStartLocation.line > baseEndLocation.line { + return memberStartPosition + } - return startLine == endLine - } + return baseAnchor + } - private func trailingClosure(dictionary: SourceKittenDictionary, - file: SwiftLintFile) -> SourceKittenDictionary? { - let arguments = dictionary.enclosedArguments - let closureArguments = filterClosureArguments(arguments, file: file) + if let functionCall = expression.as(FunctionCallExprSyntax.self) { + return anchor(for: functionCall.calledExpression) + } - if closureArguments.count == 1, - closureArguments.last?.offset == arguments.last?.offset { - return closureArguments.last + return expression.positionAfterSkippingLeadingTrivia } - return nil - } - - private func filterClosureArguments(_ arguments: [SourceKittenDictionary], - file: SwiftLintFile) -> [SourceKittenDictionary] { - 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, - match.location == range.location - else { - return false + /// Calculates the column of the first non-whitespace character on a given line. + private func getFirstNonWhitespaceColumn(onLine lineNumber: Int) -> Int { + guard lineNumber > 0, lineNumber <= file.lines.count else { + return 1 // Should not happen } + let lineContent = file.lines[lineNumber - 1].content - return true - } - } - - private func isFirstArgumentOnNewline(_ dictionary: SourceKittenDictionary, - file: SwiftLintFile) -> Bool { - guard - let nameOffset = dictionary.nameOffset, - let nameLength = dictionary.nameLength, - let firstArgument = dictionary.enclosedArguments.first, - let firstArgumentOffset = firstArgument.offset, - case let offset = nameOffset + nameLength, - case let length = firstArgumentOffset - offset, - length > 0, - case let byteRange = ByteRange(location: offset, length: length), - let range = file.stringView.byteRangeToNSRange(byteRange), - let match = regex("\\(\\s*\\n\\s*").firstMatch(in: file.contents, options: [], range: range)?.range, - match.location == range.location - else { - return false + if let firstCharIndex = lineContent.firstIndex(where: { !$0.isWhitespace }) { + return lineContent.distance(from: lineContent.startIndex, to: firstCharIndex) + 1 + } + return 1 // Empty or whitespace-only line } - - return true } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift index 45c83d6d34..56484d83eb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift @@ -1,173 +1,251 @@ internal struct ClosureEndIndentationRuleExamples { static let nonTriggeringExamples = [ - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - " }\n"), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + } + """), Example("[1, 2].map { $0 + 1 }\n"), - Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - "}.flatMap { command in\n" + - " return command.expand()\n" + - "}\n"), - Example("foo(foo: bar,\n" + - " options: baz) { _ in }\n"), - Example("someReallyLongProperty.chainingWithAnotherProperty\n" + - " .foo { _ in }"), - Example("foo(abc, 123)\n" + - "{ _ in }\n"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(parameter: param,\n" + - " closure: { x in\n" + - " print(x)\n" + - "})"), - Example("function(parameter: param, closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), + Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + }.flatMap { command in + return command.expand() + } + """), + Example(""" + foo(foo: bar, + options: baz) { _ in } + """), + Example(""" + someReallyLongProperty.chainingWithAnotherProperty + .foo { _ in } + """), + Example(""" + foo(abc, 123) + { _ in } + """), + Example(""" + function( + closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function(parameter: param, + closure: { x in + print(x) + }) + """), + Example(""" + function(parameter: param, closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), Example("(-variable).foo()"), ] static let triggeringExamples = [ - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - "↓}\n"), - Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - " ↓}.flatMap { command in\n" + - " return command.expand()\n" + - "↓}\n"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓},\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - "↓})"), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + ↓} + """), + Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + ↓}.flatMap { command in + return command.expand() + } + """), + Example(""" + function( + closure: { x in + print(x) + ↓}, + anotherClosure: { y in + print(y) + ↓}) + """), ] static let corrections = [ - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - "↓}\n"): Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - " }\n"), - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - "↓}.another { x in\n" + - " print(x)\n" + - "↓}.yetAnother { y in\n" + - " print(y)\n" + - "↓})"): Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - " }.another { x in\n" + - " print(x)\n" + - " }.yetAnother { y in\n" + - " print(y)\n" + - " })"), - Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - "↓ }.flatMap { command in\n" + - " return command.expand()\n" + - "↓}\n"): Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - "}.flatMap { command in\n" + - " return command.expand()\n" + - "}\n"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓})"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - "↓ print(x) })"): Example("function(\n" + - " closure: { x in\n" + - " print(x) \n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - "↓ab})"): Example("function(\n" + - " closure: { x in\n" + - "ab\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓},\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓ },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓ab},\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "ab\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - "↓ print(x) },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x) \n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓}, anotherClosure: { y in\n" + - " print(y)\n" + - "↓})"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " }, anotherClosure: { y in\n" + - " print(y)\n" + - " })"), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + ↓} + """): Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + } + """), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + ↓}.another { x in + print(x) + ↓}.yetAnother { y in + print(y) + ↓}) + """): Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + }.another { x in + print(x) + }.yetAnother { y in + print(y) + }) + """), + Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + ↓ }.flatMap { command in + return command.expand() + ↓} + """): Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + }.flatMap { command in + return command.expand() + } + """), + Example(""" + function( + closure: { x in + print(x) + ↓}) + """): Example(""" + function( + closure: { x in + print(x) + }) + """), + Example(""" + function( + closure: { x in + ↓ print(x) }) + """): Example(""" + function( + closure: { x in + print(x) \("") + }) + """), + Example(""" + function( + closure: { x in + ↓ab}) + """): Example(""" + function( + closure: { x in + ab + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓}, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + print(x) // comment + // comment + ↓ }, + anotherClosure: { y in + print(y) + /* comment */}) + """): Example(""" + function( + closure: { x in + print(x) // comment + // comment + }, + anotherClosure: { y in + print(y) + /* comment */ + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓ab}, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) + ab + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + ↓ print(x) }, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) \("") + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓}, anotherClosure: { y in + print(y) + ↓}) + """): Example(""" + function( + closure: { x in + print(x) + }, anotherClosure: { y in + print(y) + }) + """), + Example(""" + f { + // do something + ↓} + """): Example(""" + f { + // do something + } + """), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift index eea922a5cf..7df46cf426 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift @@ -136,7 +136,7 @@ private extension TokenSyntax { return true } if let previousToken = previousToken(viewMode: .sourceAccurate), - case .spaces(1) = Array(previousToken.trailingTrivia).last { + case .spaces(1) = Array(previousToken.trailingTrivia).last { return true } return false @@ -147,7 +147,7 @@ private extension TokenSyntax { return true } if let nextToken = nextToken(viewMode: .sourceAccurate), - case .spaces(1) = nextToken.leadingTrivia.first { + case .spaces(1) = nextToken.leadingTrivia.first { return true } return false @@ -197,7 +197,7 @@ private extension TokenSyntax { return true } if let nextToken = nextToken(viewMode: .sourceAccurate), - allowedKinds.contains(nextToken.tokenKind) { + allowedKinds.contains(nextToken.tokenKind) { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift index 893e912a3c..a4a995bfeb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift @@ -26,7 +26,7 @@ private extension CollectionAlignmentRule { override func visitPost(_ node: DictionaryElementListSyntax) { let locations = node.map { element in let position = configuration.alignColons ? element.colon.positionAfterSkippingLeadingTrivia : - element.key.positionAfterSkippingLeadingTrivia + element.key.positionAfterSkippingLeadingTrivia let location = locationConverter.location(for: position) let graphemeColumn: Int diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift index aaf84fe061..38ce4e949a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift @@ -52,19 +52,19 @@ struct ColonRule: SubstitutionCorrectableRule, SourceKitFreeRule { return nil } - if previous.trailingTrivia.isNotEmpty && !previous.trailingTrivia.containsBlockComments() { + if previous.trailingTrivia.isNotEmpty, !previous.trailingTrivia.containsBlockComments() { let start = ByteCount(previous.endPositionBeforeTrailingTrivia) let end = ByteCount(current.endPosition) return ByteRange(location: start, length: end - start) } - if current.trailingTrivia != [.spaces(1)] && !next.leadingTrivia.containsNewlines() { + if current.trailingTrivia != [.spaces(1)], !next.leadingTrivia.containsNewlines() { if case .spaces(1) = current.trailingTrivia.first { return nil } let flexibleRightSpacing = configuration.flexibleRightSpacing || caseStatementPositions.contains(current.position) - if flexibleRightSpacing && current.trailingTrivia.isNotEmpty { + if flexibleRightSpacing, current.trailingTrivia.isNotEmpty { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift index 30af47edc1..03f5967b4f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift @@ -3,7 +3,7 @@ import SourceKittenFramework import SwiftSyntax struct CommaInheritanceRule: OptInRule, SubstitutionCorrectableRule, - SourceKitFreeRule { + SourceKitFreeRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift index b91f0cb054..752396565d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift @@ -105,7 +105,7 @@ struct CommaRule: CorrectableRule, SourceKitFreeRule { if current.tokenKind != .comma { return nil } - if !previous.trailingTrivia.isEmpty && !previous.trailingTrivia.containsBlockComments() { + if !previous.trailingTrivia.isEmpty, !previous.trailingTrivia.containsBlockComments() { let start = ByteCount(previous.endPositionBeforeTrailingTrivia) let end = ByteCount(current.endPosition) let nextIsNewline = next.leadingTrivia.containsNewlines() diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift index 10dcabc645..c1635796f5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift @@ -80,7 +80,7 @@ private extension BracedSyntax { } if let closure = `as`(ClosureExprSyntax.self), closure.keyPathInParent == \FunctionCallExprSyntax.trailingClosure { - return closure.leftBrace.previousIndentationDecidingToken + return closure.leftBrace.previousIndentationDecidingToken } if let closureLabel = parent?.as(MultipleTrailingClosureElementSyntax.self)?.label { return closureLabel.previousIndentationDecidingToken diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift index 1b408096e4..6acd14aa85 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift @@ -175,7 +175,7 @@ private extension ExprSyntax { return nil } - private func containsTrailingClosure(_ node: Syntax) -> Bool { + private func containsTrailingClosure(_ node: Syntax) -> Bool { switch node.as(SyntaxEnum.self) { case .functionCallExpr(let node): node.trailingClosure != nil || node.calledExpression.is(ClosureExprSyntax.self) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift index 02e8e37cfa..a85c4a5a45 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift @@ -119,10 +119,10 @@ private extension StringView { func recursiveByteOffsets(_ dict: [String: Any]) -> [ByteCount] { let cur: [ByteCount] if let line = dict["key.line"] as? Int64, - let column = dict["key.column"] as? Int64, - let kindString = dict["key.kind"] as? String, - kindsToFind.contains(kindString), - let offset = byteOffset(forLine: line, bytePosition: column) { + let column = dict["key.column"] as? Int64, + let kindString = dict["key.kind"] as? String, + kindsToFind.contains(kindString), + let offset = byteOffset(forLine: line, bytePosition: column) { cur = [offset] } else { cur = [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift index 866fa84269..d19fb09ddc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift @@ -1,7 +1,9 @@ import Foundation import SourceKittenFramework +import SwiftSyntax -struct FileHeaderRule: OptInRule { +@SwiftSyntaxRule(optIn: true) +struct FileHeaderRule: Rule { var configuration = FileHeaderConfiguration() static let description = RuleDescription( @@ -30,80 +32,200 @@ struct FileHeaderRule: OptInRule { """), ].skipWrappingInCommentTests() ) +} + +private struct ProcessTriviaResult { + let foundNonComment: Bool +} + +private extension FileHeaderRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + let headerRange = collectHeaderComments(from: node) + + let requiredRegex = configuration.requiredRegex(for: file) + + // If no header comments found + guard let headerRange else { + if requiredRegex != nil { + let violationPosition = node.shebang?.endPosition ?? node.position + violations.append(ReasonedRuleViolation( + position: violationPosition, + reason: requiredReason() + )) + } + return .skipChildren + } + + // Extract header content + guard let headerContent = extractHeaderContent(from: headerRange) else { + return .skipChildren + } - func validate(file: SwiftLintFile) -> [StyleViolation] { - var firstToken: SwiftLintSyntaxToken? - var lastToken: SwiftLintSyntaxToken? - var firstNonCommentToken: SwiftLintSyntaxToken? - - for token in file.syntaxTokensByLines.lazy.joined() { - guard let kind = token.kind, kind.isFileHeaderKind else { - // found a token that is not a comment, which means it's not the top of the file - // so we can just skip the remaining tokens - firstNonCommentToken = token - break + // Check patterns + checkForbiddenPattern(in: headerContent, startingAt: headerRange.start) + checkRequiredPattern(requiredRegex, in: headerContent, startingAt: headerRange.start) + + return .skipChildren + } + + private func collectHeaderComments( + from node: SourceFileSyntax + ) -> (start: AbsolutePosition, end: AbsolutePosition)? { + var firstHeaderCommentStart: AbsolutePosition? + var lastHeaderCommentEnd: AbsolutePosition? + + // Skip past shebang if present + var currentPosition = node.position + if let shebang = node.shebang { + currentPosition = shebang.endPosition } - // skip SwiftLint commands - guard !isSwiftLintCommand(token: token, file: file) else { - continue + // Collect header comments from tokens' trivia + for token in node.tokens(viewMode: .sourceAccurate) { + // Skip tokens before the start position (e.g., shebang) + if token.endPosition <= currentPosition { + continue + } + + let triviaResult = processTrivia( + token.leadingTrivia, + startingAt: ¤tPosition, + firstStart: &firstHeaderCommentStart, + lastEnd: &lastHeaderCommentEnd + ) + + if triviaResult.foundNonComment || token.tokenKind != .endOfFile { + break + } + + // Update current position past the token + currentPosition = token.endPositionBeforeTrailingTrivia + + // Process trailing trivia if it's EOF + if token.tokenKind == .endOfFile { + _ = processTrivia(token.trailingTrivia, + startingAt: ¤tPosition, + firstStart: &firstHeaderCommentStart, + lastEnd: &lastHeaderCommentEnd) + } } - if firstToken == nil { - firstToken = token + guard let start = firstHeaderCommentStart, + let end = lastHeaderCommentEnd, + start < end else { + return nil } - lastToken = token + + return (start: start, end: end) } - let requiredRegex = configuration.requiredRegex(for: file) + private func processTrivia(_ trivia: Trivia, + startingAt currentPosition: inout AbsolutePosition, + firstStart: inout AbsolutePosition?, + lastEnd: inout AbsolutePosition?) -> ProcessTriviaResult { + for piece in trivia { + let pieceStart = currentPosition + currentPosition += piece.sourceLength - var violationsOffsets = [Int]() - if let firstToken, let lastToken { - let start = firstToken.offset - let length = lastToken.offset + lastToken.length - firstToken.offset - let byteRange = ByteRange(location: start, length: length) - guard let range = file.stringView.byteRangeToNSRange(byteRange) else { - return [] + if isSwiftLintCommand(piece: piece) { + continue + } + + if piece.isComment, !piece.isDocComment { + if firstStart == nil { + firstStart = pieceStart + } + lastEnd = currentPosition + } else if !piece.isWhitespace { + return ProcessTriviaResult(foundNonComment: true) + } } + return ProcessTriviaResult(foundNonComment: false) + } + + private func extractHeaderContent(from range: (start: AbsolutePosition, end: AbsolutePosition)) -> String? { + let headerByteRange = ByteRange( + location: ByteCount(range.start.utf8Offset), + length: ByteCount(range.end.utf8Offset - range.start.utf8Offset) + ) + + return file.stringView.substringWithByteRange(headerByteRange) + } - if let regex = configuration.forbiddenRegex(for: file), - let firstMatch = regex.matches(in: file.contents, options: [], range: range).first { - violationsOffsets.append(firstMatch.range.location) + private func checkForbiddenPattern(in headerContent: String, startingAt headerStart: AbsolutePosition) { + guard + let forbiddenRegex = configuration.forbiddenRegex(for: file), + let firstMatch = forbiddenRegex.firstMatch( + in: headerContent, + options: [], + range: headerContent.fullNSRange + ) + else { + return } - if let regex = requiredRegex, - case let matches = regex.matches(in: file.contents, options: [], range: range), - matches.isEmpty { - violationsOffsets.append(file.stringView.location(fromByteOffset: start)) + // Calculate violation position + let matchLocationUTF16 = firstMatch.range.location + let headerPrefix = String(headerContent.utf16.prefix(matchLocationUTF16)) ?? "" + let utf8OffsetInHeader = headerPrefix.utf8.count + let violationPosition = AbsolutePosition(utf8Offset: headerStart.utf8Offset + utf8OffsetInHeader) + + violations.append(ReasonedRuleViolation( + position: violationPosition, + reason: forbiddenReason() + )) + } + + private func checkRequiredPattern(_ requiredRegex: NSRegularExpression?, + in headerContent: String, + startingAt headerStart: AbsolutePosition) { + guard + let requiredRegex, + requiredRegex.firstMatch(in: headerContent, options: [], range: headerContent.fullNSRange) == nil + else { + return } - } else if requiredRegex != nil { - let location = firstNonCommentToken.map { - Location(file: file, byteOffset: $0.offset) - } ?? Location(file: file.path, line: 1) - return [makeViolation(at: location)] + + violations.append(ReasonedRuleViolation( + position: headerStart, + reason: requiredReason() + )) } - return violationsOffsets.map { makeViolation(at: Location(file: file, characterOffset: $0)) } - } + private func isSwiftLintCommand(piece: TriviaPiece) -> Bool { + guard let text = piece.commentText else { return false } + return text.contains("swiftlint:") + } - private func isSwiftLintCommand(token: SwiftLintSyntaxToken, file: SwiftLintFile) -> Bool { - guard let range = file.stringView.byteRangeToNSRange(token.range) else { - return false + private func forbiddenReason() -> String { + "Header comments should be consistent with project patterns" } - return file.commands(in: range).isNotEmpty + private func requiredReason() -> String { + "Header comments should be consistent with project patterns" + } } +} - private func makeViolation(at location: Location) -> StyleViolation { - StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: location, - reason: "Header comments should be consistent with project patterns") +// Helper extensions +private extension TriviaPiece { + var isDocComment: Bool { + switch self { + case .docLineComment, .docBlockComment: + return true + default: + return false + } } -} -private extension SyntaxKind { - var isFileHeaderKind: Bool { - self == .comment || self == .commentURL + var commentText: String? { + switch self { + case .lineComment(let text), .blockComment(let text), + .docLineComment(let text), .docBlockComment(let text): + return text + default: + return nil + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift index 8b4564ea78..ee00bdfa5b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift @@ -146,12 +146,6 @@ private extension IdentifierNameRule { } } -private extension DeclModifierListSyntax { - var staticOrClassModifier: DeclModifierSyntax? { - first { ["static", "class"].contains($0.name.text) } - } -} - private extension IdentifierPatternSyntax { var enclosingVarDecl: VariableDeclSyntax? { let identifierDecl = @@ -171,24 +165,6 @@ private extension IdentifierPatternSyntax { } } -private extension VariableDeclSyntax { - var allDeclaredNames: [String] { - bindings - .map(\.pattern) - .flatMap { pattern -> [String] in - if let id = pattern.as(IdentifierPatternSyntax.self) { - [id.identifier.text] - } else if let tuple = pattern.as(TuplePatternSyntax.self) { - tuple.elements.compactMap { - $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text - } - } else { - [] - } - } - } -} - private enum NamedDeclType: CustomStringConvertible { case function(name: String, resolvedName: String, isPrivate: Bool) case enumElement(name: String) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index 888c9eb01c..77c0d2f5aa 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -61,7 +61,7 @@ struct IndentationWidthRule: OptInRule { // Determine indentation let indentation: Indentation - if tabCount != 0 && spaceCount != 0 { + if tabCount != 0, spaceCount != 0 { // Catch mixed indentation violations.append( StyleViolation( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift index a5e3cd3e8e..a269fde54f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift @@ -40,9 +40,9 @@ struct LeadingWhitespaceRule: CorrectableRule, SourceKitFreeRule { let whitespaceAndNewline = CharacterSet.whitespacesAndNewlines let spaceCount = file.contents.countOfLeadingCharacters(in: whitespaceAndNewline) guard spaceCount > 0, - let firstLineRange = file.lines.first?.range, - file.ruleEnabled(violatingRanges: [firstLineRange], for: self).isNotEmpty else { - return 0 + let firstLineRange = file.lines.first?.range, + file.ruleEnabled(violatingRanges: [firstLineRange], for: self).isNotEmpty else { + return 0 } let indexEnd = file.contents.index( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift index 4c78231ca5..842a6c42c3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift @@ -220,17 +220,17 @@ extension LiteralExpressionEndIndentationRule { let contents = file.stringView guard elements.isNotEmpty, - let offset = dictionary.offset, - let length = dictionary.length, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: offset), - let firstParamOffset = elements[0].offset, - let (firstParamLine, _) = contents.lineAndCharacter(forByteOffset: firstParamOffset), - startLine != firstParamLine, - let lastParamOffset = elements.last?.offset, - let (lastParamLine, _) = contents.lineAndCharacter(forByteOffset: lastParamOffset), - case let endOffset = offset + length - 1, - let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), - lastParamLine != endLine + let offset = dictionary.offset, + let length = dictionary.length, + let (startLine, _) = contents.lineAndCharacter(forByteOffset: offset), + let firstParamOffset = elements[0].offset, + let (firstParamLine, _) = contents.lineAndCharacter(forByteOffset: firstParamOffset), + startLine != firstParamLine, + let lastParamOffset = elements.last?.offset, + let (lastParamLine, _) = contents.lineAndCharacter(forByteOffset: lastParamOffset), + case let endOffset = offset + length - 1, + let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), + lastParamLine != endLine else { return nil } @@ -239,8 +239,8 @@ extension LiteralExpressionEndIndentationRule { let regex = Self.notWhitespace let actual = endPosition - 1 guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range, - case let expected = match.location - range.location, - expected != actual + case let expected = match.location - range.location, + expected != actual else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift index da7c1a3f0e..20e4124d41 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift @@ -69,6 +69,12 @@ internal struct MultilineArgumentsRuleExamples { print("b") } """), + Example(""" + f( + foo: 1, + bar: false, + ) + """), ] static let triggeringExamples = [ diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift index 2d048013ba..859368544b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift @@ -108,10 +108,9 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { let ranges = callRanges(file: file, kind: kind, dictionary: dictionary) let calls = ranges.compactMap { range -> (dotLine: Int, dotOffset: Int, range: ByteRange)? in - guard - let offset = callDotOffset(file: file, callRange: range), - let line = file.stringView.lineAndCharacter(forCharacterOffset: offset)?.line else { - return nil + guard let offset = callDotOffset(file: file, callRange: range), + let line = file.stringView.lineAndCharacter(forCharacterOffset: offset)?.line else { + return nil } return (dotLine: line, dotOffset: offset, range: range) } @@ -133,11 +132,10 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { private static let whitespaceDotRegex = regex("\\s*\\.") private func callDotOffset(file: SwiftLintFile, callRange: ByteRange) -> Int? { - guard - let range = file.stringView.byteRangeToNSRange(callRange), - case let regex = Self.whitespaceDotRegex, - let match = regex.matches(in: file.contents, options: [], range: range).last?.range else { - return nil + guard let range = file.stringView.byteRangeToNSRange(callRange), + case let regex = Self.whitespaceDotRegex, + let match = regex.matches(in: file.contents, options: [], range: range).last?.range else { + return nil } return match.location + match.length - 1 } @@ -145,11 +143,10 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { private static let newlineWhitespaceDotRegex = regex("\\n\\s*\\.") private func callHasLeadingNewline(file: SwiftLintFile, callRange: ByteRange) -> Bool { - guard - let range = file.stringView.byteRangeToNSRange(callRange), - case let regex = Self.newlineWhitespaceDotRegex, - regex.firstMatch(in: file.contents, options: [], range: range) != nil else { - return false + guard let range = file.stringView.byteRangeToNSRange(callRange), + case let regex = Self.newlineWhitespaceDotRegex, + regex.firstMatch(in: file.contents, options: [], range: range) != nil else { + return false } return true } @@ -189,17 +186,15 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { call: SourceKittenDictionary, parentName: String, parentNameOffset: ByteCount) -> ByteRange? { - guard - case let contents = file.stringView, - let nameOffset = call.nameOffset, - parentNameOffset == nameOffset, - let nameLength = call.nameLength, - let bodyOffset = call.bodyOffset, - let bodyLength = call.bodyLength, - case let nameByteRange = ByteRange(location: nameOffset, length: nameLength), - let name = contents.substringWithByteRange(nameByteRange), - parentName.starts(with: name) - else { + guard case let contents = file.stringView, + let nameOffset = call.nameOffset, + parentNameOffset == nameOffset, + let nameLength = call.nameLength, + let bodyOffset = call.bodyOffset, + let bodyLength = call.bodyLength, + case let nameByteRange = ByteRange(location: nameOffset, length: nameLength), + let name = contents.substringWithByteRange(nameByteRange), + parentName.starts(with: name) else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift index 51c134d8db..0e360e122b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift @@ -145,7 +145,7 @@ private extension MultilineLiteralBracketsRule { firstElement: (some ExprSyntaxProtocol)?, lastElement: (some ExprSyntaxProtocol)?) { guard let firstElement, let lastElement, - isMultiline(node) else { + isMultiline(node) else { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift index 5021c338c6..a31c9c2746 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift @@ -99,7 +99,7 @@ struct MultilineParametersBracketsRule: OptInRule { // find violations at current level if let kind = substructure.declarationKind, - SwiftDeclarationKind.functionKinds.contains(kind) { + SwiftDeclarationKind.functionKinds.contains(kind) { guard let nameOffset = substructure.nameOffset, let nameLength = substructure.nameLength, @@ -117,7 +117,7 @@ struct MultilineParametersBracketsRule: OptInRule { let declarationNewlineCount = functionName.countOccurrences(of: "\n") let isMultiline = declarationNewlineCount > parametersNewlineCount - if isMultiline && parameters.isNotEmpty { + if isMultiline, parameters.isNotEmpty { if let openingBracketViolation = openingBracketViolation(parameters: parameters, file: file) { violations.append(openingBracketViolation) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift index 7d715364a8..7ff00199d6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift @@ -35,26 +35,32 @@ private extension MultilineParametersRule { } var numberOfParameters = 0 - var linesWithParameters = Set() + var linesWithParameters: Set = [] + var hasMultipleParametersOnSameLine = false for position in parameterPositions { let line = locationConverter.location(for: position).line - linesWithParameters.insert(line) + + if !linesWithParameters.insert(line).inserted { + hasMultipleParametersOnSameLine = true + } + numberOfParameters += 1 } - if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters, - configuration.allowsSingleLine, - numberOfParameters > maxNumberOfSingleLineParameters { - return true - } + if linesWithParameters.count == 1 { + guard configuration.allowsSingleLine else { + return numberOfParameters > 1 + } + + if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters { + return numberOfParameters > maxNumberOfSingleLineParameters + } - guard linesWithParameters.count > (configuration.allowsSingleLine ? 1 : 0), - numberOfParameters != linesWithParameters.count else { return false } - return true + return hasMultipleParametersOnSameLine } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift index 4abcb60e39..250713e900 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift @@ -199,11 +199,20 @@ internal struct MultilineParametersRuleExamples { """, 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) { }", + 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]), + Example(""" + func foo( + param1: Int, + param2: Bool, + param3: [String] + ) { } + """, configuration: ["max_number_of_single_line_parameters": 2]), ] static let triggeringExamples: [Example] = [ @@ -348,7 +357,9 @@ internal struct MultilineParametersRuleExamples { Example(""" func ↓foo(param1: Int, param2: Bool, param3: [String]) { } - """, - configuration: ["max_number_of_single_line_parameters": 3]), + """, configuration: ["max_number_of_single_line_parameters": 3]), + Example(""" + func ↓foo(param1: Int, param2: Bool, param3: [String]) { } + """, configuration: ["max_number_of_single_line_parameters": 2]), ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift index d23963467a..bbeaa6007d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift @@ -104,15 +104,15 @@ private extension NumberSeparatorValidator { func violation(token: TokenSyntax) -> NumberSeparatorViolation? { let content = token.text guard isDecimal(number: content), - !isInValidRanges(number: content) + !isInValidRanges(number: content) else { return nil } let exponential = CharacterSet(charactersIn: "eE") guard case let exponentialComponents = content.components(separatedBy: exponential), - let nonExponential = exponentialComponents.first else { - return nil + let nonExponential = exponentialComponents.first else { + return nil } let components = nonExponential.components(separatedBy: ".") @@ -124,8 +124,8 @@ private extension NumberSeparatorValidator { } guard let integerSubstring = components.first, - case let (valid, expected) = isValid(number: integerSubstring, isFraction: false), - !valid || !validFraction + case let (valid, expected) = isValid(number: integerSubstring, isFraction: false), + !valid || !validFraction else { return nil } @@ -180,7 +180,7 @@ private extension NumberSeparatorValidator { defer { correctComponents.append(String(char)) } guard char.unicodeScalars.allSatisfy(CharacterSet.decimalDigits.contains) else { continue } - if numerals.isMultiple(of: 3) && numerals > 0 && shouldAddSeparators { + if numerals.isMultiple(of: 3), numerals > 0, shouldAddSeparators { correctComponents.append("_") } numerals += 1 diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift index 7205157801..01715915ec 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift @@ -29,7 +29,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ActorDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) { return } @@ -38,7 +38,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ClassDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) { return } @@ -47,7 +47,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: EnumDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) { return } @@ -56,7 +56,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ExtensionDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) { return } @@ -65,7 +65,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ProtocolDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) { return } @@ -74,7 +74,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: StructDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) { return } @@ -85,7 +85,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ForStmtSyntax) { if configuration.ignoreMultilineStatementConditions, - hasMultilinePredecessors(node.body, keyword: node.forKeyword) { + hasMultilinePredecessors(node.body, keyword: node.forKeyword) { return } @@ -94,7 +94,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: IfExprSyntax) { if configuration.ignoreMultilineStatementConditions, - hasMultilinePredecessors(node.body, keyword: node.ifKeyword) { + hasMultilinePredecessors(node.body, keyword: node.ifKeyword) { return } @@ -103,7 +103,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: WhileStmtSyntax) { if configuration.ignoreMultilineStatementConditions, - hasMultilinePredecessors(node.body, keyword: node.whileKeyword) { + hasMultilinePredecessors(node.body, keyword: node.whileKeyword) { return } @@ -114,8 +114,8 @@ private extension OpeningBraceRule { override func visitPost(_ node: FunctionDeclSyntax) { if let body = node.body, - configuration.shouldIgnoreMultilineFunctionSignatures, - hasMultilinePredecessors(body, keyword: node.funcKeyword) { + configuration.shouldIgnoreMultilineFunctionSignatures, + hasMultilinePredecessors(body, keyword: node.funcKeyword) { return } @@ -124,8 +124,8 @@ private extension OpeningBraceRule { override func visitPost(_ node: InitializerDeclSyntax) { if let body = node.body, - configuration.shouldIgnoreMultilineFunctionSignatures, - hasMultilinePredecessors(body, keyword: node.initKeyword) { + configuration.shouldIgnoreMultilineFunctionSignatures, + hasMultilinePredecessors(body, keyword: node.initKeyword) { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift index 7e67a3c090..2cbdf8cbbb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift @@ -68,9 +68,9 @@ struct OperatorUsageWhitespaceRule: OptInRule, CorrectableRule, SourceKitFreeRul let equalityOperatorRegex = regex("\\s+=\\s") guard let match = equalityOperatorRegex.firstMatch( - in: matchedString, - options: [], - range: matchedString.fullNSRange), + in: matchedString, + options: [], + range: matchedString.fullNSRange), match.range == matchedString.fullNSRange else { return false @@ -175,7 +175,7 @@ private class OperatorUsageWhitespaceVisitor: SyntaxVisitor { let noSpacing = noSpacingBefore || noSpacingAfter let operatorText = operatorToken.text - if noSpacing && allowedNoSpaceOperators.contains(operatorText) { + if noSpacing, allowedNoSpaceOperators.contains(operatorText) { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift index d7d6f2a1f6..c2fd3b0554 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift @@ -22,6 +22,25 @@ struct RedundantDiscardableLetRule: Rule { return Text("Hello, World!") } """, configuration: ["ignore_swiftui_view_bodies": true]), + Example(""" + @ViewBuilder + func bar() -> some View { + let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]), + Example(""" + #Preview { + let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]), + Example(""" + static var previews: some View { + let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]), ], triggeringExamples: [ Example("↓let _ = foo()"), @@ -32,10 +51,74 @@ struct RedundantDiscardableLetRule: Rule { Text("Hello, World!") } """), + Example(""" + @ViewBuilder + func bar() -> some View { + ↓let _ = foo() + return Text("Hello, World!") + } + """), + Example(""" + #Preview { + ↓let _ = foo() + return Text("Hello, World!") + } + """), + Example(""" + static var previews: some View { + ↓let _ = foo() + Text("Hello, World!") + } + """), + Example(""" + var notBody: some View { + ↓let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true], excludeFromDocumentation: true), + Example(""" + var body: some NotView { + ↓let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true], excludeFromDocumentation: true), ], corrections: [ Example("↓let _ = foo()"): Example("_ = foo()"), Example("if _ = foo() { ↓let _ = bar() }"): Example("if _ = foo() { _ = bar() }"), + Example(""" + var body: some View { + ↓let _ = foo() + Text("Hello, World!") + } + """): Example(""" + var body: some View { + _ = foo() + Text("Hello, World!") + } + """), + Example(""" + #Preview { + ↓let _ = foo() + return Text("Hello, World!") + } + """): Example(""" + #Preview { + _ = foo() + return Text("Hello, World!") + } + """), + Example(""" + var body: some View { + let _ = foo() + return Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]): Example(""" + var body: some View { + let _ = foo() + return Text("Hello, World!") + } + """), ] ) } @@ -50,7 +133,7 @@ private extension RedundantDiscardableLetRule { private var codeBlockScopes = Stack() override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind { - codeBlockScopes.push(node.isViewBody ? .view : .normal) + codeBlockScopes.push(node.isViewBody || node.isPreviewProviderBody ? .view : .normal) return .visitChildren } @@ -58,8 +141,8 @@ private extension RedundantDiscardableLetRule { codeBlockScopes.pop() } - override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind { - codeBlockScopes.push(.normal) + override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + codeBlockScopes.push(node.isViewBuilderFunctionBody ? .view : .normal) return .visitChildren } @@ -67,6 +150,15 @@ private extension RedundantDiscardableLetRule { codeBlockScopes.pop() } + override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + codeBlockScopes.push(node.isPreviewMacroBody ? .view : .normal) + return .visitChildren + } + + override func visitPost(_: ClosureExprSyntax) { + codeBlockScopes.pop() + } + override func visitPost(_ node: VariableDeclSyntax) { if codeBlockScopes.peek() != .view || !configuration.ignoreSwiftUIViewBodies, node.bindingSpecifier.tokenKind == .keyword(.let), @@ -94,10 +186,45 @@ private extension AccessorBlockSyntax { if let binding = parent?.as(PatternBindingSyntax.self), binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "body", let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) { - return type.someOrAnySpecifier.text == "some" - && type.constraint.as(IdentifierTypeSyntax.self)?.name.text == "View" - && binding.parent?.parent?.is(VariableDeclSyntax.self) == true + return type.isView && binding.parent?.parent?.is(VariableDeclSyntax.self) == true } return false } + + var isPreviewProviderBody: Bool { + guard let binding = parent?.as(PatternBindingSyntax.self), + binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "previews", + let bindingList = binding.parent?.as(PatternBindingListSyntax.self), + let variableDecl = bindingList.parent?.as(VariableDeclSyntax.self), + variableDecl.modifiers.contains(keyword: .static), + variableDecl.bindingSpecifier.tokenKind == .keyword(.var), + let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) else { + return false + } + + return type.isView + } +} + +private extension CodeBlockSyntax { + var isViewBuilderFunctionBody: Bool { + guard let functionDecl = parent?.as(FunctionDeclSyntax.self), + functionDecl.attributes.contains(attributeNamed: "ViewBuilder") else { + return false + } + return functionDecl.signature.returnClause?.type.as(SomeOrAnyTypeSyntax.self)?.isView ?? false + } +} + +private extension ClosureExprSyntax { + var isPreviewMacroBody: Bool { + parent?.as(MacroExpansionExprSyntax.self)?.macroName.text == "Preview" + } +} + +private extension SomeOrAnyTypeSyntax { + var isView: Bool { + someOrAnySpecifier.text == "some" && + constraint.as(IdentifierTypeSyntax.self)?.name.text == "View" + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift index 971b03f014..a18a97973f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift @@ -87,7 +87,7 @@ private extension TokenSyntax { var end: AbsolutePosition? var correction = " -> " - if previousToken.trailingTrivia != .space && !leadingTrivia.containsNewlines() { + if previousToken.trailingTrivia != .space, !leadingTrivia.containsNewlines() { start = previousToken.endPositionBeforeTrailingTrivia end = endPosition @@ -96,7 +96,7 @@ private extension TokenSyntax { } } - if trailingTrivia != .space && !nextToken.leadingTrivia.containsNewlines() { + if trailingTrivia != .space, !nextToken.leadingTrivia.containsNewlines() { if leadingTrivia.containsNewlines() { start = positionAfterSkippingLeadingTrivia correction = "-> " diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift index d07022130c..5f4993acab 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift @@ -91,8 +91,8 @@ private extension SelfBindingRule { return super.visit(node.with(\.pattern, newPattern)) } if node.initializer == nil, - identifierPattern.identifier.text == "self", - configuration.bindIdentifier != "self" { + identifierPattern.identifier.text == "self", + configuration.bindIdentifier != "self" { numberOfCorrections += 1 let newPattern = PatternSyntax( identifierPattern diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift index f7eeb9c310..9c194558f3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift @@ -103,7 +103,7 @@ private extension ShorthandArgumentRule { if complexArguments.contains(argument) { nil } else if locationConverter.location(for: argument.position).line - <= startLine + configuration.allowUntilLineAfterOpeningBrace { + <= startLine + configuration.allowUntilLineAfterOpeningBrace { nil } else { ReasonedRuleViolation( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift index 705d8d4dec..94408f4709 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift @@ -84,8 +84,9 @@ struct SortedImportsRule: CorrectableRule, OptInRule { let contents = file.stringView let lines = file.lines let importLines: [Line] = importRanges.compactMap { range in - guard let line = contents.lineAndCharacter(forCharacterOffset: range.location)?.line - else { return nil } + guard let line = contents.lineAndCharacter(forCharacterOffset: range.location)?.line else { + return nil + } return lines[line - 1] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift index 342037995b..891bb036e3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift @@ -184,7 +184,7 @@ private extension StatementPositionRule { let validator = Self.uncuddledMatchValidator(contents: file.stringView) let filterRanges = Self.uncuddledMatchFilter(contents: file.stringView, syntaxMap: syntaxMap) let validMatches = matches.compactMap(validator).filter(filterRanges) - .filter { file.ruleEnabled(violatingRanges: [$0.range], for: self).isNotEmpty } + .filter { file.ruleEnabled(violatingRanges: [$0.range], for: self).isNotEmpty } if validMatches.isEmpty { return 0 } @@ -195,11 +195,11 @@ private extension StatementPositionRule { var whitespace = contents.bridge().substring(with: range1) let newLines: String if newlineRange.location != NSNotFound { - newLines = contents.bridge().substring(with: newlineRange) + newLines = contents.bridge().substring(with: newlineRange) } else { newLines = "" } - if !whitespace.hasPrefix("\n") && newLines != "\n" { + if !whitespace.hasPrefix("\n"), newLines != "\n" { whitespace.insert("\n", at: whitespace.startIndex) } contents = contents.bridge().replacingCharacters(in: range2, with: whitespace) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift index 46592dd7a6..90cc5352a3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift @@ -54,7 +54,7 @@ extension SwitchCaseAlignmentRule { let switchKeywordPosition = node.switchKeyword.positionAfterSkippingLeadingTrivia let switchKeywordLocation = locationConverter.location(for: switchKeywordPosition) - if configuration.ignoreOneLiners && switchKeywordLocation.line == closingBraceLocation.line { + if configuration.ignoreOneLiners, switchKeywordLocation.line == closingBraceLocation.line { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift index fb68edaf03..91ebcec4b5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift @@ -154,7 +154,7 @@ private extension TrailingCommaRule { .with(\.expression, lastElement.expression.with(\.trailingTrivia, [])) .with(\.trailingComma, .commaToken()) .with(\.trailingTrivia, lastElement.expression.trailingTrivia) - ) + ) return super.visit(newNode) case (_, true), (nil, false): return super.visit(node) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift index 4e1e78d411..fe0ef2b8a2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift @@ -1,7 +1,9 @@ import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax -struct TrailingWhitespaceRule: CorrectableRule { +@SwiftSyntaxRule(correctable: true) +struct TrailingWhitespaceRule: Rule { var configuration = TrailingWhitespaceConfiguration() static let description = RuleDescription( @@ -14,77 +16,313 @@ struct TrailingWhitespaceRule: CorrectableRule { Example("let name: String //\n"), Example("let name: String // \n"), ], triggeringExamples: [ - Example("let name: String \n"), Example("/* */ let name: String \n") + 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"), + Example("/* */ let name: String↓ \n"): Example("/* */ let name: String\n"), ] ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - let filteredLines = file.lines.filter { - guard $0.content.hasTrailingWhitespace() else { return false } +private extension TrailingWhitespaceRule { + final class Visitor: ViolationsSyntaxVisitor { + // Pre-computed comment information for performance + private var linesFullyCoveredByBlockComments = Set() + private var linesEndingWithComment = Set() - let commentKinds = SyntaxKind.commentKinds - if configuration.ignoresComments, - let lastSyntaxKind = file.syntaxKindsByLines[$0.index].last, - commentKinds.contains(lastSyntaxKind) { - return false + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + // Pre-compute all comment information in a single pass if needed + if configuration.ignoresComments { + precomputeCommentInformation(node) } - return !configuration.ignoresEmptyLines || - // If configured, ignore lines that contain nothing but whitespace (empty lines) - $0.content.trimmingCharacters(in: .whitespaces).isNotEmpty + // Process each line for trailing whitespace violations + for lineContents in file.lines { + let line = lineContents.content + let lineNumber = lineContents.index // 1-based + + // Calculate trailing whitespace info + guard let trailingWhitespaceInfo = line.trailingWhitespaceInfo() else { + continue // No trailing whitespace + } + + // Apply `ignoresEmptyLines` configuration + if configuration.ignoresEmptyLines, line.trimmingCharacters(in: .whitespaces).isEmpty { + continue + } + + // Apply `ignoresComments` configuration + if configuration.ignoresComments { + // Check if line is fully within a block comment + if linesFullyCoveredByBlockComments.contains(lineNumber) { + continue + } + + // Check if line ends with a comment (using pre-computed info) + if linesEndingWithComment.contains(lineNumber) { + continue + } + } + + // Calculate violation position + let lineStartPos = locationConverter.position(ofLine: lineNumber, column: 1) + let violationStartOffset = line.utf8.count - trailingWhitespaceInfo.byteLength + let violationPosition = lineStartPos.advanced(by: violationStartOffset) + + let correctionEnd = lineStartPos.advanced(by: line.utf8.count) + + violations.append(ReasonedRuleViolation( + position: violationPosition, + correction: .init(start: violationPosition, end: correctionEnd, replacement: "") + )) + } + return .skipChildren } - return filteredLines.map { - StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: Location(file: file.path, line: $0.index)) + /// Pre-computes all comment information in a single pass for better performance + private func precomputeCommentInformation(_ node: SourceFileSyntax) { + // First, collect block comment information + collectLinesFullyCoveredByBlockComments(node) + + // Then, collect line comment ranges and determine which lines end with comments + let lineCommentRanges = collectLineCommentRanges(from: node) + determineLineEndingComments(using: lineCommentRanges) } - } - func correct(file: SwiftLintFile) -> Int { - let whitespaceCharacterSet = CharacterSet.whitespaces - var correctedLines = [String]() - var numberOfCorrections = 0 - for line in file.lines { - guard line.content.hasTrailingWhitespace() else { - correctedLines.append(line.content) - continue + /// Collects ranges of line comments organized by line number + private func collectLineCommentRanges(from node: SourceFileSyntax) -> [Int: [Range]] { + var lineCommentRanges: [Int: [Range]] = [:] + + for token in node.tokens(viewMode: .sourceAccurate) { + // Process leading trivia + var currentPos = token.position + for piece in token.leadingTrivia { + let pieceStart = currentPos + currentPos += piece.sourceLength + + if piece.isComment, !piece.isBlockComment { + let pieceStartLine = locationConverter.location(for: pieceStart).line + lineCommentRanges[pieceStartLine, default: []].append(pieceStart..]]) { + for lineNumber in 1...file.lines.count { + let line = file.lines[lineNumber - 1].content + + // Skip if no trailing whitespace + guard let trailingWhitespaceInfo = line.trailingWhitespaceInfo() else { + continue + } + + // Get the effective content (before trailing whitespace) + let effectiveContent = getEffectiveContent(from: line, removing: trailingWhitespaceInfo) + + // Check if the effective content ends with a comment + if checkIfContentEndsWithComment( + effectiveContent, + lineNumber: lineNumber, + lineCommentRanges: lineCommentRanges + ) { + linesEndingWithComment.insert(lineNumber) + } + } + } + + /// Gets the content of a line before its trailing whitespace + private func getEffectiveContent( + from line: String, + removing trailingWhitespaceInfo: TrailingWhitespaceInfo + ) -> String { + if trailingWhitespaceInfo.characterCount > 0, line.count >= trailingWhitespaceInfo.characterCount { + return String(line.prefix(line.count - trailingWhitespaceInfo.characterCount)) + } + return "" + } + + /// Checks if the given content ends with a comment + private func checkIfContentEndsWithComment( + _ effectiveContent: String, + lineNumber: Int, + lineCommentRanges: [Int: [Range]] + ) -> Bool { + guard !effectiveContent.isEmpty, + let lastNonWhitespaceIdx = effectiveContent.lastIndex(where: { !$0.isWhitespace }) else { + return false + } + + // Calculate the byte position of the last non-whitespace character + let contentUpToLastChar = effectiveContent.prefix(through: lastNonWhitespaceIdx) + let byteOffsetToLastChar = contentUpToLastChar.utf8.count - 1 // -1 for position of char + let lineStartPos = locationConverter.position(ofLine: lineNumber, column: 1) + let lastNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToLastChar) + + // Check if this position falls within any comment range on this line + if let ranges = lineCommentRanges[lineNumber] { + for range in ranges { + if range.lowerBound <= lastNonWhitespacePos, lastNonWhitespacePos < range.upperBound { + return true + } + } } - let commentKinds = SyntaxKind.commentKinds - if configuration.ignoresComments, - let lastSyntaxKind = file.syntaxKindsByLines[line.index].last, - commentKinds.contains(lastSyntaxKind) { - correctedLines.append(line.content) - continue + return false + } + + /// Collects line numbers that are fully covered by block comments + private func collectLinesFullyCoveredByBlockComments(_ sourceFile: SourceFileSyntax) { + for token in sourceFile.tokens(viewMode: .sourceAccurate) { + var currentPos = token.position + + // Process leading trivia + for piece in token.leadingTrivia { + let pieceStartPos = currentPos + currentPos += piece.sourceLength + + if piece.isBlockComment { + markLinesFullyCoveredByBlockComment( + blockCommentStart: pieceStartPos, + blockCommentEnd: currentPos + ) + } + } + + // Advance past token content + currentPos = token.endPositionBeforeTrailingTrivia + + // Process trailing trivia + for piece in token.trailingTrivia { + let pieceStartPos = currentPos + currentPos += piece.sourceLength + + if piece.isBlockComment { + markLinesFullyCoveredByBlockComment( + blockCommentStart: pieceStartPos, + blockCommentEnd: currentPos + ) + } + } + } + } + + /// Marks lines that are fully covered by a block comment + private func markLinesFullyCoveredByBlockComment( + blockCommentStart: AbsolutePosition, + blockCommentEnd: AbsolutePosition + ) { + let startLocation = locationConverter.location(for: blockCommentStart) + let endLocation = locationConverter.location(for: blockCommentEnd) + + let startLine = startLocation.line + var endLine = endLocation.line + + // If comment ends at column 1, it actually ended on the previous line + if endLocation.column == 1, endLine > startLine { + endLine -= 1 } - let correctedLine = line.content.bridge() - .trimmingTrailingCharacters(in: whitespaceCharacterSet) + for lineNum in startLine...endLine { + if lineNum <= 0 || lineNum > file.lines.count { continue } - if configuration.ignoresEmptyLines && correctedLine.isEmpty { - correctedLines.append(line.content) - continue + let lineInfo = file.lines[lineNum - 1] + let lineContent = lineInfo.content + let lineStartPos = locationConverter.position(ofLine: lineNum, column: 1) + + // Check if the line's non-whitespace content is fully within the block comment + if let firstNonWhitespaceIdx = lineContent.firstIndex(where: { !$0.isWhitespace }), + let lastNonWhitespaceIdx = lineContent.lastIndex(where: { !$0.isWhitespace }) { + // Line has non-whitespace content + // Calculate byte offsets (not character offsets) for AbsolutePosition + let contentBeforeFirstNonWS = lineContent.prefix(upTo: firstNonWhitespaceIdx) + let byteOffsetToFirstNonWS = contentBeforeFirstNonWS.utf8.count + let firstNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToFirstNonWS) + + let contentBeforeLastNonWS = lineContent.prefix(upTo: lastNonWhitespaceIdx) + let byteOffsetToLastNonWS = contentBeforeLastNonWS.utf8.count + let lastNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToLastNonWS) + + // Check if both first and last non-whitespace positions are within the comment + if firstNonWhitespacePos >= blockCommentStart, lastNonWhitespacePos < blockCommentEnd { + linesFullyCoveredByBlockComments.insert(lineNum) + } + } else { + // Line is all whitespace - check if it's within the comment bounds + let lineEndPos = lineStartPos.advanced(by: lineContent.utf8.count) + if lineStartPos >= blockCommentStart, lineEndPos <= blockCommentEnd { + linesFullyCoveredByBlockComments.insert(lineNum) + } + } } + } + } +} + +// Helper struct to return both character count and byte length for whitespace +private struct TrailingWhitespaceInfo { + let characterCount: Int + let byteLength: Int +} - if file.ruleEnabled(violatingRanges: [line.range], for: self).isEmpty { - correctedLines.append(line.content) - continue +private extension String { + func hasTrailingWhitespace() -> Bool { + if isEmpty { return false } + guard let lastScalar = unicodeScalars.last else { return false } + return CharacterSet.whitespaces.contains(lastScalar) + } + + /// Returns information about trailing whitespace (spaces and tabs only) + func trailingWhitespaceInfo() -> TrailingWhitespaceInfo? { + var charCount = 0 + var byteLen = 0 + for char in self.reversed() { + if char.isWhitespace, char == " " || char == "\t" { // Only count spaces and tabs + charCount += 1 + byteLen += char.utf8.count + } else { + break } + } + return charCount > 0 ? TrailingWhitespaceInfo(characterCount: charCount, byteLength: byteLen) : nil + } - if line.content != correctedLine { - numberOfCorrections += 1 + func trimmingTrailingCharacters(in characterSet: CharacterSet) -> String { + var end = endIndex + while end > startIndex { + let index = index(before: end) + if !characterSet.contains(self[index].unicodeScalars.first!) { + break } - correctedLines.append(correctedLine) + end = index } - if numberOfCorrections > 0 { - // join and re-add trailing newline - file.write(correctedLines.joined(separator: "\n") + "\n") + return String(self[.. 0 && paramLoc.line > firstParamLoc.line { let previousParamLoc = paramLocations[index - 1] - if previousParamLoc.line < paramLoc.line && firstParamLoc.column != paramLoc.column { + if previousParamLoc.line < paramLoc.line, firstParamLoc.column != paramLoc.column { violations.append(paramLoc.position) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift index 081b73ed69..c7cd724404 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift @@ -148,7 +148,7 @@ struct VerticalWhitespaceBetweenCasesRule: Rule { let patternRegex = regex(pattern) let substring = file.contents.substring(from: range.location, length: range.length) guard let matchResult = patternRegex.firstMatch(in: substring, options: [], range: substring.fullNSRange), - matchResult.numberOfRanges > 1 else { + matchResult.numberOfRanges > 1 else { return false } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 2ff363264a..23cc943260 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -1,146 +1,171 @@ import Foundation -import SourceKittenFramework +import SwiftSyntax -private let defaultDescriptionReason = "Limit vertical whitespace to a single empty line" - -struct VerticalWhitespaceRule: CorrectableRule { +@SwiftSyntaxRule(explicitRewriter: true, correctable: true) +struct VerticalWhitespaceRule: Rule { var configuration = VerticalWhitespaceConfiguration() static let description = RuleDescription( identifier: "vertical_whitespace", name: "Vertical Whitespace", - description: defaultDescriptionReason + ".", + description: VerticalWhitespaceConfiguration.defaultDescriptionReason, kind: .style, nonTriggeringExamples: [ Example("let abc = 0\n"), Example("let abc = 0\n\n"), Example("/* bcs \n\n\n\n*/"), Example("// bca \n\n"), + Example("class CCCC {\n \n}"), + Example(""" + // comment + + import Foundation + """), + Example(""" + + // comment + + import Foundation + """), ], triggeringExamples: [ Example("let aaaa = 0\n\n\n"), Example("struct AAAA {}\n\n\n\n"), Example("class BBBB {}\n\n\n"), + Example("class CCCC {\n \n \n}"), + Example(""" + + + import Foundation + """), ], 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("class CCCC {\n \n \n \n}"): Example("class CCCC {\n \n}"), ] // End of line autocorrections are handled by Trailing Newline Rule. ) +} - private var configuredDescriptionReason: String { - guard configuration.maxEmptyLines == 1 else { - return "Limit vertical whitespace to maximum \(configuration.maxEmptyLines) empty lines" - } - return defaultDescriptionReason - } - - func validate(file: SwiftLintFile) -> [StyleViolation] { - let linesSections = violatingLineSections(in: file) - guard linesSections.isNotEmpty else { - return [] - } +private extension VerticalWhitespaceRule { + final class Visitor: ViolationsSyntaxVisitor { + /// The number of additional newlines to expect before the first token. + private var firstTokenAdditionalNewlines = 1 - return linesSections.map { eachLastLine, eachSectionCount in - StyleViolation( - ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: Location(file: file.path, line: eachLastLine.index), - reason: configuredDescriptionReason + "; currently \(eachSectionCount + 1)" - ) - } - } - - private typealias LineSection = (lastLine: Line, linesToRemove: Int) + override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind { + // Reset immediately. Only the first token has an additional leading newline. + defer { firstTokenAdditionalNewlines = 0 } - private func violatingLineSections(in file: SwiftLintFile) -> [LineSection] { - let nonSpaceRegex = regex("\\S", options: []) - let filteredLines = file.lines.filter { - nonSpaceRegex.firstMatch(in: file.contents, options: [], range: $0.range) == nil - } + // The strategy here is to keep track of the position of the _first_ violating newline + // in each consecutive run, and report the violation when the run _ends_. - guard filteredLines.isNotEmpty else { - return [] - } + if token.leadingTrivia.isEmpty { + return .visitChildren + } - let blankLinesSections = extractSections(from: filteredLines) + var consecutiveNewlines = 0 + var currentPosition = token.position + var violationPosition: AbsolutePosition? + + func process(_ count: Int, _ offset: Int) { + for _ in 0..<(count + firstTokenAdditionalNewlines) { + if consecutiveNewlines > configuration.maxEmptyLines, violationPosition == nil { + violationPosition = currentPosition + } + consecutiveNewlines += 1 + currentPosition = currentPosition.advanced(by: offset) + } + } - // filtering out violations in comments and strings - let stringAndComments = SyntaxKind.commentAndStringKinds - let syntaxMap = file.syntaxMap - let result = blankLinesSections.compactMap { eachSection -> (lastLine: Line, linesToRemove: Int)? in - guard let lastLine = eachSection.last else { - return nil + for piece in token.leadingTrivia { + switch piece { + case .newlines(let count), .carriageReturns(let count), .formfeeds(let count), .verticalTabs(let count): + process(count, 1) + case .carriageReturnLineFeeds(let count): + process(count, 2) // CRLF is 2 bytes + case .spaces, .tabs: + currentPosition += piece.sourceLength + default: + // A comment breaks the chain of newlines. + firstTokenAdditionalNewlines = 0 + if let violationPosition { + report(violationPosition, consecutiveNewlines) + } + violationPosition = nil + consecutiveNewlines = 0 + currentPosition += piece.sourceLength + } } - let kindInSection = syntaxMap.kinds(inByteRange: lastLine.byteRange) - if stringAndComments.isDisjoint(with: kindInSection) { - return (lastLine, eachSection.count) + if let violationPosition { + report(violationPosition, consecutiveNewlines) } - return nil + return .visitChildren } - return result.filter { $0.linesToRemove >= configuration.maxEmptyLines } - } - - private func extractSections(from lines: [Line]) -> [[Line]] { - var blankLinesSections = [[Line]]() - var lineSection = [Line]() - - var previousIndex = 0 - for (index, line) in lines.enumerated() { - let previousLine: Line = lines[previousIndex] - if previousLine.index + 1 == line.index { - lineSection.append(line) - } else if lineSection.isNotEmpty { - blankLinesSections.append(lineSection) - lineSection.removeAll() - } - previousIndex = index + private func report(_ position: AbsolutePosition, _ newlines: Int) { + violations.append(ReasonedRuleViolation( + position: position, + reason: configuration.configuredDescriptionReason + "; currently \(newlines - 1)" + )) } - if lineSection.isNotEmpty { - blankLinesSections.append(lineSection) - } - - return blankLinesSections } - func correct(file: SwiftLintFile) -> Int { - let linesSections = violatingLineSections(in: file) - if linesSections.isEmpty { - return 0 - } - - var indexOfLinesToDelete = [Int]() - - for section in linesSections { - let linesToRemove = section.linesToRemove - configuration.maxEmptyLines + 1 - let start = section.lastLine.index - linesToRemove - indexOfLinesToDelete.append(contentsOf: start.. { + override func visit(_ token: TokenSyntax) -> TokenSyntax { + var result = [TriviaPiece]() + var pendingWhitespace = [TriviaPiece]() + var consecutiveNewlines = 0 + + func process(_ count: Int, _ create: (Int) -> TriviaPiece) { + let linesToPreserve = min(count, max(0, configuration.maxEmptyLines + 1 - consecutiveNewlines)) + consecutiveNewlines += count + + if count > linesToPreserve { + self.numberOfCorrections += count - linesToPreserve + } + + if linesToPreserve > 0 { + // We can still add this piece, even if we adjusted its count lower. + // Pull in any pending whitespace along with it. + result.append(contentsOf: pendingWhitespace) + result.append(create(linesToPreserve)) + pendingWhitespace.removeAll() + } else { + // We're now in violation. Dump pending whitespace so it's excluded from the result. + pendingWhitespace.removeAll() + } + } - var correctedLines = [String]() - var numberOfCorrections = 0 - for currentLine in file.lines { - // Doesn't correct lines where rule is disabled - if file.ruleEnabled(violatingRanges: [currentLine.range], for: self).isEmpty { - correctedLines.append(currentLine.content) - continue + for piece in token.leadingTrivia { + switch piece { + case .newlines(let count): + process(count, TriviaPiece.newlines) + case .carriageReturns(let count): + process(count, TriviaPiece.carriageReturns) + case .carriageReturnLineFeeds(let count): + process(count, TriviaPiece.carriageReturnLineFeeds) + case .formfeeds(let count): + process(count, TriviaPiece.formfeeds) + case .verticalTabs(let count): + process(count, TriviaPiece.verticalTabs) + case .spaces, .tabs: + pendingWhitespace.append(piece) + default: + // Reset and pull in pending whitespace + consecutiveNewlines = 0 + result.append(contentsOf: pendingWhitespace) + result.append(piece) + pendingWhitespace.removeAll() + } } - // removes lines by skipping them from correctedLines - if Set(indexOfLinesToDelete).contains(currentLine.index) { - // reports every line that is being deleted - numberOfCorrections += 1 - continue // skips line + // Pull in any remaining pending whitespace + if !pendingWhitespace.isEmpty { + result.append(contentsOf: pendingWhitespace) } - // all lines that pass get added to final output file - correctedLines.append(currentLine.content) - } - // converts lines back to file and adds trailing line - if numberOfCorrections > 0 { - file.write(correctedLines.joined(separator: "\n") + "\n") + + return super.visit(token.with(\.leadingTrivia, Trivia(pieces: result))) } - return numberOfCorrections } } diff --git a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift index c9221ccebf..3f1ae318fd 100644 --- a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift @@ -46,12 +46,6 @@ public struct SourceKittenDictionary { (value["key.bodyoffset"] as? Int64).map(ByteCount.init) } - /// Body byte range. - public var bodyByteRange: ByteRange? { - guard let offset = bodyOffset, let length = bodyLength else { return nil } - return ByteRange(location: offset, length: length) - } - /// Kind. public var kind: String? { value["key.kind"] as? String @@ -76,12 +70,6 @@ public struct SourceKittenDictionary { (value["key.nameoffset"] as? Int64).map(ByteCount.init) } - /// Byte range of name. - public var nameByteRange: ByteRange? { - guard let offset = nameOffset, let length = nameLength else { return nil } - return ByteRange(location: offset, length: length) - } - /// Offset. public var offset: ByteCount? { (value["key.offset"] as? Int64).map(ByteCount.init) @@ -103,11 +91,6 @@ public struct SourceKittenDictionary { value["key.typename"] as? String } - /// Documentation length. - public var docLength: ByteCount? { - (value["key.doclength"] as? Int64).flatMap(ByteCount.init) - } - /// The attribute for this dictionary, as returned by SourceKit. public var attribute: String? { value["key.attribute"] as? String @@ -144,7 +127,7 @@ public struct SourceKittenDictionary { public var elements: [Self] { let elements = value["key.elements"] as? [any SourceKitRepresentable] ?? [] return elements.compactMap { $0 as? [String: any SourceKitRepresentable] } - .map(Self.init) + .map(Self.init) } public var entities: [Self] { diff --git a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift index 34acd32dc4..d012b9adba 100644 --- a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift @@ -5,6 +5,37 @@ public extension Request { static let disableSourceKit = ProcessInfo.processInfo.environment["SWIFTLINT_DISABLE_SOURCEKIT"] != nil func sendIfNotDisabled() throws -> [String: any SourceKitRepresentable] { + // Skip safety checks if explicitly allowed (e.g., for testing or specific operations) + if !CurrentRule.allowSourceKitRequestWithoutRule { + // Check if we have a rule context + if let ruleID = CurrentRule.identifier { + // Skip registry check for mock test rules + if ruleID != "mock_test_rule_for_swiftlint_tests" { + // Ensure the rule exists in the registry + guard let ruleType = RuleRegistry.shared.rule(forID: ruleID) else { + queuedFatalError(""" + Rule '\(ruleID)' not found in RuleRegistry. This indicates a configuration or wiring issue. + """) + } + + // Check if the current rule is a SourceKitFreeRule + // Skip check for ConditionallySourceKitFree rules since we can't determine + // at the type level if they're effectively SourceKit-free + if ruleType is any SourceKitFreeRule.Type, !(ruleType is any ConditionallySourceKitFree.Type) { + queuedFatalError(""" + '\(ruleID)' is a SourceKitFreeRule and should not be making requests to SourceKit. + """) + } + } + } else { + // No rule context and not explicitly allowed + queuedFatalError(""" + SourceKit request made outside of rule execution context without explicit permission. + Use CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { ... } for allowed exceptions. + """) + } + } + guard !Self.disableSourceKit else { throw Self.Error.connectionInterrupted("SourceKit is disabled by `SWIFTLINT_DISABLE_SOURCEKIT`.") } diff --git a/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift b/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift index dff42f5f20..edfd2c8498 100644 --- a/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift +++ b/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift @@ -27,21 +27,6 @@ public extension SourceKittenDictionary { return results } - func structures(forByteOffset byteOffset: ByteCount) -> [SourceKittenDictionary] { - var results = [SourceKittenDictionary]() - - func parse(_ dictionary: SourceKittenDictionary) { - guard let byteRange = dictionary.byteRange, byteRange.contains(byteOffset) else { - return - } - - results.append(dictionary) - dictionary.substructure.forEach(parse) - } - parse(self) - return results - } - /// Return the string content of this structure in the given file. /// - Parameter file: File this structure occurs in /// - Returns: The content of the file which this `SourceKittenDictionary` structure represents diff --git a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift index d89f5130fb..c8c48acac8 100644 --- a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift @@ -55,8 +55,8 @@ public extension String { limitedBy: utf16.endIndex) ?? utf16.endIndex guard let fromIndex = Index(from16, within: self), - let toIndex = Index(to16, within: self) else { - return nil + let toIndex = Index(to16, within: self) else { + return nil } return fromIndex.. = [ - .varClass, - .varGlobal, - .varInstance, - .varLocal, - .varParameter, - .varStatic, - ] - static let functionKinds: Set = [ .functionAccessorAddress, .functionAccessorDidset, @@ -34,12 +25,4 @@ public extension SwiftDeclarationKind { .associatedtype, .enum, ] - - static let extensionKinds: Set = [ - .extension, - .extensionClass, - .extensionEnum, - .extensionProtocol, - .extensionStruct, - ] } diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift index 61a5d63302..85aeb8fb77 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift @@ -49,6 +49,10 @@ private let syntaxClassificationsCache = Cache { $0.syntaxTree.classifications } private let syntaxKindsByLinesCache = Cache { $0.syntaxKindsByLine() } private let syntaxTokensByLinesCache = Cache { $0.syntaxTokensByLine() } private let linesWithTokensCache = Cache { $0.computeLinesWithTokens() } +private let swiftSyntaxTokensCache = Cache { file -> [SwiftLintSyntaxToken]? in + // Use SwiftSyntaxKindBridge to derive SourceKitten-compatible tokens from SwiftSyntax + SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) +} package typealias AssertHandler = () -> Void // Re-enable once all parser diagnostics in tests have been addressed. @@ -190,6 +194,10 @@ extension SwiftLintFile { return syntaxKindsByLines } + public var swiftSyntaxDerivedSourceKittenTokens: [SwiftLintSyntaxToken]? { + swiftSyntaxTokensCache.get(self) + } + /// Invalidates all cached data for this file. public func invalidateCache() { file.clearCaches() @@ -200,6 +208,7 @@ extension SwiftLintFile { syntaxMapCache.invalidate(self) syntaxTokensByLinesCache.invalidate(self) syntaxKindsByLinesCache.invalidate(self) + swiftSyntaxTokensCache.invalidate(self) syntaxTreeCache.invalidate(self) foldedSyntaxTreeCache.invalidate(self) locationConverterCache.invalidate(self) @@ -215,6 +224,7 @@ extension SwiftLintFile { syntaxMapCache.clear() syntaxTokensByLinesCache.clear() syntaxKindsByLinesCache.clear() + swiftSyntaxTokensCache.clear() syntaxTreeCache.clear() foldedSyntaxTreeCache.clear() locationConverterCache.clear() diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift index 1f4ce74a43..dfe0f1af57 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift @@ -117,11 +117,6 @@ extension SwiftLintFile { } } - public func rangesAndTokens(matching pattern: String, - range: NSRange? = nil) -> [(NSRange, [SwiftLintSyntaxToken])] { - matchesAndTokens(matching: pattern, range: range).map { ($0.0.range, $0.1) } - } - public func match(pattern: String, range: NSRange? = nil, captureGroup: Int = 0) -> [(NSRange, [SyntaxKind])] { matchesAndSyntaxKinds(matching: pattern, range: range).map { textCheckingResult, syntaxKinds in (textCheckingResult.range(at: captureGroup), syntaxKinds) @@ -164,7 +159,7 @@ extension SwiftLintFile { let tokenRange = token.range if line.byteRange.contains(token.offset) || tokenRange.contains(line.byteRange.location) { - results[line.index].append(token) + results[line.index].append(token) } let tokenEnd = tokenRange.upperBound let lineEnd = line.byteRange.upperBound @@ -208,23 +203,6 @@ extension SwiftLintFile { .map(\.0) } - public typealias MatchMapping = (NSTextCheckingResult) -> NSRange - - public func match(pattern: String, - range: NSRange? = nil, - excludingSyntaxKinds: Set, - excludingPattern: String, - exclusionMapping: MatchMapping = \.range) -> [NSRange] { - let matches = match(pattern: pattern, excludingSyntaxKinds: excludingSyntaxKinds) - if matches.isEmpty { - return [] - } - let range = range ?? stringView.range - let exclusionRanges = regex(excludingPattern).matches(in: stringView, options: [], - range: range).map(exclusionMapping) - return matches.filter { !$0.intersects(exclusionRanges) } - } - public func append(_ string: String) { guard string.isNotEmpty else { return @@ -282,15 +260,6 @@ extension SwiftLintFile { ruleEnabled(violatingRanges: [violatingRange], for: rule).first } - public func isACL(token: SwiftLintSyntaxToken) -> Bool { - guard token.kind == .attributeBuiltin else { - return false - } - - let aclString = contents(for: token) - return aclString.flatMap(AccessControlLevel.init(description:)) != nil - } - public func contents(for token: SwiftLintSyntaxToken) -> String? { stringView.substringWithByteRange(token.range) } diff --git a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift index af40515e4b..b0e837fb63 100644 --- a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift @@ -1,6 +1,8 @@ import SourceKittenFramework import SwiftSyntax +// swiftlint:disable file_length + // workaround for https://bugs.swift.org/browse/SR-10121 so we can use `Self` in a closure public protocol SwiftLintSyntaxVisitor: SyntaxVisitor {} extension SyntaxVisitor: SwiftLintSyntaxVisitor {} @@ -66,8 +68,8 @@ public extension ExprSyntax { return functionCall } if let tuple = self.as(TupleExprSyntax.self), - let firstElement = tuple.elements.onlyElement, - let functionCall = firstElement.expression.as(FunctionCallExprSyntax.self) { + let firstElement = tuple.elements.onlyElement, + let functionCall = firstElement.expression.as(FunctionCallExprSyntax.self) { return functionCall } return nil @@ -191,6 +193,19 @@ public extension EnumDeclSyntax { return rawValueTypes.contains(identifier) } } + + /// True if this enum is a `CodingKey`. For that, it has to be named `CodingKeys` + /// and must conform to the `CodingKey` protocol. + var definesCodingKeys: Bool { + guard let inheritedTypeCollection = inheritanceClause?.inheritedTypes, + name.text == "CodingKeys" else { + return false + } + + return inheritedTypeCollection.contains { element in + element.type.as(IdentifierTypeSyntax.self)?.name.text == "CodingKey" + } + } } public extension FunctionDeclSyntax { diff --git a/Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift b/Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift new file mode 100644 index 0000000000..637930d2ff --- /dev/null +++ b/Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift @@ -0,0 +1,61 @@ +import SourceKittenFramework +import SwiftIDEUtils +import SwiftSyntax + +/// Bridge to convert SwiftSyntax classifications to SourceKitten syntax kinds. +/// This enables SwiftSyntax-based custom rules to work with kind filtering +/// without making any SourceKit calls. +public enum SwiftSyntaxKindBridge { + /// Map a SwiftSyntax classification to SourceKitten syntax kind. + static func mapClassification(_ classification: SyntaxClassification) -> SourceKittenFramework.SyntaxKind? { + // swiftlint:disable:previous cyclomatic_complexity + switch classification { + case .attribute: + return .attributeID + case .blockComment, .lineComment: + return .comment + case .docBlockComment, .docLineComment: + return .docComment + case .dollarIdentifier, .identifier: + return .identifier + case .editorPlaceholder: + return .placeholder + case .floatLiteral, .integerLiteral: + return .number + case .ifConfigDirective: + return .poundDirectiveKeyword + case .keyword: + return .keyword + case .none, .regexLiteral: + return nil + case .operator: + return .operator + case .stringLiteral: + return .string + case .type: + return .typeidentifier + case .argumentLabel: + return .argument + @unknown default: + return nil + } + } + + /// Convert SwiftSyntax syntax classifications to SourceKitten-compatible syntax tokens. + public static func sourceKittenSyntaxKinds(for file: SwiftLintFile) -> [SwiftLintSyntaxToken] { + file.syntaxClassifications.compactMap { classifiedRange in + guard let syntaxKind = mapClassification(classifiedRange.kind) else { + return nil + } + + let byteRange = classifiedRange.range.toSourceKittenByteRange() + let token = SyntaxToken( + type: syntaxKind.rawValue, + offset: byteRange.location, + length: byteRange.length + ) + + return SwiftLintSyntaxToken(value: token) + } + } +} diff --git a/Source/SwiftLintCore/Models/AccessControlLevel.swift b/Source/SwiftLintCore/Models/AccessControlLevel.swift index 160956d0b8..6570f916d7 100644 --- a/Source/SwiftLintCore/Models/AccessControlLevel.swift +++ b/Source/SwiftLintCore/Models/AccessControlLevel.swift @@ -1,7 +1,7 @@ /// The accessibility of a Swift source declaration. /// /// - SeeAlso: https://github.com/apple/swift/blob/main/docs/AccessControl.md -public enum AccessControlLevel: String, CustomStringConvertible { +public enum AccessControlLevel: String, CustomStringConvertible, Sendable { /// Accessible by the declaration's immediate lexical scope. case `private` = "source.lang.swift.accessibility.private" /// Accessible by the declaration's same file. @@ -47,11 +47,6 @@ public enum AccessControlLevel: String, CustomStringConvertible { case .open: return "open" } } - - /// Returns true if is `private` or `fileprivate` - public var isPrivate: Bool { - self == .private || self == .fileprivate - } } extension AccessControlLevel: Comparable { diff --git a/Source/SwiftLintCore/Models/Baseline.swift b/Source/SwiftLintCore/Models/Baseline.swift index 62aa573e2f..05bbd29add 100644 --- a/Source/SwiftLintCore/Models/Baseline.swift +++ b/Source/SwiftLintCore/Models/Baseline.swift @@ -105,10 +105,9 @@ public struct Baseline: Equatable { var filteredViolations: Set = [] for (ruleIdentifier, ruleViolations) in violationsByRuleIdentifier { - guard - let baselineViolations = baselineViolationsByRuleIdentifier[ruleIdentifier], - baselineViolations.isNotEmpty else { - filteredViolations.formUnion(ruleViolations) + guard let baselineViolations = baselineViolationsByRuleIdentifier[ruleIdentifier], + baselineViolations.isNotEmpty else { + filteredViolations.formUnion(ruleViolations) continue } diff --git a/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift b/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift index f0f28828e5..dc0360da38 100644 --- a/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift +++ b/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift @@ -24,7 +24,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 { + let optionSeverity = ChildOptionSeverity(rawValue: configString.lowercased()) else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } self.optionSeverity = optionSeverity diff --git a/Source/SwiftLintCore/Models/Command.swift b/Source/SwiftLintCore/Models/Command.swift index 09fb2db0bf..40a2e60f89 100644 --- a/Source/SwiftLintCore/Models/Command.swift +++ b/Source/SwiftLintCore/Models/Command.swift @@ -109,9 +109,9 @@ public struct Command: Equatable { // Store any text after the comment delimiter as the trailingComment. // The addition to currentIndex is to move past the delimiter trailingComment = String( - scanner - .string[scanner.currentIndex...] - .dropFirst(Self.commentDelimiter.count) + scanner + .string[scanner.currentIndex...] + .dropFirst(Self.commentDelimiter.count) ) } let ruleTexts = rawRuleTexts.components(separatedBy: .whitespacesAndNewlines).filter { diff --git a/Source/SwiftLintCore/Models/CurrentRule.swift b/Source/SwiftLintCore/Models/CurrentRule.swift new file mode 100644 index 0000000000..81cd169d0c --- /dev/null +++ b/Source/SwiftLintCore/Models/CurrentRule.swift @@ -0,0 +1,11 @@ +/// A task-local value that holds the identifier of the currently executing rule. +/// This allows SourceKit request handling to determine if the current rule +/// is a SourceKitFreeRule without modifying function signatures throughout the codebase. +public enum CurrentRule { + /// The Rule ID for the currently executing rule. + @TaskLocal public static var identifier: String? + + /// Allows specific SourceKit requests to be made outside of rule execution context. + /// This should only be used for essential operations like getting the Swift version. + @TaskLocal public static var allowSourceKitRequestWithoutRule = false +} diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index b16417b139..86e0190b34 100644 --- a/Source/SwiftLintCore/Models/Issue.swift +++ b/Source/SwiftLintCore/Models/Issue.swift @@ -116,7 +116,7 @@ public enum Issue: LocalizedError, Equatable { } /// The issues description which is ready to be printed to the console. - package var errorDescription: String { + public var errorDescription: String? { switch self { case .genericError: return "error: \(message)" @@ -132,8 +132,8 @@ public enum Issue: LocalizedError, Equatable { if case .ruleDeprecated = self, !Self.printDeprecationWarnings { return } - Self.printQueueContinuation?.yield(errorDescription) - queuedPrintError(errorDescription) + Self.printQueueContinuation?.yield(localizedDescription) + queuedPrintError(localizedDescription) } private var message: String { diff --git a/Source/SwiftLintCore/Models/RuleDescription.swift b/Source/SwiftLintCore/Models/RuleDescription.swift index ae5f9565a5..7dc528f688 100644 --- a/Source/SwiftLintCore/Models/RuleDescription.swift +++ b/Source/SwiftLintCore/Models/RuleDescription.swift @@ -11,7 +11,7 @@ public struct RuleDescription: Equatable, Sendable { /// explanation of the rule's purpose and rationale. public let description: String - /// A longer explanation of the rule's purpose and rationale. Typically defined as a multiline string, long text + /// A longer explanation of the rule's purpose and rationale. Typically defined as a multiline string, long text /// lines should be wrapped. Markdown formatting is supported. Multiline code blocks will be formatted as /// `swift` code unless otherwise specified, and will automatically be indented by four spaces when printed /// to the console. diff --git a/Source/SwiftLintCore/Models/RuleParameter.swift b/Source/SwiftLintCore/Models/RuleParameter.swift index 356395baaa..9d09264489 100644 --- a/Source/SwiftLintCore/Models/RuleParameter.swift +++ b/Source/SwiftLintCore/Models/RuleParameter.swift @@ -1,5 +1,5 @@ /// A configuration parameter for rules. -public struct RuleParameter: Equatable { +public struct RuleParameter: Equatable, Sendable where T: Sendable { /// The severity that should be assigned to the violation of this parameter's value is met. public let severity: ViolationSeverity /// The value to configure the rule. diff --git a/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift b/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift index 96d5f5e1fc..99214dbfda 100644 --- a/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift +++ b/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift @@ -2,9 +2,6 @@ import SourceKittenFramework /// Represents a Swift file's syntax information. public struct SwiftLintSyntaxMap { - /// The raw `SyntaxMap` obtained by SourceKitten. - public let value: SyntaxMap - /// The SwiftLint-specific syntax tokens for this syntax map. public let tokens: [SwiftLintSyntaxToken] @@ -12,7 +9,6 @@ public struct SwiftLintSyntaxMap { /// /// - parameter value: The raw `SyntaxMap` obtained by SourceKitten. public init(value: SyntaxMap) { - self.value = value self.tokens = value.tokens.map(SwiftLintSyntaxToken.init) } diff --git a/Source/SwiftLintCore/Models/SwiftVersion.swift b/Source/SwiftLintCore/Models/SwiftVersion.swift index 0a50d65c73..193283bf3c 100644 --- a/Source/SwiftLintCore/Models/SwiftVersion.swift +++ b/Source/SwiftLintCore/Models/SwiftVersion.swift @@ -69,6 +69,12 @@ public extension SwiftVersion { static let six = SwiftVersion(rawValue: "6.0.0") /// Swift 6.1 static let sixDotOne = SwiftVersion(rawValue: "6.1.0") + /// Swift 6.1.1 + static let sixDotOneDotOne = SwiftVersion(rawValue: "6.1.1") + /// Swift 6.1.2 + static let sixDotOneDotTwo = SwiftVersion(rawValue: "6.1.2") + /// Swift 6.2 + static let sixDotTwo = SwiftVersion(rawValue: "6.2.0") /// The current detected Swift compiler version, based on the currently accessible SourceKit version. /// @@ -81,8 +87,12 @@ public extension SwiftVersion { if !Request.disableSourceKit { // This request was added in Swift 5.1 let params: SourceKitObject = ["key.request": UID("source.request.compiler_version")] - if let result = try? Request.customRequest(request: params).send(), - let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch { + // Allow this specific SourceKit request outside of rule execution context + let result = CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + try? Request.customRequest(request: params).sendIfNotDisabled() + } + if let result, + let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch { return SwiftVersion(rawValue: "\(major).\(minor).\(patch)") } } diff --git a/Source/SwiftLintCore/Protocols/Rule.swift b/Source/SwiftLintCore/Protocols/Rule.swift index 94e36e6273..3e31915f28 100644 --- a/Source/SwiftLintCore/Protocols/Rule.swift +++ b/Source/SwiftLintCore/Protocols/Rule.swift @@ -2,7 +2,7 @@ import Foundation import SourceKittenFramework /// An executable value that can identify issues (violations) in Swift source code. -public protocol Rule { +public protocol Rule: Sendable { /// The type of the configuration used to configure this rule. associatedtype ConfigurationType: RuleConfiguration @@ -28,7 +28,7 @@ public protocol Rule { /// Create a description of how this rule has been configured to run. /// /// - parameter exclusiveOptions: A set of options that should be excluded from the description. - /// + /// /// - returns: A description of the rule's configuration. func createConfigurationDescription(exclusiveOptions: Set) -> RuleConfigurationDescription @@ -83,17 +83,17 @@ public protocol Rule { /// - 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 + 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 { @@ -240,6 +240,32 @@ public extension SubstitutionCorrectableRule { /// A rule that does not need SourceKit to operate and can still operate even after SourceKit has crashed. public protocol SourceKitFreeRule: Rule {} +/// A rule that may or may not require SourceKit depending on its configuration. +public protocol ConditionallySourceKitFree: Rule { + /// Whether this rule is currently configured in a way that doesn't require SourceKit. + var isEffectivelySourceKitFree: Bool { get } +} + +public extension Rule { + /// Whether this rule requires SourceKit to operate. + /// Returns false if the rule conforms to SourceKitFreeRule or if it conforms to + /// ConditionallySourceKitFree and is currently configured to not require SourceKit. + var requiresSourceKit: Bool { + // Check if rule conforms to SourceKitFreeRule + if self is any SourceKitFreeRule { + return false + } + + // Check if rule is conditionally SourceKit-free and currently doesn't need SourceKit + if let conditionalRule = self as? any ConditionallySourceKitFree { + return !conditionalRule.isEffectivelySourceKitFree + } + + // All other rules require SourceKit + return true + } +} + /// A rule that can operate on the post-typechecked AST using compiler arguments. Performs rules that are more like /// static analysis than syntactic checks. public protocol AnalyzerRule: OptInRule {} diff --git a/Source/SwiftLintCore/Protocols/RuleConfiguration.swift b/Source/SwiftLintCore/Protocols/RuleConfiguration.swift index c750417df4..ce97982aba 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: Equatable { +public protocol RuleConfiguration: Equatable, Sendable { /// The type of the rule that's using this configuration. associatedtype Parent: Rule diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index 6329261b45..83e6529d61 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -1,9 +1,18 @@ import Foundation -import SourceKittenFramework +@preconcurrency import SourceKittenFramework /// A rule configuration used for defining custom rules in yaml. public struct RegexConfiguration: SeverityBasedRuleConfiguration, Hashable, CacheDescriptionProvider, InlinableOptionType { + /// The execution mode for this custom rule. + public enum ExecutionMode: String, Codable, Sendable { + /// Uses SwiftSyntax to obtain syntax token kinds. + case swiftsyntax + /// Uses SourceKit to obtain syntax token kinds. + case sourcekit + /// Uses SwiftSyntax by default unless overridden to use SourceKit. + case `default` + } /// The identifier for this custom rule. public let identifier: String /// The name for this custom rule. @@ -24,6 +33,8 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, public var severityConfiguration = SeverityConfiguration(.warning) /// The index of the regex capture group to match. public var captureGroup = 0 + /// The execution mode for this rule. + public var executionMode: ExecutionMode = .default public var cacheDescription: String { let jsonObject: [String] = [ @@ -36,10 +47,11 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, SyntaxKind.allKinds.subtracting(excludedMatchKinds) .map(\.rawValue).sorted(by: <).joined(separator: ","), severity.rawValue, + executionMode.rawValue, ] if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject), - let jsonString = String(data: jsonData, encoding: .utf8) { - return jsonString + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString } queuedFatalError("Could not serialize regex configuration for cache") } @@ -57,6 +69,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, self.identifier = identifier } + // swiftlint:disable:next cyclomatic_complexity public mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any], let regexString = configurationDict[$regex.key] as? String else { @@ -97,11 +110,19 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, self.captureGroup = captureGroup } + if let modeString = configurationDict["execution_mode"] as? String { + guard let mode = ExecutionMode(rawValue: modeString) else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + self.executionMode = mode + } + self.excludedMatchKinds = try self.excludedMatchKinds(from: configurationDict) } public func hash(into hasher: inout Hasher) { hasher.combine(identifier) + hasher.combine(executionMode) } package func shouldValidate(filePath: String) -> Bool { diff --git a/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift index e23f082b05..64b7797541 100644 --- a/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift @@ -32,7 +32,8 @@ public struct SeverityLevelsConfiguration: RuleConfiguration, Inli warning = configurationArray[0] error = (configurationArray.count > 1) ? configurationArray[1] : nil } else if let configDict = configuration as? [String: Any?] { - if let warningValue = configDict[$warning.key] { + let warningValue = configDict[$warning.key] + if let warningValue { if let warning = warningValue as? Int { self.warning = warning } else { @@ -47,7 +48,7 @@ public struct SeverityLevelsConfiguration: RuleConfiguration, Inli } else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } - } else { + } else if warningValue != nil { self.error = nil } } else { diff --git a/Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift b/Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift deleted file mode 100644 index 22e985a65a..0000000000 --- a/Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift +++ /dev/null @@ -1,142 +0,0 @@ -import SwiftSyntax - -/// Visitor that collection violations of code block lengths. -public final class BodyLengthRuleVisitor: ViolationsSyntaxVisitor> { - @usableFromInline let kind: Kind - - /// The code block types to check. - public enum Kind { - /// Closure code blocks. - case closure - /// Function body blocks. - case function - /// Type (class, enum, ...) member blocks. - case type - - fileprivate var name: String { - switch self { - case .closure: - return "Closure" - case .function: - return "Function" - case .type: - return "Type" - } - } - } - - /// Initializer. - /// - /// - Parameters: - /// - kind: The code block type to check. See ``Kind``. - /// - file: The file to collect violation for. - /// - configuration: The configuration that defines the acceptable limits. - @inlinable - public init(kind: Kind, file: SwiftLintFile, configuration: SeverityLevelsConfiguration) { - self.kind = kind - super.init(configuration: configuration, file: file) - } - - override public func visitPost(_ node: EnumDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.enumKeyword - ) - } - } - - override public func visitPost(_ node: ClassDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.classKeyword - ) - } - } - - override public func visitPost(_ node: StructDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.structKeyword - ) - } - } - - override public func visitPost(_ node: ActorDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.actorKeyword - ) - } - } - - override public func visitPost(_ node: ClosureExprSyntax) { - if kind == .closure { - registerViolations( - leftBrace: node.leftBrace, - rightBrace: node.rightBrace, - violationNode: node.leftBrace - ) - } - } - - override public func visitPost(_ node: FunctionDeclSyntax) { - if kind == .function, let body = node.body { - registerViolations( - leftBrace: body.leftBrace, - rightBrace: body.rightBrace, - violationNode: node.name - ) - } - } - - override public func visitPost(_ node: InitializerDeclSyntax) { - if kind == .function, let body = node.body { - registerViolations( - leftBrace: body.leftBrace, - rightBrace: body.rightBrace, - violationNode: node.initKeyword - ) - } - } - - private func registerViolations( - leftBrace: TokenSyntax, rightBrace: TokenSyntax, violationNode: some SyntaxProtocol - ) { - let leftBracePosition = leftBrace.positionAfterSkippingLeadingTrivia - let leftBraceLine = locationConverter.location(for: leftBracePosition).line - let rightBracePosition = rightBrace.positionAfterSkippingLeadingTrivia - let rightBraceLine = locationConverter.location(for: rightBracePosition).line - let lineCount = file.bodyLineCountIgnoringCommentsAndWhitespace(leftBraceLine: leftBraceLine, - rightBraceLine: rightBraceLine) - let severity: ViolationSeverity, upperBound: Int - if let error = configuration.error, lineCount > error { - severity = .error - upperBound = error - } else if lineCount > configuration.warning { - severity = .warning - upperBound = configuration.warning - } else { - return - } - - let reason = """ - \(kind.name) body should span \(upperBound) lines or less excluding comments and whitespace: \ - currently spans \(lineCount) lines - """ - - let violation = ReasonedRuleViolation( - position: violationNode.positionAfterSkippingLeadingTrivia, - reason: reason, - severity: severity - ) - violations.append(violation) - } -} diff --git a/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift b/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift new file mode 100644 index 0000000000..2f75e4b173 --- /dev/null +++ b/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift @@ -0,0 +1,57 @@ +import SwiftSyntax + +/// A configuration that's based on warning and error thresholds for violations. +public protocol SeverityLevelsBasedRuleConfiguration: RuleConfiguration { + /// The severity configuration that defines the thresholds for warning and error severities. + var severityConfiguration: SeverityLevelsConfiguration { get } +} + +extension SeverityLevelsConfiguration: SeverityLevelsBasedRuleConfiguration { + public var severityConfiguration: SeverityLevelsConfiguration { self } +} + +/// Violation visitor customized to collect violations of code blocks that exceed a specified number of lines. +open class BodyLengthVisitor: ViolationsSyntaxVisitor { + @inlinable + override public init(configuration: LevelConfig, file: SwiftLintFile) { + super.init(configuration: configuration, file: file) + } + + /// Registers a violation if a body exceeds the configured line count. + /// + /// - Parameters: + /// - leftBrace: The left brace token of the body. + /// - rightBrace: The right brace token of the body. + /// - violationNode: The syntax node where the violation is to be reported. + /// - objectName: The name of the object (e.g., "Function", "Closure") used in the violation message. + public func registerViolations(leftBrace: TokenSyntax, + rightBrace: TokenSyntax, + violationNode: some SyntaxProtocol, + objectName: String) { + let leftBracePosition = leftBrace.positionAfterSkippingLeadingTrivia + let leftBraceLine = locationConverter.location(for: leftBracePosition).line + let rightBracePosition = rightBrace.positionAfterSkippingLeadingTrivia + let rightBraceLine = locationConverter.location(for: rightBracePosition).line + let lineCount = file.bodyLineCountIgnoringCommentsAndWhitespace(leftBraceLine: leftBraceLine, + rightBraceLine: rightBraceLine) + let severity: ViolationSeverity, upperBound: Int + if let error = configuration.severityConfiguration.error, lineCount > error { + severity = .error + upperBound = error + } else if lineCount > configuration.severityConfiguration.warning { + severity = .warning + upperBound = configuration.severityConfiguration.warning + } else { + return + } + + violations.append(.init( + position: violationNode.positionAfterSkippingLeadingTrivia, + reason: """ + \(objectName) body should span \(upperBound) lines or less excluding comments and whitespace: \ + currently spans \(lineCount) lines + """, + severity: severity + )) + } +} diff --git a/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift b/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift index 71752dd3ba..cd0d2056d7 100644 --- a/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift +++ b/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift @@ -32,7 +32,7 @@ open class CodeBlockVisitor: ViolationsSyntaxV return } if parent.is(FunctionCallExprSyntax.self) || parent.is(MultipleTrailingClosureElementSyntax.self), - node.keyPathInParent != \FunctionCallExprSyntax.calledExpression { + node.keyPathInParent != \FunctionCallExprSyntax.calledExpression { // Trailing closure collectViolations(for: node) } diff --git a/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift index d9b1c05545..f33da18fcd 100644 --- a/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift +++ b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift @@ -66,8 +66,8 @@ extension Array where Element == String { let responseFile = String(arg.dropFirst()) return (try? String(contentsOf: URL(fileURLWithPath: responseFile, isDirectory: false))).flatMap { $0.trimmingCharacters(in: .newlines) - .components(separatedBy: "\n") - .expandingResponseFiles + .components(separatedBy: "\n") + .expandingResponseFiles } ?? [arg] } } diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index d33c732a5f..06549f9bbe 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -75,8 +75,8 @@ private func autoreleasepool(block: () -> T) -> T { block() } extension Configuration { func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage) async throws -> [SwiftLintFile] { - let files = try await Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") { - try await getFiles(with: visitor) + let files = try Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") { + try getFiles(with: visitor) } let groupedFiles = try Signposts.record(name: "Configuration.VisitLintableFiles.GroupFiles") { try groupFiles(files, visitor: visitor) @@ -105,7 +105,7 @@ extension Configuration { private func groupFiles(_ files: [SwiftLintFile], visitor: LintableFilesVisitor) throws -> [Configuration: [SwiftLintFile]] { - if files.isEmpty && !visitor.allowZeroLintableFiles { + if files.isEmpty, !visitor.allowZeroLintableFiles { throw SwiftLintError.usageError( description: "No lintable files found at paths: '\(visitor.options.paths.joined(separator: ", "))'" ) @@ -166,12 +166,12 @@ extension Configuration { let counter = CounterActor() let total = linters.filter(\.isCollecting).count let progress = ProgressBar(count: total) - if visitor.options.progress && 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.options.quiet && linter.isCollecting { + if !visitor.options.quiet, linter.isCollecting { if visitor.options.progress { await progress.printNext() } else if let filePath = linter.file.path { @@ -236,7 +236,7 @@ extension Configuration { linters.asyncMap(visit) } - fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] { + fileprivate func getFiles(with visitor: LintableFilesVisitor) throws -> [SwiftLintFile] { let options = visitor.options if options.useSTDIN { let stdinData = FileHandle.standardInput.readDataToEndOfFile() @@ -273,8 +273,8 @@ extension Configuration { queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } let excludeLintableFilesBy = options.useExcludingByPrefix - ? Configuration.ExcludeBy.prefix - : .paths(excludedPaths: excludedPaths()) + ? Configuration.ExcludeBy.prefix + : .paths(excludedPaths: excludedPaths()) return options.paths.flatMap { self.lintableFiles( inPath: $0, diff --git a/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift b/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift index 464553659e..e15d0f5fa6 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift @@ -146,7 +146,7 @@ package extension Configuration { // Local vertices are allowed to have local / remote references // Remote vertices are only allowed to have remote references - if vertex.originatesFromRemote && !referencedVertex.originatesFromRemote { + if vertex.originatesFromRemote, !referencedVertex.originatesFromRemote { throw Issue.genericWarning("Remote configs are not allowed to reference local configs.") } let existingVertex = findPossiblyExistingVertex(sameAs: referencedVertex) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift b/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift index f1a8283cf7..584a78c27b 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift @@ -71,7 +71,7 @@ internal extension Configuration.FileGraph { } private func read(at path: String) throws -> String { - guard !path.isEmpty && FileManager.default.fileExists(atPath: path) else { + guard !path.isEmpty, FileManager.default.fileExists(atPath: path) else { throw isInitialVertex ? Issue.initialFileNotFound(path: path) : Issue.fileNotFound(path: path) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index c8326c0776..33ae5e8bb9 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -171,8 +171,8 @@ extension Configuration { ) { for key in dict.keys where !validGlobalKeys.contains(key) { guard let identifier = ruleList.identifier(for: key), - let ruleType = ruleList.list[identifier] else { - continue + let ruleType = ruleList.list[identifier] else { + continue } switch rulesMode { diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift b/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift index e8eec05c86..bd27bce998 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift @@ -240,7 +240,7 @@ internal extension Configuration.FileGraph.FilePath { // Delete all cache folders except for the current version's folder let directoryWithoutVersionNum = directory.components(separatedBy: "/").dropLast().joined(separator: "/") try (try FileManager.default.subpathsOfDirectory(atPath: directoryWithoutVersionNum)).forEach { - if !$0.contains("/") && $0 != Configuration.FileGraph.FilePath.remoteCacheVersionNumber { + if !$0.contains("/"), $0 != Configuration.FileGraph.FilePath.remoteCacheVersionNumber { try FileManager.default.removeItem(atPath: $0.bridge().absolutePathRepresentation(rootDirectory: directoryWithoutVersionNum) ) diff --git a/Source/SwiftLintFramework/Configuration/Configuration.swift b/Source/SwiftLintFramework/Configuration/Configuration.swift index bf5d6d9bc1..ee3e2fa788 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration.swift @@ -280,7 +280,7 @@ public struct Configuration { if useDefaultConfigOnFailure ?? !hasCustomConfigurationFiles { // No files were explicitly specified, so maybe the user doesn't want a config at all -> warn queuedPrintError( - "\(Issue.wrap(error: error).errorDescription) – Falling back to default configuration" + "\(Issue.wrap(error: error).localizedDescription) – Falling back to default configuration" ) self.init(rulesMode: rulesMode, cachePath: cachePath) } else { diff --git a/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift b/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift index e80d72a439..157fb9fa73 100644 --- a/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift +++ b/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift @@ -12,7 +12,12 @@ struct RuleDocumentation { var isLinterRule: Bool { !isAnalyzerRule } /// If this rule uses SourceKit. - var usesSourceKit: Bool { !(ruleType is any SourceKitFreeRule.Type) } + /// Note: For ConditionallySourceKitFree rules, this returns true since we can't + /// determine at the type level if they're effectively SourceKit-free. + var usesSourceKit: Bool { + !(ruleType is any SourceKitFreeRule.Type) || + (ruleType is any ConditionallySourceKitFree.Type) + } /// If this rule is disabled by default. var isDisabledByDefault: Bool { ruleType is any OptInRule.Type } diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index c5aad909a5..f76ca676f3 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -35,7 +35,7 @@ extension FileManager: LintableFileManager { .standardizingPath // if path is a file, it won't be returned in `enumerator(atPath:)` - if absolutePath.bridge().isSwiftFile() && absolutePath.isFile { + if absolutePath.bridge().isSwiftFile(), absolutePath.isFile { return [absolutePath] } diff --git a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index 9c80db6ab7..4af32083a4 100644 --- a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -131,7 +131,7 @@ package struct LintOrAnalyzeCommand { Could not change working directory to '\(workingDirectory)'. \ Make sure it exists and is accessible. """ - ) + ) } } try await Signposts.record(name: "LintOrAnalyzeCommand.run") { @@ -203,8 +203,7 @@ package struct LintOrAnalyzeCommand { ) throws -> Int { let options = builder.options let configuration = builder.configuration - if isWarningThresholdBroken(configuration: configuration, violations: builder.violations) - && !options.lenient { + if isWarningThresholdBroken(configuration: configuration, violations: builder.violations), !options.lenient { builder.violations.append( createThresholdViolation(threshold: configuration.warningThreshold!) ) @@ -236,9 +235,8 @@ package struct LintOrAnalyzeCommand { return try Baseline(fromPath: baselinePath) } catch { Issue.baselineNotReadable(path: baselinePath).print() - if - (error as? CocoaError)?.code != CocoaError.fileReadNoSuchFile || - options.writeBaseline != options.baseline { + if (error as? CocoaError)?.code != CocoaError.fileReadNoSuchFile || + options.writeBaseline != options.baseline { throw error } } @@ -326,7 +324,7 @@ package struct LintOrAnalyzeCommand { } let corrections = linter.correct(using: storage) - if !corrections.isEmpty && !options.quiet { + if !corrections.isEmpty, !options.quiet { if options.useSTDIN { queuedPrint(linter.file.contents) } else { diff --git a/Source/SwiftLintFramework/LintableFilesVisitor.swift b/Source/SwiftLintFramework/LintableFilesVisitor.swift index c6c630e335..2e812f0ce9 100644 --- a/Source/SwiftLintFramework/LintableFilesVisitor.swift +++ b/Source/SwiftLintFramework/LintableFilesVisitor.swift @@ -57,16 +57,6 @@ enum LintOrAnalyzeModeWithCompilerArguments { case analyze(allCompilerInvocations: CompilerInvocations) } -private func resolveParamsFiles(args: [String]) -> [String] { - 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 { - allArgs.append(arg) - } - } -} - struct LintableFilesVisitor { let options: LintOrAnalyzeOptions let cache: LinterCache? @@ -152,7 +142,7 @@ struct LintableFilesVisitor { private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? { if let data = FileManager.default.contents(atPath: path), - let logContents = String(data: data, encoding: .utf8) { + let logContents = String(data: data, encoding: .utf8) { if logContents.isEmpty { return nil } @@ -174,7 +164,7 @@ struct LintableFilesVisitor { } guard let object = try? JSONSerialization.jsonObject(with: fileContents), - let compileDB = object as? [[String: Any]] else { + let compileDB = object as? [[String: Any]] else { throw CompileCommandsLoadError.malformedCommands(path) } diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index 67c1251b75..525d25b18c 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -1,5 +1,6 @@ import Foundation import SourceKittenFramework +import SwiftLintCore // swiftlint:disable file_length @@ -77,7 +78,9 @@ private extension Rule { return false } - if !(self is any SourceKitFreeRule) && file.sourcekitdFailed { + // Only check sourcekitdFailed if the rule requires SourceKit. + // This avoids triggering SourceKit initialization for SourceKit-free rules. + if requiresSourceKit, file.sourcekitdFailed { warnSourceKitFailedOnce() return false } @@ -95,6 +98,32 @@ private extension Rule { compilerArguments: [String]) -> LintResult { let ruleID = Self.identifier + // Wrap entire lint process including shouldRun check in rule context + return CurrentRule.$identifier.withValue(ruleID) { + guard shouldRun(onFile: file) else { + return LintResult(violations: [], ruleTime: nil, deprecatedToValidIDPairs: []) + } + + return performLint( + file: file, + regions: regions, + benchmark: benchmark, + storage: storage, + superfluousDisableCommandRule: superfluousDisableCommandRule, + compilerArguments: compilerArguments + ) + } + } + + // swiftlint:disable:next function_parameter_count + private func performLint(file: SwiftLintFile, + regions: [Region], + benchmark: Bool, + storage: RuleStorage, + superfluousDisableCommandRule: SuperfluousDisableCommandRule?, + compilerArguments: [String]) -> LintResult { + let ruleID = Self.identifier + let violations: [StyleViolation] let ruleTime: (String, Double)? if benchmark { @@ -116,11 +145,11 @@ private extension Rule { } let customRulesIDs: [String] = { - guard let customRules = self as? CustomRules else { - return [] - } - return customRules.customRuleIdentifiers - }() + guard let customRules = self as? CustomRules else { + return [] + } + return customRules.customRuleIdentifiers + }() let ruleIDs = Self.description.allIdentifiers + customRulesIDs + (superfluousDisableCommandRule.map({ type(of: $0) })?.description.allIdentifiers ?? []) + @@ -247,7 +276,11 @@ public struct Linter { /// - returns: A linter capable of checking for violations after running each rule's collection step. public func collect(into storage: RuleStorage) -> CollectedLinter { DispatchQueue.concurrentPerform(iterations: rules.count) { idx in - rules[idx].collectInfo(for: file, into: storage, compilerArguments: compilerArguments) + let rule = rules[idx] + let ruleID = type(of: rule).identifier + CurrentRule.$identifier.withValue(ruleID) { + rule.collectInfo(for: file, into: storage, compilerArguments: compilerArguments) + } } return CollectedLinter(from: self) } @@ -306,15 +339,11 @@ public struct CollectedLinter { let superfluousDisableCommandRule = rules.first(where: { $0 is SuperfluousDisableCommandRule }) as? SuperfluousDisableCommandRule - let validationResults: [LintResult] = rules.parallelCompactMap { - guard $0.shouldRun(onFile: file) else { - return nil - } - - return $0.lint(file: file, regions: regions, benchmark: benchmark, - storage: storage, - superfluousDisableCommandRule: superfluousDisableCommandRule, - compilerArguments: compilerArguments) + let validationResults: [LintResult] = rules.parallelMap { + $0.lint(file: file, regions: regions, benchmark: benchmark, + storage: storage, + superfluousDisableCommandRule: superfluousDisableCommandRule, + compilerArguments: compilerArguments) } let undefinedSuperfluousCommandViolations = self.undefinedSuperfluousCommandViolations( regions: regions, configuration: configuration, @@ -344,7 +373,7 @@ public struct CollectedLinter { private func cachedStyleViolations(benchmark: Bool = false) -> ([StyleViolation], [(id: String, time: Double)])? { let start = Date() guard let cache, let file = file.path, - let cachedViolations = cache.violations(forFile: file, configuration: configuration) else { + let cachedViolations = cache.violations(forFile: file, configuration: configuration) else { return nil } @@ -381,17 +410,20 @@ public struct CollectedLinter { } var corrections = [String: Int]() - for rule in rules where rule.shouldRun(onFile: file) { - guard let rule = rule as? any CorrectableRule else { - continue + for rule in rules.compactMap({ $0 as? any CorrectableRule }) { + // Set rule context before checking shouldRun to allow file property access + let ruleCorrections = CurrentRule.$identifier.withValue(type(of: rule).identifier) { () -> Int? in + guard rule.shouldRun(onFile: file) else { + return nil + } + return rule.correct(file: file, using: storage, compilerArguments: compilerArguments) } - let corrected = rule.correct(file: file, using: storage, compilerArguments: compilerArguments) - if corrected != 0 { + if let corrected = ruleCorrections, corrected != 0 { corrections[type(of: rule).description.identifier] = corrected if !file.isVirtual { file.invalidateCache() } - } + } } return corrections } diff --git a/Source/SwiftLintFramework/Models/LinterCache.swift b/Source/SwiftLintFramework/Models/LinterCache.swift index 75a4097216..c6c541255a 100644 --- a/Source/SwiftLintFramework/Models/LinterCache.swift +++ b/Source/SwiftLintFramework/Models/LinterCache.swift @@ -74,9 +74,9 @@ public final class LinterCache { internal func violations(forFile file: String, configuration: Configuration) -> [StyleViolation]? { guard let lastModification = fileManager.modificationDate(forFileAtPath: file), - let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file], - entry.lastModification == lastModification, - entry.swiftVersion == swiftVersion + let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file], + entry.lastModification == lastModification, + entry.swiftVersion == swiftVersion else { return nil } diff --git a/Source/SwiftLintFramework/Models/YamlParser.swift b/Source/SwiftLintFramework/Models/YamlParser.swift index 018478a927..da44759271 100644 --- a/Source/SwiftLintFramework/Models/YamlParser.swift +++ b/Source/SwiftLintFramework/Models/YamlParser.swift @@ -4,7 +4,7 @@ import Yams // MARK: - YamlParser /// An interface for parsing YAML. -public struct YamlParser { +struct YamlParser { /// Parses the input YAML string as an untyped dictionary. /// /// - parameter yaml: YAML-formatted string. @@ -13,8 +13,8 @@ public struct YamlParser { /// - returns: The parsed YAML as an untyped dictionary. /// /// - throws: Throws if the `yaml` string provided could not be parsed. - public static func parse(_ yaml: String, - env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] { + static func parse(_ yaml: String, + env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] { do { return try Yams.load(yaml: yaml, .default, .swiftlintConstructor(env: env)) as? [String: Any] ?? [:] diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index ec6a4f9000..d54c57488e 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -7,19 +7,33 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { var parameterDescription: RuleConfigurationDescription? { RuleConfigurationOption.noOptions } var cacheDescription: String { - customRuleConfigurations + let configsDescription = customRuleConfigurations .sorted { $0.identifier < $1.identifier } .map(\.cacheDescription) .joined(separator: "\n") + + if let defaultMode = defaultExecutionMode { + return "default_execution_mode:\(defaultMode.rawValue)\n\(configsDescription)" + } + return configsDescription } var customRuleConfigurations = [RegexConfiguration]() + var defaultExecutionMode: RegexConfiguration.ExecutionMode? mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any] else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } - for (key, value) in configurationDict { + // Parse default execution mode if present + if let defaultModeString = configurationDict["default_execution_mode"] as? String { + guard let mode = RegexConfiguration.ExecutionMode(rawValue: defaultModeString) else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + defaultExecutionMode = mode + } + + for (key, value) in configurationDict where key != "default_execution_mode" { var ruleConfiguration = RegexConfiguration(identifier: key) do { @@ -36,7 +50,7 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { // MARK: - CustomRules -struct CustomRules: Rule, CacheDescriptionProvider { +struct CustomRules: Rule, CacheDescriptionProvider, ConditionallySourceKitFree { var cacheDescription: String { configuration.cacheDescription } @@ -50,12 +64,23 @@ struct CustomRules: Rule, CacheDescriptionProvider { name: "Custom Rules", description: """ Create custom rules by providing a regex string. Optionally specify what syntax kinds to match against, \ - the severity level, and what message to display. + the severity level, and what message to display. Rules default to SwiftSyntax mode for improved \ + performance. Use `execution_mode: sourcekit` or `default_execution_mode: sourcekit` for SourceKit mode. """, kind: .style) var configuration = CustomRulesConfiguration() + /// Returns true if all configured custom rules use SwiftSyntax mode, making this rule effectively SourceKit-free. + var isEffectivelySourceKitFree: Bool { + configuration.customRuleConfigurations.allSatisfy { config in + let effectiveMode = config.executionMode == .default + ? (configuration.defaultExecutionMode ?? .swiftsyntax) + : config.executionMode + return effectiveMode == .swiftsyntax + } + } + func validate(file: SwiftLintFile) -> [StyleViolation] { var configurations = configuration.customRuleConfigurations diff --git a/Source/SwiftLintFramework/RulesFilter.swift b/Source/SwiftLintFramework/RulesFilter.swift index 8a5d1d250e..d0cac706d4 100644 --- a/Source/SwiftLintFramework/RulesFilter.swift +++ b/Source/SwiftLintFramework/RulesFilter.swift @@ -30,13 +30,13 @@ package final class RulesFilter { } let isRuleEnabled = enabledRule != nil - if excludingOptions.contains(.enabled) && isRuleEnabled { + if excludingOptions.contains(.enabled), isRuleEnabled { return nil } - if excludingOptions.contains(.disabled) && !isRuleEnabled { + if excludingOptions.contains(.disabled), !isRuleEnabled { return nil } - if excludingOptions.contains(.uncorrectable) && !(ruleType is any CorrectableRule.Type) { + if excludingOptions.contains(.uncorrectable), !(ruleType is any CorrectableRule.Type) { return nil } diff --git a/Source/swiftlint-dev/Reporters+Register.swift b/Source/swiftlint-dev/Reporters+Register.swift index 118de060ff..ed4fe57cdb 100644 --- a/Source/swiftlint-dev/Reporters+Register.swift +++ b/Source/swiftlint-dev/Reporters+Register.swift @@ -18,7 +18,7 @@ extension SwiftLintDev.Reporters { .appendingPathComponent("Reporters", isDirectory: true) } - func run() async throws { + func run() throws { guard FileManager.default.fileExists(atPath: reportersDirectory.path) else { throw ValidationError("Command must be run from the root of the SwiftLint repository.") } diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index a8a87a1c06..269a03abf3 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -26,7 +26,7 @@ extension SwiftLintDev.Rules { .appendingPathComponent("GeneratedTests", isDirectory: true) } - func run() async throws { + func run() throws { try runFor(newRule: nil) } @@ -47,6 +47,7 @@ extension SwiftLintDev.Rules { .sorted() try registerInRulesList(rules) try registerInTests(rules) + try registerInTestsBzl(rules) try registerInTestReference(adding: newRule) print("(Re-)Registered \(rules.count) rules.") } @@ -74,53 +75,157 @@ struct NewRuleDetails: Equatable { } } +/// Struct to hold processed rule information and shard calculations +private struct ProcessedRulesContext { + let baseRuleNames: [String] + let totalShards: Int + let shardSize: Int + + init(ruleFiles: [String], shardSize: Int) { + self.baseRuleNames = ruleFiles.map { $0.replacingOccurrences(of: ".swift", with: "") } + self.shardSize = shardSize + guard shardSize > 0, !baseRuleNames.isEmpty else { + self.totalShards = baseRuleNames.isEmpty ? 0 : 1 + return + } + self.totalShards = (baseRuleNames.count + shardSize - 1) / shardSize // Ceiling division + } + + /// Returns the rule names for a specific shard index + func shardRules(forIndex shardIndex: Int) -> ArraySlice { + let startIndex = shardIndex * shardSize + let endIndex = min(startIndex + shardSize, baseRuleNames.count) + return baseRuleNames[startIndex.. String { + """ + // GENERATED FILE. DO NOT EDIT! + + /// The rule list containing all available rules built into SwiftLint. + public let builtInRules: [any Rule.Type] = [ + \(rulesImportList.indent(by: 4)), + ] + + """ + } + + /// Generate content for Swift test files + private func generateSwiftTestFileContent(forTestClasses testClassesString: String) -> String { + """ + // GENERATED FILE. DO NOT EDIT! + + // swiftlint:disable:next blanket_disable_command superfluous_disable_command + // swiftlint:disable single_test_class type_name + + @testable import SwiftLintBuiltInRules + @testable import SwiftLintCore + import TestHelpers + + \(testClassesString) + + """ + } + + /// Generate content for Bazel .bzl files + private func generateBzlFileContent(macroInvocations: String) -> String { + #""" + # GENERATED FILE. DO NOT EDIT! + + load(":test_macros.bzl", "generated_test_shard") + + def generated_tests(copts, strict_concurrency_copts): + """Creates all generated test targets for SwiftLint rules. + + Args: + copts: Common compiler options + strict_concurrency_copts: Strict concurrency compiler options + """ + \#(macroInvocations) + + """# + } + func registerInRulesList(_ ruleFiles: [String]) throws { - let rules = ruleFiles + let rulesImportString = ruleFiles .map { $0.replacingOccurrences(of: ".swift", with: ".self") } .joined(separator: ",\n") let builtInRulesFile = rulesDirectory.deletingLastPathComponent() .appendingPathComponent("Models", isDirectory: true) .appendingPathComponent("BuiltInRules.swift", isDirectory: false) - try """ - // GENERATED FILE. DO NOT EDIT! - - /// The rule list containing all available rules built into SwiftLint. - public let builtInRules: [any Rule.Type] = [ - \(rules.indent(by: 4)), - ] - """.write(to: builtInRulesFile, atomically: true, encoding: .utf8) + let fileContent = generateBuiltInRulesFileContent(rulesImportList: rulesImportString) + try fileContent.write(to: builtInRulesFile, atomically: true, encoding: .utf8) } func registerInTests(_ ruleFiles: [String]) throws { - let testFile = testsDirectory - .appendingPathComponent("GeneratedTests.swift", isDirectory: false) - let rules = ruleFiles - .map { $0.replacingOccurrences(of: ".swift", with: "") } - .map { testName in """ + let rulesContext = ProcessedRulesContext(ruleFiles: ruleFiles, shardSize: Self.shardSize) + + // Remove old generated files + let existingFiles = try FileManager.default.contentsOfDirectory( + at: testsDirectory, + includingPropertiesForKeys: nil + ) + for file in existingFiles where file.lastPathComponent.hasPrefix("GeneratedTests") && + file.pathExtension == "swift" { + try FileManager.default.removeItem(at: file) + } + + // Create sharded test files + for shardIndex in 0..(warning: warning2, error: error2) let config2: [String: Int] = ["warning": warning2, "error": error2] - let length3 = SeverityLevelsConfiguration(warning: warning2) let config3: [String: Bool] = ["ignores_case_statements": false] try configuration.apply(configuration: config1) @@ -58,7 +57,7 @@ final class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { XCTAssertTrue(configuration.ignoresCaseStatements) try configuration.apply(configuration: config3) - XCTAssertEqual(configuration.length, length3) + XCTAssertEqual(configuration.length, length2) XCTAssertFalse(configuration.ignoresCaseStatements) } diff --git a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift index 82b56cdfea..6cfceab5cd 100644 --- a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift @@ -33,7 +33,8 @@ final class FileHeaderRuleTests: SwiftLintTestCase { verifyRule(description, ruleConfiguration: ["required_string": "**Header"], stringDoesntViolate: false, skipCommentTests: true, - testMultiByteOffsets: false, testShebang: false) + skipDisableCommandTests: true, testMultiByteOffsets: false, + testShebang: false) } func testFileHeaderWithRequiredPattern() { diff --git a/Tests/BuiltInRulesTests/FileNameRuleTests.swift b/Tests/BuiltInRulesTests/FileNameRuleTests.swift index 9b4e528681..d81ede7ffa 100644 --- a/Tests/BuiltInRulesTests/FileNameRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileNameRuleTests.swift @@ -7,6 +7,7 @@ private let fixturesDirectory = "\(TestResources.path())/FileNameRuleFixtures" final class FileNameRuleTests: SwiftLintTestCase { private func validate(fileName: String, excluded: [String]? = nil, + excludedPaths: [String]? = nil, prefixPattern: String? = nil, suffixPattern: String? = nil, nestedTypeSeparator: String? = nil, @@ -18,6 +19,9 @@ final class FileNameRuleTests: SwiftLintTestCase { if let excluded { configuration["excluded"] = excluded } + if let excludedPaths { + configuration["excluded_paths"] = excludedPaths + } if let prefixPattern { configuration["prefix_pattern"] = prefixPattern } @@ -130,4 +134,79 @@ final class FileNameRuleTests: SwiftLintTestCase { ).isEmpty ) } + + func testExcludedDoesntSupportRegex() { + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [".*"] + ).isNotEmpty + ) + } + + func testExcludedPathPatternsSupportRegex() { + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*"] + ).isEmpty + ) + + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*.swift"] + ).isEmpty + ) + + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*/FileNameRuleFixtures/.*"] + ).isEmpty + ) + } + + func testExcludedPathPatternsWithRegexDoesntMatch() { + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*/OtherFolder/.*", "MAIN\\.swift"] + ).isNotEmpty + ) + } + + func testInvalidRegex() { + XCTAssertThrowsError( + try validate( + fileName: "NSString+Extension.swift", + excluded: [], + excludedPaths: ["("], + prefixPattern: "", + suffixPattern: "" + ) + ) + } + + func testExcludedPathPatternsWithMultipleRegexs() { + XCTAssertThrowsError( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: ["/FileNameRuleFixtures/.*", "("] + ) + ) + + XCTAssertThrowsError( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: ["/FileNameRuleFixtures/.*", "(", ".*.swift"] + ) + ) + } } diff --git a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift index f8acfbaa2e..41422a8dd1 100644 --- a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift +++ b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift @@ -2,102 +2,67 @@ import TestHelpers import XCTest -private func funcWithBody(_ body: String, - violates: Bool = false, - 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 = #filePath, line: UInt = #line) -> Example { - funcWithBody(body, violates: true, file: file, line: line) -} - 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" - ), - ] - ) - - let longerFunctionBodyWithEmptyLines = funcWithBody( - repeatElement("\n", count: 100).joined() - ) - XCTAssertEqual(self.violations(longerFunctionBodyWithEmptyLines), []) - } - - func testFunctionBodyLengthsWithComments() { - let longFunctionBodyWithComments = funcWithBody( - repeatElement("x = 0\n", count: 49).joined() + - "// comment only line should be ignored.\n" - ) - XCTAssertEqual(violations(longFunctionBodyWithComments), []) + func testWarning() { + let example = Example(""" + func f() { + let x = 0 + let y = 1 + let z = 2 + } + """) - let longerFunctionBodyWithComments = violatingFuncWithBody( - repeatElement("x = 0\n", count: 51).joined() + - "// comment only line should be ignored.\n" - ) XCTAssertEqual( - self.violations(longerFunctionBodyWithComments), + self.violations(example, configuration: ["warning": 2, "error": 4]), [ 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" + severity: .warning, + location: Location(file: nil, line: 1, character: 1), + reason: """ + Function body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ ), ] ) } - func testFunctionBodyLengthsWithMultilineComments() { - let longFunctionBodyWithMultilineComments = funcWithBody( - repeatElement("x = 0\n", count: 49).joined() + - "/* multi line comment only line should be ignored.\n*/\n" - ) - XCTAssertEqual(self.violations(longFunctionBodyWithMultilineComments), []) + func testError() { + let example = Example(""" + func f() { + let x = 0 + let y = 1 + let z = 2 + } + """) - let longerFunctionBodyWithMultilineComments = violatingFuncWithBody( - repeatElement("x = 0\n", count: 51).joined() + - "/* multi line comment only line should be ignored.\n*/\n" - ) XCTAssertEqual( - self.violations(longerFunctionBodyWithMultilineComments), + self.violations(example, configuration: ["warning": 1, "error": 2]), [ 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" + severity: .error, + location: Location(file: nil, line: 1, character: 1), + reason: """ + Function body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ ), ] ) } - func testConfiguration() { - let function = violatingFuncWithBody(repeatElement("x = 0\n", count: 10).joined()) + func testViolationMessages() { + let types = FunctionBodyLengthRule.description.triggeringExamples.flatMap { + self.violations($0, configuration: ["warning": 2]) + }.compactMap { + $0.reason.split(separator: " ", maxSplits: 1).first + } - XCTAssertEqual(self.violations(function, configuration: ["warning": 12]).count, 0) - XCTAssertEqual(self.violations(function, configuration: ["warning": 12, "error": 14]).count, 0) - XCTAssertEqual( - self.violations(function, configuration: ["warning": 8]).map(\.reason), - ["Function body should span 8 lines or less excluding comments and whitespace: currently spans 10 lines"] - ) XCTAssertEqual( - self.violations(function, configuration: ["warning": 12, "error": 8]).map(\.reason), - ["Function body should span 8 lines or less excluding comments and whitespace: currently spans 10 lines"] + types, + ["Function", "Deinitializer", "Initializer", "Subscript", "Accessor", "Accessor", "Accessor"] ) } diff --git a/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift b/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift index abdbe9628b..2f4dd25ec3 100644 --- a/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift +++ b/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift @@ -99,7 +99,7 @@ final class LineLengthConfigurationTests: SwiftLintTestCase { } } - func testLineLengthConfigurationApplyConfigurationWithArray() { + func testLineLengthConfigurationApplyConfigurationWithArray() throws { var configuration = LineLengthConfiguration(length: SeverityLevelsConfiguration(warning: 0, error: 0)) let warning1 = 100 @@ -111,18 +111,14 @@ final class LineLengthConfigurationTests: SwiftLintTestCase { let length2 = SeverityLevelsConfiguration(warning: warning2, error: nil) let config2 = [warning2] - do { - try configuration.apply(configuration: config1) - XCTAssertEqual(configuration.length, length1) + try configuration.apply(configuration: config1) + XCTAssertEqual(configuration.length, length1) - try configuration.apply(configuration: config2) - XCTAssertEqual(configuration.length, length2) - } catch { - XCTFail("Failed to apply configuration with array") - } + try configuration.apply(configuration: config2) + XCTAssertEqual(configuration.length, length2) } - func testLineLengthConfigurationApplyConfigurationWithDictionary() { + func testLineLengthConfigurationApplyConfigurationWithDictionary() throws { var configuration = LineLengthConfiguration(length: SeverityLevelsConfiguration(warning: 0, error: 0)) let warning1 = 100 @@ -141,34 +137,29 @@ final class LineLengthConfigurationTests: SwiftLintTestCase { let length2 = SeverityLevelsConfiguration(warning: warning2, error: error2) 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, ] - do { - try configuration.apply(configuration: config1) - XCTAssertEqual(configuration.length, length1) - XCTAssertTrue(configuration.ignoresURLs) - XCTAssertTrue(configuration.ignoresFunctionDeclarations) - XCTAssertTrue(configuration.ignoresComments) - - try configuration.apply(configuration: config2) - XCTAssertEqual(configuration.length, length2) - XCTAssertTrue(configuration.ignoresURLs) - XCTAssertTrue(configuration.ignoresFunctionDeclarations) - XCTAssertTrue(configuration.ignoresComments) - - try configuration.apply(configuration: config3) - XCTAssertEqual(configuration.length, length3) - XCTAssertFalse(configuration.ignoresURLs) - XCTAssertFalse(configuration.ignoresFunctionDeclarations) - XCTAssertFalse(configuration.ignoresComments) - } catch { - XCTFail("Failed to apply configuration with dictionary") - } + try configuration.apply(configuration: config1) + XCTAssertEqual(configuration.length, length1) + XCTAssertTrue(configuration.ignoresURLs) + XCTAssertTrue(configuration.ignoresFunctionDeclarations) + XCTAssertTrue(configuration.ignoresComments) + + try configuration.apply(configuration: config2) + XCTAssertEqual(configuration.length, length2) + XCTAssertTrue(configuration.ignoresURLs) + XCTAssertTrue(configuration.ignoresFunctionDeclarations) + XCTAssertTrue(configuration.ignoresComments) + + try configuration.apply(configuration: config3) + XCTAssertEqual(configuration.length, length2) + XCTAssertFalse(configuration.ignoresURLs) + XCTAssertFalse(configuration.ignoresFunctionDeclarations) + XCTAssertFalse(configuration.ignoresComments) } func testLineLengthConfigurationCompares() { diff --git a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift new file mode 100644 index 0000000000..54025ec317 --- /dev/null +++ b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift @@ -0,0 +1,38 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { + func testOneDeclarationPerFileConfigurationCheckSettingIgnoredTypes() throws { + let initial: [OneDeclarationPerFileConfiguration.IgnoredType] = [ + .actor, .class + ] + let configuration = OneDeclarationPerFileConfiguration(severityConfiguration: .warning, ignoredTypes: initial) + XCTAssertEqual(Set(initial), configuration.allowedTypes) + } + + func testOneDeclarationPerFileConfigurationGoodConfig() throws { + let ignoredTypes = OneDeclarationPerFileConfiguration.IgnoredType.all + let ignoredTypesString: [String] = ignoredTypes.map(\.rawValue) + .sorted() + let goodConfig: [String: Any] = [ + "severity": "error", + "ignored_types": ignoredTypesString, + ] + var configuration = OneDeclarationPerFileConfiguration() + try configuration.apply(configuration: goodConfig) + XCTAssertEqual(configuration.severityConfiguration.severity, .error) + XCTAssertEqual(configuration.allowedTypes, ignoredTypes) + } + + func testOneDeclarationPerFileConfigurationBadConfigWrongTypes() throws { + let badConfig: [String: Any] = [ + "severity": "error", + "ignored_types": ["clas"], + ] + var configuration = OneDeclarationPerFileConfiguration() + checkError(Issue.invalidConfiguration(ruleID: OneDeclarationPerFileRule.identifier)) { + try configuration.apply(configuration: badConfig) + } + } +} diff --git a/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift b/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift index fe04b07fda..a2246f8261 100644 --- a/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift +++ b/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift @@ -193,9 +193,3 @@ final class OpeningBraceRuleTests: SwiftLintTestCase { verifyRule(description, ruleConfiguration: ["ignore_multiline_function_signatures": true]) } } - -private extension Array where Element == Example { - func removing(_ examples: Self) -> Self { - filter { !examples.contains($0) } - } -} diff --git a/Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift b/Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift new file mode 100644 index 0000000000..bf1b72ce93 --- /dev/null +++ b/Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift @@ -0,0 +1,91 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class TypeBodyLengthConfigurationTests: SwiftLintTestCase { + func testDefaultConfiguration() { + let config = TypeBodyLengthConfiguration() + XCTAssertEqual(config.severityConfiguration.warning, 250) + XCTAssertEqual(config.severityConfiguration.error, 350) + XCTAssertEqual(config.excludedTypes, [.extension, .protocol]) + } + + func testApplyingCustomConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "warning": 150, + "error": 200, + "excluded_types": ["struct", "class"], + ] as [String: any Sendable] + ) + XCTAssertEqual(config.severityConfiguration.warning, 150) + XCTAssertEqual(config.severityConfiguration.error, 200) + XCTAssertEqual(config.excludedTypes, Set([.struct, .class])) + } + + func testApplyingOnlyExcludedTypesConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": ["actor", "enum"] + ] as [String: any Sendable] + ) + + // Severity should remain default + XCTAssertEqual(config.severityConfiguration.warning, 250) + XCTAssertEqual(config.severityConfiguration.error, 350) + + // Excluded types should be updated + XCTAssertEqual(config.excludedTypes, Set([.actor, .enum])) + } + + func testApplyingAllTypesAsExcludedConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": ["struct", "class", "actor", "enum", "extension", "protocol"] + ] as [String: any Sendable] + ) + XCTAssertEqual(config.excludedTypes, Set(TypeBodyLengthCheckType.allCases)) + } + + func testApplyingEmptyExcludedTypesConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": [] as [String] + ] as [String: any Sendable] + ) + XCTAssertTrue(config.excludedTypes.isEmpty) + } + + func testApplyingSingleExcludedTypeConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": ["extension"] + ] as [String: any Sendable] + ) + XCTAssertEqual(config.excludedTypes, Set([.extension])) + } + + func testInvalidExcludedTypeConfiguration() throws { + var config = TypeBodyLengthConfiguration() + checkError(Issue.invalidConfiguration(ruleID: TypeBodyLengthRule.identifier)) { + try config.apply( + configuration: [ + "excluded_types": ["invalid_type"] + ] as [String: any Sendable] + ) + } + XCTAssertEqual(config.excludedTypes, Set([.extension, .protocol])) + } + + func testTypeEnumComparability() { + XCTAssertEqual( + TypeBodyLengthCheckType.allCases.sorted(), + [.actor, .class, .enum, .extension, .protocol, .struct] + ) + } +} diff --git a/Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift b/Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift new file mode 100644 index 0000000000..9b2e04c769 --- /dev/null +++ b/Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift @@ -0,0 +1,73 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class TypeBodyLengthRuleTests: SwiftLintTestCase { + func testWarning() { + let example = Example(""" + actor A { + let x = 0 + let y = 1 + let z = 2 + } + """) + + XCTAssertEqual( + self.violations(example, configuration: ["warning": 2, "error": 4]), + [ + StyleViolation( + ruleDescription: TypeBodyLengthRule.description, + severity: .warning, + location: Location(file: nil, line: 1, character: 1), + reason: """ + Actor body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ + ), + ] + ) + } + + func testError() { + let example = Example(""" + class C { + let x = 0 + let y = 1 + let z = 2 + } + """) + + XCTAssertEqual( + self.violations(example, configuration: ["warning": 1, "error": 2]), + [ + StyleViolation( + ruleDescription: TypeBodyLengthRule.description, + severity: .error, + location: Location(file: nil, line: 1, character: 1), + reason: """ + Class body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ + ), + ] + ) + } + + func testViolationMessages() { + let types = TypeBodyLengthRule.description.triggeringExamples.flatMap { + self.violations($0, configuration: ["warning": 2]) + }.compactMap { + $0.reason.split(separator: " ", maxSplits: 1).first + } + + XCTAssertEqual( + types, + ["Actor", "Class", "Enum", "Extension", "Protocol", "Struct"] + ) + } + + private func violations(_ example: Example, configuration: Any? = nil) -> [StyleViolation] { + let config = makeConfig(configuration, TypeBodyLengthRule.identifier)! + return TestHelpers.violations(example, config: config) + } +} diff --git a/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift b/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift index 7f8589f401..f3fd7ad4e4 100644 --- a/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift +++ b/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift @@ -9,7 +9,10 @@ final class VerticalWhitespaceRuleTests: SwiftLintTestCase { // Test with custom `max_empty_lines` let maxEmptyLinesDescription = VerticalWhitespaceRule.description .with(nonTriggeringExamples: [Example("let aaaa = 0\n\n\n")]) - .with(triggeringExamples: [Example("struct AAAA {}\n\n\n\n")]) + .with(triggeringExamples: [ + Example("struct AAAA {}\n\n\n\n"), + Example("class BBBB {\n \n \n \n}"), + ]) .with(corrections: [:]) verifyRule(maxEmptyLinesDescription, @@ -23,6 +26,7 @@ final class VerticalWhitespaceRuleTests: SwiftLintTestCase { .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("class BB {\n \n \n↓ \n let b = 0\n}\n"): Example("class BB {\n \n \n let b = 0\n}\n"), ]) verifyRule(maxEmptyLinesDescription, diff --git a/Tests/FrameworkTests/BaselineTests.swift b/Tests/FrameworkTests/BaselineTests.swift index f341d0012d..a97585c827 100644 --- a/Tests/FrameworkTests/BaselineTests.swift +++ b/Tests/FrameworkTests/BaselineTests.swift @@ -47,10 +47,6 @@ final class BaselineTests: XCTestCase { DirectReturnRule.description, ] - private actor CurrentDirectoryHolder { - static var currentDirectoryPath: String? - } - private static func violations(for filePath: String?) -> [StyleViolation] { ruleDescriptions.violations(for: filePath) } @@ -59,17 +55,16 @@ final class BaselineTests: XCTestCase { Baseline(violations: ruleDescriptions.violations(for: filePath)) } + private nonisolated(unsafe) static var currentDirectoryPath: String? + override static func setUp() { super.setUp() - CurrentDirectoryHolder.currentDirectoryPath = FileManager.default.currentDirectoryPath + currentDirectoryPath = FileManager.default.currentDirectoryPath XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(temporaryDirectoryPath)) } override static func tearDown() { - if let currentDirectoryPath = CurrentDirectoryHolder.currentDirectoryPath { - XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(currentDirectoryPath)) - CurrentDirectoryHolder.currentDirectoryPath = nil - } + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(currentDirectoryPath!)) super.tearDown() } diff --git a/Tests/FrameworkTests/CollectingRuleTests.swift b/Tests/FrameworkTests/CollectingRuleTests.swift index 04a11a39e3..7e87dc3440 100644 --- a/Tests/FrameworkTests/CollectingRuleTests.swift +++ b/Tests/FrameworkTests/CollectingRuleTests.swift @@ -136,7 +136,7 @@ extension MockCollectingRule { @RuleConfigurationDescriptionBuilder var configurationDescription: some Documentable { RuleConfigurationOption.noOptions } static var description: RuleDescription { - RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint) + RuleDescription(identifier: "mock_test_rule_for_swiftlint_tests", name: "", description: "", kind: .lint) } static var configuration: Configuration? { Configuration(rulesMode: .onlyConfiguration([identifier]), ruleList: RuleList(rules: self)) diff --git a/Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift b/Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift new file mode 100644 index 0000000000..1d69eb140c --- /dev/null +++ b/Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift @@ -0,0 +1,87 @@ +@testable import SwiftLintCore +@testable import SwiftLintFramework +import XCTest + +final class ConditionallySourceKitFreeTests: XCTestCase { + // Mock rule for testing ConditionallySourceKitFree protocol + private struct MockConditionalRule: Rule, ConditionallySourceKitFree { + static let description = RuleDescription( + identifier: "mock_conditional", + name: "Mock Conditional Rule", + description: "A mock rule for testing ConditionallySourceKitFree", + kind: .style + ) + + var configuration = SeverityConfiguration(.warning) + var isEffectivelySourceKitFree = true + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } + } + + private struct MockSourceKitFreeRule: Rule, SourceKitFreeRule { + static let description = RuleDescription( + identifier: "mock_sourcekit_free", + name: "Mock SourceKit Free Rule", + description: "A mock rule that is always SourceKit-free", + kind: .style + ) + + var configuration = SeverityConfiguration(.warning) + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } + } + + private struct MockRegularRule: Rule { + static let description = RuleDescription( + identifier: "mock_regular", + name: "Mock Regular Rule", + description: "A mock rule that requires SourceKit", + kind: .style + ) + + var configuration = SeverityConfiguration(.warning) + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } + } + + func testRequiresSourceKitForDifferentRuleTypes() { + // SourceKitFreeRule should not require SourceKit + let sourceKitFreeRule = MockSourceKitFreeRule() + XCTAssertFalse(sourceKitFreeRule.requiresSourceKit) + + // ConditionallySourceKitFree rule that is effectively SourceKit-free + var conditionalRuleFree = MockConditionalRule() + conditionalRuleFree.isEffectivelySourceKitFree = true + XCTAssertFalse(conditionalRuleFree.requiresSourceKit) + + // ConditionallySourceKitFree rule that requires SourceKit + var conditionalRuleRequires = MockConditionalRule() + conditionalRuleRequires.isEffectivelySourceKitFree = false + XCTAssertTrue(conditionalRuleRequires.requiresSourceKit) + + // Regular rule should require SourceKit + let regularRule = MockRegularRule() + XCTAssertTrue(regularRule.requiresSourceKit) + } + + func testTypeCheckingBehavior() { + // Verify instance-level checks work correctly + let sourceKitFreeRule: any Rule = MockSourceKitFreeRule() + XCTAssertTrue(sourceKitFreeRule is any SourceKitFreeRule) + XCTAssertFalse(sourceKitFreeRule is any ConditionallySourceKitFree) + + let conditionalRule: any Rule = MockConditionalRule() + XCTAssertFalse(conditionalRule is any SourceKitFreeRule) + XCTAssertTrue(conditionalRule is any ConditionallySourceKitFree) + + let regularRule: any Rule = MockRegularRule() + XCTAssertFalse(regularRule is any SourceKitFreeRule) + XCTAssertFalse(regularRule is any ConditionallySourceKitFree) + } +} diff --git a/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift b/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift index 43dadadc62..08efda167f 100644 --- a/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift +++ b/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift @@ -10,6 +10,7 @@ private extension Configuration { } } +// swiftlint:disable:next type_body_length extension ConfigurationTests { // MARK: - Rules Merging func testMerge() { diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 87691f4e79..cf98a15860 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -11,6 +11,12 @@ final class CustomRulesTests: SwiftLintTestCase { private var testFile: SwiftLintFile { SwiftLintFile(path: "\(TestResources.path())/test.txt")! } + override func invokeTest() { + CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + super.invokeTest() + } + } + func testCustomRuleConfigurationSetsCorrectlyWithMatchKinds() { let configDict = [ "my_custom_rule": [ @@ -27,6 +33,7 @@ final class CustomRulesTests: SwiftLintTestCase { comp.regex = "regex" comp.severityConfiguration = SeverityConfiguration(.error) comp.excludedMatchKinds = SyntaxKind.allKinds.subtracting([.comment]) + comp.executionMode = .default var compRules = CustomRulesConfiguration() compRules.customRuleConfigurations = [comp] do { @@ -54,6 +61,7 @@ final class CustomRulesTests: SwiftLintTestCase { comp.regex = "regex" comp.severityConfiguration = SeverityConfiguration(.error) comp.excludedMatchKinds = Set([.comment]) + comp.executionMode = .default var compRules = CustomRulesConfiguration() compRules.customRuleConfigurations = [comp] do { @@ -376,7 +384,7 @@ final class CustomRulesTests: SwiftLintTestCase { ] let example = Example( - """ + """ // swiftlint:disable custom1 custom3 return 10 """ @@ -506,6 +514,367 @@ final class CustomRulesTests: SwiftLintTestCase { XCTAssertTrue(violations[3].isSuperfluousDisableCommandViolation(for: "rule2")) } + // MARK: - ExecutionMode Tests (Phase 1) + + func testRegexConfigurationParsesExecutionMode() throws { + let configDict = [ + "regex": "pattern", + "execution_mode": "swiftsyntax", + ] + + var regexConfig = Configuration(identifier: "test_rule") + try regexConfig.apply(configuration: configDict) + XCTAssertEqual(regexConfig.executionMode, .swiftsyntax) + } + + func testRegexConfigurationParsesSourceKitMode() throws { + let configDict = [ + "regex": "pattern", + "execution_mode": "sourcekit", + ] + + var regexConfig = Configuration(identifier: "test_rule") + try regexConfig.apply(configuration: configDict) + XCTAssertEqual(regexConfig.executionMode, .sourcekit) + } + + func testRegexConfigurationWithoutModeIsDefault() throws { + let configDict = [ + "regex": "pattern", + ] + + var regexConfig = Configuration(identifier: "test_rule") + try regexConfig.apply(configuration: configDict) + XCTAssertEqual(regexConfig.executionMode, .default) + } + + func testRegexConfigurationRejectsInvalidMode() { + let configDict = [ + "regex": "pattern", + "execution_mode": "invalid_mode", + ] + + var regexConfig = Configuration(identifier: "test_rule") + checkError(Issue.invalidConfiguration(ruleID: CustomRules.identifier)) { + try regexConfig.apply(configuration: configDict) + } + } + + func testCustomRulesConfigurationParsesDefaultExecutionMode() throws { + let configDict: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "my_rule": [ + "regex": "pattern", + ], + ] + + var customRulesConfig = CustomRulesConfiguration() + try customRulesConfig.apply(configuration: configDict) + XCTAssertEqual(customRulesConfig.defaultExecutionMode, .swiftsyntax) + XCTAssertEqual(customRulesConfig.customRuleConfigurations.count, 1) + XCTAssertEqual(customRulesConfig.customRuleConfigurations[0].executionMode, .default) + } + + func testCustomRulesAppliesDefaultModeToRulesWithoutExplicitMode() throws { + let configDict: [String: Any] = [ + "default_execution_mode": "sourcekit", + "rule1": [ + "regex": "pattern1", + ], + "rule2": [ + "regex": "pattern2", + "execution_mode": "swiftsyntax", + ], + ] + + var customRulesConfig = CustomRulesConfiguration() + try customRulesConfig.apply(configuration: configDict) + XCTAssertEqual(customRulesConfig.defaultExecutionMode, .sourcekit) + XCTAssertEqual(customRulesConfig.customRuleConfigurations.count, 2) + + // rule1 should have default mode + let rule1 = customRulesConfig.customRuleConfigurations.first { $0.identifier == "rule1" } + XCTAssertEqual(rule1?.executionMode, .default) + + // rule2 should keep its explicit mode + let rule2 = customRulesConfig.customRuleConfigurations.first { $0.identifier == "rule2" } + XCTAssertEqual(rule2?.executionMode, .swiftsyntax) + } + + func testCustomRulesConfigurationRejectsInvalidDefaultMode() { + let configDict: [String: Any] = [ + "default_execution_mode": "invalid", + "my_rule": [ + "regex": "pattern", + ], + ] + + var customRulesConfig = CustomRulesConfiguration() + checkError(Issue.invalidConfiguration(ruleID: CustomRules.identifier)) { + try customRulesConfig.apply(configuration: configDict) + } + } + + func testExecutionModeIncludedInCacheDescription() { + var regexConfig = Configuration(identifier: "test_rule") + regexConfig.regex = "pattern" + regexConfig.executionMode = .swiftsyntax + + XCTAssertTrue(regexConfig.cacheDescription.contains("swiftsyntax")) + } + + func testExecutionModeAffectsHash() { + var config1 = Configuration(identifier: "test_rule") + config1.regex = "pattern" + config1.executionMode = .swiftsyntax + + var config2 = Configuration(identifier: "test_rule") + config2.regex = "pattern" + config2.executionMode = .sourcekit + + var config3 = Configuration(identifier: "test_rule") + config3.regex = "pattern" + config3.executionMode = .default + + // Different execution modes should produce different hashes + XCTAssertNotEqual(config1.hashValue, config2.hashValue) + XCTAssertNotEqual(config1.hashValue, config3.hashValue) + XCTAssertNotEqual(config2.hashValue, config3.hashValue) + } + + // MARK: - Phase 2 Tests: SwiftSyntax Mode Execution + + func testCustomRuleUsesSwiftSyntaxModeWhenConfigured() throws { + // Test that a rule configured with swiftsyntax mode works correctly + let customRules: [String: Any] = [ + "no_foo": [ + "regex": "\\bfoo\\b", + "execution_mode": "swiftsyntax", + "message": "Don't use foo", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].ruleIdentifier, "no_foo") + XCTAssertEqual(violations[0].reason, "Don't use foo") + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 5) + } + + func testCustomRuleWithoutMatchKindsUsesSwiftSyntaxByDefault() throws { + // When default_execution_mode is swiftsyntax, rules without match_kinds should use it + let customRules: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "no_bar": [ + "regex": "\\bbar\\b", + "message": "Don't use bar", + ], + ] + + let example = Example("let bar = 42 // bar is not allowed") + let violations = try violations(forExample: example, customRules: customRules) + + // Should find both occurrences of 'bar' since no match_kinds filtering + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 5) + XCTAssertEqual(violations[1].location.line, 1) + XCTAssertEqual(violations[1].location.character, 18) + } + + func testCustomRuleDefaultsToSwiftSyntaxWhenNoModeSpecified() throws { + // When NO execution mode is specified (neither default nor per-rule), it should default to swiftsyntax + let customRules: [String: Any] = [ + "no_foo": [ + "regex": "\\bfoo\\b", + "message": "Don't use foo", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should work correctly with implicit swiftsyntax mode + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].ruleIdentifier, "no_foo") + XCTAssertEqual(violations[0].reason, "Don't use foo") + + // Verify the rule is effectively SourceKit-free + let configuration = try SwiftLintFramework.Configuration(dict: [ + "only_rules": ["custom_rules"], + "custom_rules": customRules, + ]) + + guard let customRule = configuration.rules.first(where: { $0 is CustomRules }) as? CustomRules else { + XCTFail("Expected CustomRules in configuration") + return + } + + XCTAssertTrue(customRule.isEffectivelySourceKitFree, + "Rule should be effectively SourceKit-free when defaulting to swiftsyntax") + } + + func testCustomRuleWithMatchKindsUsesSwiftSyntaxWhenConfigured() throws { + // Phase 4: Rules with match_kinds in swiftsyntax mode should use SwiftSyntax bridging + let customRules: [String: Any] = [ + "comment_foo": [ + "regex": "foo", + "execution_mode": "swiftsyntax", + "match_kinds": "comment", + "message": "No foo in comments", + ], + ] + + let example = Example(""" + let foo = 42 // This foo should match + let bar = 42 // This should not match + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should only match 'foo' in comment, not in code + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 23) // Position of 'foo' in comment + } + + func testCustomRuleWithKindFilteringDefaultsToSwiftSyntax() throws { + // When using kind filtering without specifying mode, it should default to swiftsyntax + let customRules: [String: Any] = [ + "no_keywords": [ + "regex": "\\b\\w+\\b", + "excluded_match_kinds": "keyword", + "message": "Found non-keyword", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'foo' and '42' but not 'let' (keyword) + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].location.character, 5) // 'foo' + XCTAssertEqual(violations[1].location.character, 11) // '42' + + // Verify the rule is effectively SourceKit-free + let configuration = try SwiftLintFramework.Configuration(dict: [ + "only_rules": ["custom_rules"], + "custom_rules": customRules, + ]) + + guard let customRule = configuration.rules.first(where: { $0 is CustomRules }) as? CustomRules else { + XCTFail("Expected CustomRules in configuration") + return + } + + XCTAssertTrue(customRule.isEffectivelySourceKitFree, + "Rule with kind filtering should default to swiftsyntax mode") + } + + func testCustomRuleWithExcludedMatchKindsUsesSwiftSyntaxWithDefaultMode() throws { + // Phase 4: Rules with excluded_match_kinds should use SwiftSyntax when default mode is swiftsyntax + let customRules: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "no_foo_outside_comments": [ + "regex": "foo", + "excluded_match_kinds": "comment", + "message": "No foo outside comments", + ], + ] + + let example = Example(""" + let foo = 42 // This foo in comment should not match + let foobar = 42 + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'foo' in code but not in comment + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 5) // 'foo' in variable name + XCTAssertEqual(violations[1].location.line, 2) + XCTAssertEqual(violations[1].location.character, 5) // 'foo' in foobar + } + + func testSwiftSyntaxModeProducesSameResultsAsSourceKitForSimpleRules() throws { + // Test that both modes produce identical results for rules without kind filtering + let pattern = "\\bTODO\\b" + let message = "TODOs should be resolved" + + let swiftSyntaxRules: [String: Any] = [ + "todo_rule": [ + "regex": pattern, + "execution_mode": "swiftsyntax", + "message": message, + ], + ] + + let sourceKitRules: [String: Any] = [ + "todo_rule": [ + "regex": pattern, + "execution_mode": "sourcekit", + "message": message, + ], + ] + + let example = Example(""" + // TODO: Fix this later + func doSomething() { + // Another TODO item + print("TODO is not matched in strings") + } + """) + + let swiftSyntaxViolations = try violations(forExample: example, customRules: swiftSyntaxRules) + let sourceKitViolations = try violations(forExample: example, customRules: sourceKitRules) + + // Both modes should produce identical results + XCTAssertEqual(swiftSyntaxViolations.count, sourceKitViolations.count) + XCTAssertEqual(swiftSyntaxViolations.count, 3) // Two in comments, one in string + + // Verify locations match + for (ssViolation, skViolation) in zip(swiftSyntaxViolations, sourceKitViolations) { + XCTAssertEqual(ssViolation.location.line, skViolation.location.line) + XCTAssertEqual(ssViolation.location.character, skViolation.location.character) + XCTAssertEqual(ssViolation.reason, skViolation.reason) + } + } + + func testSwiftSyntaxModeWithCaptureGroups() throws { + // Test that capture groups work correctly in SwiftSyntax mode + let customRules: [String: Any] = [ + "number_suffix": [ + "regex": "\\b(\\d+)_suffix\\b", + "capture_group": 1, + "execution_mode": "swiftsyntax", + "message": "Number found", + ], + ] + + let example = Example("let value = 42_suffix + 100_suffix") + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 2) + // First capture group should highlight just the number part + XCTAssertEqual(violations[0].location.character, 13) // Position of "42" + XCTAssertEqual(violations[1].location.character, 25) // Position of "100" + } + + func testSwiftSyntaxModeRespectsIncludedExcludedPaths() throws { + // Verify that included/excluded path filtering works in SwiftSyntax mode + var regexConfig = Configuration(identifier: "test_rule") + regexConfig.regex = "pattern" + regexConfig.executionMode = .swiftsyntax + regexConfig.included = [try RegularExpression(pattern: "\\.swift$")] + regexConfig.excluded = [try RegularExpression(pattern: "Tests")] + + XCTAssertTrue(regexConfig.shouldValidate(filePath: "/path/to/file.swift")) + XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/file.m")) + XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/Tests/file.swift")) + } + // MARK: - Private private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { @@ -568,6 +937,142 @@ final class CustomRulesTests: SwiftLintTestCase { customRules.configuration = customRuleConfiguration return customRules } + + // MARK: - Phase 4 Tests: SwiftSyntax Mode WITH Kind Filtering + + func testSwiftSyntaxModeWithMatchKindsProducesCorrectResults() throws { + // Test various syntax kinds with SwiftSyntax bridging + let customRules: [String: Any] = [ + "keyword_test": [ + "regex": "\\b\\w+\\b", + "execution_mode": "swiftsyntax", + "match_kinds": "keyword", + "message": "Found keyword", + ], + ] + + let example = Example(""" + let value = 42 + func test() { + return value + } + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'let', 'func', and 'return' keywords + XCTAssertEqual(violations.count, 3) + + // Verify the locations correspond to keywords + let expectedLocations = [ + (line: 1, character: 1), // 'let' + (line: 2, character: 1), // 'func' + (line: 3, character: 5), // 'return' + ] + + for (index, expected) in expectedLocations.enumerated() { + XCTAssertEqual(violations[index].location.line, expected.line) + XCTAssertEqual(violations[index].location.character, expected.character) + } + } + + func testSwiftSyntaxModeWithExcludedKindsFiltersCorrectly() throws { + // Test that excluded kinds are properly filtered out + let customRules: [String: Any] = [ + "no_identifier": [ + "regex": "\\b\\w+\\b", + "execution_mode": "swiftsyntax", + "excluded_match_kinds": ["identifier", "typeidentifier"], + "message": "Found non-identifier", + ], + ] + + let example = Example(""" + let value: Int = 42 + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'let' (keyword) and '42' (number), but not 'value' or 'Int' + XCTAssertEqual(violations.count, 2) + } + + func testSwiftSyntaxModeHandlesComplexKindMatching() throws { + // Test matching multiple specific kinds + let customRules: [String: Any] = [ + "special_tokens": [ + "regex": "\\S+", + "execution_mode": "swiftsyntax", + "match_kinds": ["string", "number", "comment"], + "message": "Found special token", + ], + ] + + let example = Example(""" + let name = "Alice" // User name + let age = 25 + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match "Alice" (string), 25 (number), and "// User name" (comment) + // The regex \S+ will match non-whitespace sequences + XCTAssertGreaterThanOrEqual(violations.count, 3) + } + + func testSwiftSyntaxModeWorksWithCaptureGroups() throws { + // Test that capture groups work correctly with SwiftSyntax mode + let customRules: [String: Any] = [ + "string_content": [ + "regex": #""([^"]+)""#, + "execution_mode": "swiftsyntax", + "match_kinds": "string", + "capture_group": 1, + "message": "String content", + ], + ] + + let example = Example(#"let greeting = "Hello, World!""#) + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].location.character, 17) // Start of "Hello, World!" content + } + + func testSwiftSyntaxModeRespectsSourceKitModeOverride() throws { + // Test that explicit sourcekit mode overrides default swiftsyntax mode + let customRules: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "sourcekit_rule": [ + "regex": "foo", + "execution_mode": "sourcekit", + "match_kinds": "identifier", + "message": "Found foo", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should still work correctly with explicit sourcekit mode + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].location.character, 5) + } + + func testSwiftSyntaxModeHandlesEmptyBridging() throws { + // Test graceful handling when no tokens match the specified kinds + let customRules: [String: Any] = [ + "attribute_only": [ + "regex": "\\w+", + "execution_mode": "swiftsyntax", + "match_kinds": "attributeBuiltin", // Very specific kind that won't match normal code + "message": "Found attribute", + ], + ] + + let example = Example("let value = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should produce no violations since there are no built-in attributes + XCTAssertEqual(violations.count, 0) + } } private extension StyleViolation { diff --git a/Tests/FrameworkTests/EmptyFileTests.swift b/Tests/FrameworkTests/EmptyFileTests.swift index 4fe7c61404..feee49af3f 100644 --- a/Tests/FrameworkTests/EmptyFileTests.swift +++ b/Tests/FrameworkTests/EmptyFileTests.swift @@ -39,7 +39,7 @@ private struct DontLintEmptyFiles: ShouldLintEmptyFilesProtocol { static var shouldLintEmptyFiles: Bool { false } } -private struct RuleMock: CorrectableRule { +private struct RuleMock: CorrectableRule, SourceKitFreeRule { var configuration = SeverityConfiguration(.warning) static var description: RuleDescription { diff --git a/Tests/FrameworkTests/GlobTests.swift b/Tests/FrameworkTests/GlobTests.swift index f4ed2f38b6..9ac90d424e 100644 --- a/Tests/FrameworkTests/GlobTests.swift +++ b/Tests/FrameworkTests/GlobTests.swift @@ -38,7 +38,7 @@ final class GlobTests: SwiftLintTestCase { func testNoMatchOneCharacterInBracket() { let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level[ab].swift")) - XCTAssertTrue(files.isEmpty) + XCTAssertTrue(files.isEmpty) } func testMatchesCharacterInRange() { diff --git a/Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift b/Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift new file mode 100644 index 0000000000..43c248e1dc --- /dev/null +++ b/Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift @@ -0,0 +1,166 @@ +@testable import SwiftLintCore +import TestHelpers +import XCTest + +struct MockSeverityLevelsRule: Rule { + static let identifier = "test_severity_levels" + static let description = RuleDescription( + identifier: identifier, + name: "Test Severity Levels", + description: "A test rule for SeverityLevelsConfiguration", + kind: .style + ) + + var configuration = SeverityLevelsConfiguration(warning: 12, error: nil) + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } +} + +final class SeverityLevelsConfigurationTests: SwiftLintTestCase { + func testInitializationWithWarningOnly() { + let config = SeverityLevelsConfiguration(warning: 10) + XCTAssertEqual(config.warning, 10) + XCTAssertNil(config.error) + + let params = config.params + XCTAssertEqual(params.count, 1) + XCTAssertEqual(params[0].severity, .warning) + XCTAssertEqual(params[0].value, 10) + } + + func testInitializationWithWarningAndError() { + let config = SeverityLevelsConfiguration(warning: 10, error: 20) + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 20) + + let params = config.params + XCTAssertEqual(params.count, 2) + XCTAssertEqual(params[0].severity, .error) + XCTAssertEqual(params[0].value, 20) + XCTAssertEqual(params[1].severity, .warning) + XCTAssertEqual(params[1].value, 10) + } + + func testApplyConfigurationWithSingleElementArray() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: [15]) + + XCTAssertEqual(config.warning, 15) + XCTAssertNil(config.error) + } + + func testApplyConfigurationWithTwoElementArray() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: [10, 25]) + + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 25) + } + + func testApplyConfigurationWithMultipleElementArray() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: [10, 25, 50]) + + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 25) // Only first two elements are used + } + + func testApplyConfigurationWithEmptyArray() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.nothingApplied(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: [] as [Int]) + } + } + + func testApplyConfigurationWithInvalidArrayType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.nothingApplied(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: ["invalid"]) + } + } + + func testApplyConfigurationWithWarningOnlyDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: ["warning": 15]) + + XCTAssertEqual(config.warning, 15) + XCTAssertNil(config.error) + } + + func testApplyConfigurationWithWarningAndErrorDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: ["warning": 10, "error": 25]) + + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 25) + } + + func testApplyConfigurationWithErrorOnlyDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + try config.apply(configuration: ["error": 25]) + + XCTAssertEqual(config.warning, 12) // Should remain unchanged + XCTAssertEqual(config.error, 25) + } + + func testApplyConfigurationWithNilErrorDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 10, error: 20) + + try config.apply(configuration: ["error": nil as Int?]) + + XCTAssertEqual(config.warning, 10) + XCTAssertNil(config.error) + } + + func testApplyConfigurationWithWarningSetToNilError() throws { + var config = SeverityLevelsConfiguration(warning: 10, error: 20) + + try config.apply(configuration: ["warning": 15]) + + XCTAssertEqual(config.warning, 15) + XCTAssertNil(config.error) // Should be set to nil when warning is specified without error + } + + func testApplyConfigurationWithInvalidWarningType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.invalidConfiguration(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: ["warning": "invalid"]) + } + } + + func testApplyConfigurationWithInvalidErrorType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.invalidConfiguration(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: ["error": "invalid"]) + } + } + + func testApplyConfigurationWithInvalidConfigurationType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.nothingApplied(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: "invalid") + } + } + + func testApplyConfigurationWithEmptyDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 12, error: 15) + + try config.apply(configuration: [:] as [String: Any]) + + XCTAssertEqual(config.warning, 12) + XCTAssertEqual(config.error, 15) // Should remain unchanged when nothing is applied + } +} diff --git a/Tests/FrameworkTests/SourceKitCrashTests.swift b/Tests/FrameworkTests/SourceKitCrashTests.swift index fbb429c92b..553aaaf025 100644 --- a/Tests/FrameworkTests/SourceKitCrashTests.swift +++ b/Tests/FrameworkTests/SourceKitCrashTests.swift @@ -2,6 +2,12 @@ import XCTest final class SourceKitCrashTests: SwiftLintTestCase { + override func invokeTest() { + CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + super.invokeTest() + } + } + func testAssertHandlerIsNotCalledOnNormalFile() { let file = SwiftLintFile(contents: "A file didn't crash SourceKitService") file.sourcekitdFailed = false diff --git a/Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift b/Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift new file mode 100644 index 0000000000..8fdb9da72a --- /dev/null +++ b/Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift @@ -0,0 +1,171 @@ +import SourceKittenFramework +import SwiftIDEUtils +@testable import SwiftLintCore +import SwiftSyntax +import TestHelpers +import XCTest + +final class SwiftSyntaxKindBridgeTests: SwiftLintTestCase { + func testBasicKeywordMapping() { + // Test basic keyword mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.keyword), .keyword) + } + + func testIdentifierMapping() { + // Test identifier mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.identifier), .identifier) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.dollarIdentifier), .identifier) + } + + func testCommentMapping() { + // Test comment mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.lineComment), .comment) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.blockComment), .comment) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.docLineComment), .docComment) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.docBlockComment), .docComment) + } + + func testLiteralMapping() { + // Test literal mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.stringLiteral), .string) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.integerLiteral), .number) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.floatLiteral), .number) + } + + func testOperatorAndTypeMapping() { + // Test operator and type mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.operator), .operator) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.type), .typeidentifier) + } + + func testSpecialCaseMapping() { + // Test special case mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.attribute), .attributeID) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.editorPlaceholder), .placeholder) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.ifConfigDirective), .poundDirectiveKeyword) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.argumentLabel), .argument) + } + + func testUnmappedClassifications() { + // Test classifications that have no mapping + XCTAssertNil(SwiftSyntaxKindBridge.mapClassification(.none)) + XCTAssertNil(SwiftSyntaxKindBridge.mapClassification(.regexLiteral)) + } + + func testSourceKittenSyntaxKindsGeneration() { + // Test that we can generate SourceKitten-compatible tokens from a simple Swift file + let contents = """ + // This is a comment + let x = 42 + """ + let file = SwiftLintFile(contents: contents) + + // Get the tokens from the bridge + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + + // Verify we got some tokens + XCTAssertFalse(tokens.isEmpty) + + // Check that we have expected token types + let tokenTypes = Set(tokens.map { $0.value.type }) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.comment.rawValue)) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.keyword.rawValue)) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.identifier.rawValue)) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.number.rawValue)) + } + + func testTokenOffsetAndLength() { + // Test that token offsets and lengths are correct + let contents = "let x = 42" + let file = SwiftLintFile(contents: contents) + + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + + // Find the "let" keyword token + let letToken = tokens.first { token in + if token.value.type == SyntaxKind.keyword.rawValue { + let start = token.value.offset.value + let end = token.value.offset.value + token.value.length.value + let startIndex = contents.index(contents.startIndex, offsetBy: start) + let endIndex = contents.index(contents.startIndex, offsetBy: end) + let substring = String(contents[startIndex.. Int { return value } + } + """ + let file = SwiftLintFile(contents: contents) + + // This should succeed without any fatal errors from the validation system + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + XCTAssertFalse(tokens.isEmpty) + } + + func testEmptyFileHandling() { + // Test that empty files are handled gracefully + let file = SwiftLintFile(contents: "") + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + XCTAssertTrue(tokens.isEmpty) + } + + func testWhitespaceOnlyFile() { + // Test files with only whitespace + let file = SwiftLintFile(contents: " \n\n \t \n") + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + // Whitespace is not classified, so we should get no tokens + XCTAssertTrue(tokens.isEmpty) + } +} diff --git a/Tests/FrameworkTests/SwiftVersionTests.swift b/Tests/FrameworkTests/SwiftVersionTests.swift index 17ab506cb5..c62735fa24 100644 --- a/Tests/FrameworkTests/SwiftVersionTests.swift +++ b/Tests/FrameworkTests/SwiftVersionTests.swift @@ -3,7 +3,13 @@ import XCTest final class SwiftVersionTests: SwiftLintTestCase { func testDetectSwiftVersion() { -#if compiler(>=6.1.0) +#if compiler(>=6.2.0) + let version = "6.2.0" +#elseif compiler(>=6.1.2) + let version = "6.1.2" +#elseif compiler(>=6.1.1) + let version = "6.1.1" +#elseif compiler(>=6.1.0) let version = "6.1.0" #elseif compiler(>=6.0.3) let version = "6.0.3" diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift deleted file mode 100644 index 1b65b9e47b..0000000000 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ /dev/null @@ -1,1460 +0,0 @@ -// GENERATED FILE. DO NOT EDIT! - -@testable import SwiftLintBuiltInRules -@testable import SwiftLintCore -import TestHelpers - -// swiftlint:disable:next blanket_disable_command -// swiftlint:disable file_length single_test_class type_name - -final class AccessibilityLabelForImageRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(AccessibilityLabelForImageRule.description) - } -} - -final class AccessibilityTraitForButtonRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(AccessibilityTraitForButtonRule.description) - } -} - -final class AnonymousArgumentInMultilineClosureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(AnonymousArgumentInMultilineClosureRule.description) - } -} - -final class ArrayInitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ArrayInitRule.description) - } -} - -final class AsyncWithoutAwaitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(AsyncWithoutAwaitRule.description) - } -} - -final class AttributeNameSpacingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(AttributeNameSpacingRule.description) - } -} - -final class AttributesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(AttributesRule.description) - } -} - -final class BalancedXCTestLifecycleRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(BalancedXCTestLifecycleRule.description) - } -} - -final class BlanketDisableCommandRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(BlanketDisableCommandRule.description) - } -} - -final class BlockBasedKVORuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(BlockBasedKVORule.description) - } -} - -final class CaptureVariableRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CaptureVariableRule.description) - } -} - -final class ClassDelegateProtocolRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ClassDelegateProtocolRule.description) - } -} - -final class ClosingBraceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ClosingBraceRule.description) - } -} - -final class ClosureBodyLengthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ClosureBodyLengthRule.description) - } -} - -final class ClosureEndIndentationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ClosureEndIndentationRule.description) - } -} - -final class ClosureParameterPositionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ClosureParameterPositionRule.description) - } -} - -final class ClosureSpacingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ClosureSpacingRule.description) - } -} - -final class CollectionAlignmentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CollectionAlignmentRule.description) - } -} - -final class ColonRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ColonRule.description) - } -} - -final class CommaInheritanceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CommaInheritanceRule.description) - } -} - -final class CommaRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CommaRule.description) - } -} - -final class CommentSpacingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CommentSpacingRule.description) - } -} - -final class CompilerProtocolInitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CompilerProtocolInitRule.description) - } -} - -final class ComputedAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ComputedAccessorsOrderRule.description) - } -} - -final class ConditionalReturnsOnNewlineRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ConditionalReturnsOnNewlineRule.description) - } -} - -final class ContainsOverFilterCountRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ContainsOverFilterCountRule.description) - } -} - -final class ContainsOverFilterIsEmptyRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ContainsOverFilterIsEmptyRule.description) - } -} - -final class ContainsOverFirstNotNilRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ContainsOverFirstNotNilRule.description) - } -} - -final class ContainsOverRangeNilComparisonRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ContainsOverRangeNilComparisonRule.description) - } -} - -final class ContrastedOpeningBraceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ContrastedOpeningBraceRule.description) - } -} - -final class ControlStatementRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ControlStatementRule.description) - } -} - -final class ConvenienceTypeRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ConvenienceTypeRule.description) - } -} - -final class CyclomaticComplexityRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(CyclomaticComplexityRule.description) - } -} - -final class DeploymentTargetRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DeploymentTargetRule.description) - } -} - -final class DirectReturnRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DirectReturnRule.description) - } -} - -final class DiscardedNotificationCenterObserverRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscardedNotificationCenterObserverRule.description) - } -} - -final class DiscouragedAssertRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscouragedAssertRule.description) - } -} - -final class DiscouragedDirectInitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscouragedDirectInitRule.description) - } -} - -final class DiscouragedNoneNameRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscouragedNoneNameRule.description) - } -} - -final class DiscouragedObjectLiteralRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscouragedObjectLiteralRule.description) - } -} - -final class DiscouragedOptionalBooleanRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscouragedOptionalBooleanRule.description) - } -} - -final class DiscouragedOptionalCollectionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DiscouragedOptionalCollectionRule.description) - } -} - -final class DuplicateConditionsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DuplicateConditionsRule.description) - } -} - -final class DuplicateEnumCasesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DuplicateEnumCasesRule.description) - } -} - -final class DuplicateImportsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DuplicateImportsRule.description) - } -} - -final class DuplicatedKeyInDictionaryLiteralRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DuplicatedKeyInDictionaryLiteralRule.description) - } -} - -final class DynamicInlineRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(DynamicInlineRule.description) - } -} - -final class EmptyCollectionLiteralRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyCollectionLiteralRule.description) - } -} - -final class EmptyCountRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyCountRule.description) - } -} - -final class EmptyEnumArgumentsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyEnumArgumentsRule.description) - } -} - -final class EmptyParametersRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyParametersRule.description) - } -} - -final class EmptyParenthesesWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyParenthesesWithTrailingClosureRule.description) - } -} - -final class EmptyStringRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyStringRule.description) - } -} - -final class EmptyXCTestMethodRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EmptyXCTestMethodRule.description) - } -} - -final class EnumCaseAssociatedValuesLengthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(EnumCaseAssociatedValuesLengthRule.description) - } -} - -final class ExpiringTodoRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExpiringTodoRule.description) - } -} - -final class ExplicitACLRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExplicitACLRule.description) - } -} - -final class ExplicitEnumRawValueRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExplicitEnumRawValueRule.description) - } -} - -final class ExplicitInitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExplicitInitRule.description) - } -} - -final class ExplicitSelfRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExplicitSelfRule.description) - } -} - -final class ExplicitTopLevelACLRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExplicitTopLevelACLRule.description) - } -} - -final class ExplicitTypeInterfaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExplicitTypeInterfaceRule.description) - } -} - -final class ExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ExtensionAccessModifierRule.description) - } -} - -final class FallthroughRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FallthroughRule.description) - } -} - -final class FatalErrorMessageRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FatalErrorMessageRule.description) - } -} - -final class FileHeaderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FileHeaderRule.description) - } -} - -final class FileLengthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FileLengthRule.description) - } -} - -final class FileNameNoSpaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FileNameNoSpaceRule.description) - } -} - -final class FileNameRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FileNameRule.description) - } -} - -final class FileTypesOrderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FileTypesOrderRule.description) - } -} - -final class FinalTestCaseRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FinalTestCaseRule.description) - } -} - -final class FirstWhereRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FirstWhereRule.description) - } -} - -final class FlatMapOverMapReduceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FlatMapOverMapReduceRule.description) - } -} - -final class ForWhereRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ForWhereRule.description) - } -} - -final class ForceCastRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ForceCastRule.description) - } -} - -final class ForceTryRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ForceTryRule.description) - } -} - -final class ForceUnwrappingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ForceUnwrappingRule.description) - } -} - -final class FunctionBodyLengthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FunctionBodyLengthRule.description) - } -} - -final class FunctionDefaultParameterAtEndRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FunctionDefaultParameterAtEndRule.description) - } -} - -final class FunctionParameterCountRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(FunctionParameterCountRule.description) - } -} - -final class GenericTypeNameRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(GenericTypeNameRule.description) - } -} - -final class IBInspectableInExtensionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(IBInspectableInExtensionRule.description) - } -} - -final class IdenticalOperandsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(IdenticalOperandsRule.description) - } -} - -final class IdentifierNameRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(IdentifierNameRule.description) - } -} - -final class ImplicitGetterRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ImplicitGetterRule.description) - } -} - -final class ImplicitReturnRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ImplicitReturnRule.description) - } -} - -final class ImplicitlyUnwrappedOptionalRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ImplicitlyUnwrappedOptionalRule.description) - } -} - -final class InclusiveLanguageRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(InclusiveLanguageRule.description) - } -} - -final class IndentationWidthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(IndentationWidthRule.description) - } -} - -final class InvalidSwiftLintCommandRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(InvalidSwiftLintCommandRule.description) - } -} - -final class IsDisjointRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(IsDisjointRule.description) - } -} - -final class JoinedDefaultParameterRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(JoinedDefaultParameterRule.description) - } -} - -final class LargeTupleRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LargeTupleRule.description) - } -} - -final class LastWhereRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LastWhereRule.description) - } -} - -final class LeadingWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LeadingWhitespaceRule.description) - } -} - -final class LegacyCGGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyCGGeometryFunctionsRule.description) - } -} - -final class LegacyConstantRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyConstantRule.description) - } -} - -final class LegacyConstructorRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyConstructorRule.description) - } -} - -final class LegacyHashingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyHashingRule.description) - } -} - -final class LegacyMultipleRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyMultipleRule.description) - } -} - -final class LegacyNSGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyNSGeometryFunctionsRule.description) - } -} - -final class LegacyObjcTypeRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyObjcTypeRule.description) - } -} - -final class LegacyRandomRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LegacyRandomRule.description) - } -} - -final class LetVarWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LetVarWhitespaceRule.description) - } -} - -final class LineLengthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LineLengthRule.description) - } -} - -final class LiteralExpressionEndIndentationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LiteralExpressionEndIndentationRule.description) - } -} - -final class LocalDocCommentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LocalDocCommentRule.description) - } -} - -final class LowerACLThanParentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(LowerACLThanParentRule.description) - } -} - -final class MarkRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MarkRule.description) - } -} - -final class MissingDocsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MissingDocsRule.description) - } -} - -final class ModifierOrderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ModifierOrderRule.description) - } -} - -final class MultilineArgumentsBracketsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultilineArgumentsBracketsRule.description) - } -} - -final class MultilineArgumentsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultilineArgumentsRule.description) - } -} - -final class MultilineFunctionChainsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultilineFunctionChainsRule.description) - } -} - -final class MultilineLiteralBracketsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultilineLiteralBracketsRule.description) - } -} - -final class MultilineParametersBracketsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultilineParametersBracketsRule.description) - } -} - -final class MultilineParametersRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultilineParametersRule.description) - } -} - -final class MultipleClosuresWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(MultipleClosuresWithTrailingClosureRule.description) - } -} - -final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NSLocalizedStringKeyRule.description) - } -} - -final class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NSLocalizedStringRequireBundleRule.description) - } -} - -final class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NSNumberInitAsFunctionReferenceRule.description) - } -} - -final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NSObjectPreferIsEqualRule.description) - } -} - -final class NestingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NestingRule.description) - } -} - -final class NimbleOperatorRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NimbleOperatorRule.description) - } -} - -final class NoEmptyBlockRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NoEmptyBlockRule.description) - } -} - -final class NoExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NoExtensionAccessModifierRule.description) - } -} - -final class NoFallthroughOnlyRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NoFallthroughOnlyRule.description) - } -} - -final class NoGroupingExtensionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NoGroupingExtensionRule.description) - } -} - -final class NoMagicNumbersRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NoMagicNumbersRule.description) - } -} - -final class NoSpaceInMethodCallRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NoSpaceInMethodCallRule.description) - } -} - -final class NonOptionalStringDataConversionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NonOptionalStringDataConversionRule.description) - } -} - -final class NonOverridableClassDeclarationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NonOverridableClassDeclarationRule.description) - } -} - -final class NotificationCenterDetachmentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NotificationCenterDetachmentRule.description) - } -} - -final class NumberSeparatorRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(NumberSeparatorRule.description) - } -} - -final class ObjectLiteralRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ObjectLiteralRule.description) - } -} - -final class OneDeclarationPerFileRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OneDeclarationPerFileRule.description) - } -} - -final class OpeningBraceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OpeningBraceRule.description) - } -} - -final class OperatorFunctionWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OperatorFunctionWhitespaceRule.description) - } -} - -final class OperatorUsageWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OperatorUsageWhitespaceRule.description) - } -} - -final class OptionalDataStringConversionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OptionalDataStringConversionRule.description) - } -} - -final class OptionalEnumCaseMatchingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OptionalEnumCaseMatchingRule.description) - } -} - -final class OrphanedDocCommentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OrphanedDocCommentRule.description) - } -} - -final class OverriddenSuperCallRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OverriddenSuperCallRule.description) - } -} - -final class OverrideInExtensionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(OverrideInExtensionRule.description) - } -} - -final class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PatternMatchingKeywordsRule.description) - } -} - -final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PeriodSpacingRule.description) - } -} - -final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferKeyPathRule.description) - } -} - -final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferNimbleRule.description) - } -} - -final class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferSelfInStaticReferencesRule.description) - } -} - -final class PreferSelfTypeOverTypeOfSelfRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferSelfTypeOverTypeOfSelfRule.description) - } -} - -final class PreferTypeCheckingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferTypeCheckingRule.description) - } -} - -final class PreferZeroOverExplicitInitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferZeroOverExplicitInitRule.description) - } -} - -final class PrefixedTopLevelConstantRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrefixedTopLevelConstantRule.description) - } -} - -final class PrivateActionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrivateActionRule.description) - } -} - -final class PrivateOutletRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrivateOutletRule.description) - } -} - -final class PrivateOverFilePrivateRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrivateOverFilePrivateRule.description) - } -} - -final class PrivateSubjectRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrivateSubjectRule.description) - } -} - -final class PrivateSwiftUIStatePropertyRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrivateSwiftUIStatePropertyRule.description) - } -} - -final class PrivateUnitTestRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PrivateUnitTestRule.description) - } -} - -final class ProhibitedInterfaceBuilderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ProhibitedInterfaceBuilderRule.description) - } -} - -final class ProhibitedSuperRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ProhibitedSuperRule.description) - } -} - -final class ProtocolPropertyAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ProtocolPropertyAccessorsOrderRule.description) - } -} - -final class QuickDiscouragedCallRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(QuickDiscouragedCallRule.description) - } -} - -final class QuickDiscouragedFocusedTestRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(QuickDiscouragedFocusedTestRule.description) - } -} - -final class QuickDiscouragedPendingTestRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(QuickDiscouragedPendingTestRule.description) - } -} - -final class RawValueForCamelCasedCodableEnumRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RawValueForCamelCasedCodableEnumRule.description) - } -} - -final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ReduceBooleanRule.description) - } -} - -final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ReduceIntoRule.description) - } -} - -final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantDiscardableLetRule.description) - } -} - -final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantNilCoalescingRule.description) - } -} - -final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantObjcAttributeRule.description) - } -} - -final class RedundantOptionalInitializationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantOptionalInitializationRule.description) - } -} - -final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantSelfInClosureRule.description) - } -} - -final class RedundantSendableRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantSendableRule.description) - } -} - -final class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantSetAccessControlRule.description) - } -} - -final class RedundantStringEnumValueRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantStringEnumValueRule.description) - } -} - -final class RedundantTypeAnnotationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantTypeAnnotationRule.description) - } -} - -final class RedundantVoidReturnRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantVoidReturnRule.description) - } -} - -final class RequiredDeinitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RequiredDeinitRule.description) - } -} - -final class RequiredEnumCaseRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RequiredEnumCaseRule.description) - } -} - -final class ReturnArrowWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ReturnArrowWhitespaceRule.description) - } -} - -final class ReturnValueFromVoidFunctionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ReturnValueFromVoidFunctionRule.description) - } -} - -final class SelfBindingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SelfBindingRule.description) - } -} - -final class SelfInPropertyInitializationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SelfInPropertyInitializationRule.description) - } -} - -final class ShorthandArgumentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ShorthandArgumentRule.description) - } -} - -final class ShorthandOperatorRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ShorthandOperatorRule.description) - } -} - -final class ShorthandOptionalBindingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ShorthandOptionalBindingRule.description) - } -} - -final class SingleTestClassRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SingleTestClassRule.description) - } -} - -final class SortedEnumCasesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SortedEnumCasesRule.description) - } -} - -final class SortedFirstLastRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SortedFirstLastRule.description) - } -} - -final class SortedImportsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SortedImportsRule.description) - } -} - -final class StatementPositionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StatementPositionRule.description) - } -} - -final class StaticOperatorRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StaticOperatorRule.description) - } -} - -final class StaticOverFinalClassRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StaticOverFinalClassRule.description) - } -} - -final class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StrictFilePrivateRule.description) - } -} - -final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StrongIBOutletRule.description) - } -} - -final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SuperfluousElseRule.description) - } -} - -final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SwitchCaseAlignmentRule.description) - } -} - -final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SwitchCaseOnNewlineRule.description) - } -} - -final class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SyntacticSugarRule.description) - } -} - -final class TestCaseAccessibilityRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TestCaseAccessibilityRule.description) - } -} - -final class TodoRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TodoRule.description) - } -} - -final class ToggleBoolRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ToggleBoolRule.description) - } -} - -final class TrailingClosureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TrailingClosureRule.description) - } -} - -final class TrailingCommaRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TrailingCommaRule.description) - } -} - -final class TrailingNewlineRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TrailingNewlineRule.description) - } -} - -final class TrailingSemicolonRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TrailingSemicolonRule.description) - } -} - -final class TrailingWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TrailingWhitespaceRule.description) - } -} - -final class TypeBodyLengthRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TypeBodyLengthRule.description) - } -} - -final class TypeContentsOrderRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TypeContentsOrderRule.description) - } -} - -final class TypeNameRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TypeNameRule.description) - } -} - -final class TypesafeArrayInitRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(TypesafeArrayInitRule.description) - } -} - -final class UnavailableConditionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnavailableConditionRule.description) - } -} - -final class UnavailableFunctionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnavailableFunctionRule.description) - } -} - -final class UnhandledThrowingTaskRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnhandledThrowingTaskRule.description) - } -} - -final class UnneededBreakInSwitchRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnneededBreakInSwitchRule.description) - } -} - -final class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnneededOverrideRule.description) - } -} - -final class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnneededParenthesesInClosureArgumentRule.description) - } -} - -final class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnneededSynthesizedInitializerRule.description) - } -} - -final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnownedVariableCaptureRule.description) - } -} - -final class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UntypedErrorInCatchRule.description) - } -} - -final class UnusedClosureParameterRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedClosureParameterRule.description) - } -} - -final class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedControlFlowLabelRule.description) - } -} - -final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedDeclarationRule.description) - } -} - -final class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedEnumeratedRule.description) - } -} - -final class UnusedImportRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedImportRule.description) - } -} - -final class UnusedOptionalBindingRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedOptionalBindingRule.description) - } -} - -final class UnusedParameterRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedParameterRule.description) - } -} - -final class UnusedSetterValueRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedSetterValueRule.description) - } -} - -final class ValidIBInspectableRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(ValidIBInspectableRule.description) - } -} - -final class VerticalParameterAlignmentOnCallRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VerticalParameterAlignmentOnCallRule.description) - } -} - -final class VerticalParameterAlignmentRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VerticalParameterAlignmentRule.description) - } -} - -final class VerticalWhitespaceBetweenCasesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VerticalWhitespaceBetweenCasesRule.description) - } -} - -final class VerticalWhitespaceClosingBracesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VerticalWhitespaceClosingBracesRule.description) - } -} - -final class VerticalWhitespaceOpeningBracesRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VerticalWhitespaceOpeningBracesRule.description) - } -} - -final class VerticalWhitespaceRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VerticalWhitespaceRule.description) - } -} - -final class VoidFunctionInTernaryConditionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VoidFunctionInTernaryConditionRule.description) - } -} - -final class VoidReturnRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(VoidReturnRule.description) - } -} - -final class WeakDelegateRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(WeakDelegateRule.description) - } -} - -final class XCTFailMessageRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(XCTFailMessageRule.description) - } -} - -final class XCTSpecificMatcherRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(XCTSpecificMatcherRule.description) - } -} - -final class YodaConditionRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(YodaConditionRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_01.swift b/Tests/GeneratedTests/GeneratedTests_01.swift new file mode 100644 index 0000000000..39903d1af5 --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_01.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class AccessibilityLabelForImageRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AccessibilityLabelForImageRule.description) + } +} + +final class AccessibilityTraitForButtonRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AccessibilityTraitForButtonRule.description) + } +} + +final class AnonymousArgumentInMultilineClosureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AnonymousArgumentInMultilineClosureRule.description) + } +} + +final class ArrayInitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ArrayInitRule.description) + } +} + +final class AsyncWithoutAwaitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AsyncWithoutAwaitRule.description) + } +} + +final class AttributeNameSpacingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AttributeNameSpacingRule.description) + } +} + +final class AttributesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(AttributesRule.description) + } +} + +final class BalancedXCTestLifecycleRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(BalancedXCTestLifecycleRule.description) + } +} + +final class BlanketDisableCommandRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(BlanketDisableCommandRule.description) + } +} + +final class BlockBasedKVORuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(BlockBasedKVORule.description) + } +} + +final class CaptureVariableRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CaptureVariableRule.description) + } +} + +final class ClassDelegateProtocolRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ClassDelegateProtocolRule.description) + } +} + +final class ClosingBraceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ClosingBraceRule.description) + } +} + +final class ClosureBodyLengthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ClosureBodyLengthRule.description) + } +} + +final class ClosureEndIndentationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ClosureEndIndentationRule.description) + } +} + +final class ClosureParameterPositionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ClosureParameterPositionRule.description) + } +} + +final class ClosureSpacingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ClosureSpacingRule.description) + } +} + +final class CollectionAlignmentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CollectionAlignmentRule.description) + } +} + +final class ColonRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ColonRule.description) + } +} + +final class CommaInheritanceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CommaInheritanceRule.description) + } +} + +final class CommaRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CommaRule.description) + } +} + +final class CommentSpacingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CommentSpacingRule.description) + } +} + +final class CompilerProtocolInitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CompilerProtocolInitRule.description) + } +} + +final class ComputedAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ComputedAccessorsOrderRule.description) + } +} + +final class ConditionalReturnsOnNewlineRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ConditionalReturnsOnNewlineRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_02.swift b/Tests/GeneratedTests/GeneratedTests_02.swift new file mode 100644 index 0000000000..c8d05b2749 --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_02.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class ContainsOverFilterCountRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ContainsOverFilterCountRule.description) + } +} + +final class ContainsOverFilterIsEmptyRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ContainsOverFilterIsEmptyRule.description) + } +} + +final class ContainsOverFirstNotNilRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ContainsOverFirstNotNilRule.description) + } +} + +final class ContainsOverRangeNilComparisonRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ContainsOverRangeNilComparisonRule.description) + } +} + +final class ContrastedOpeningBraceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ContrastedOpeningBraceRule.description) + } +} + +final class ControlStatementRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ControlStatementRule.description) + } +} + +final class ConvenienceTypeRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ConvenienceTypeRule.description) + } +} + +final class CyclomaticComplexityRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(CyclomaticComplexityRule.description) + } +} + +final class DeploymentTargetRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DeploymentTargetRule.description) + } +} + +final class DirectReturnRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DirectReturnRule.description) + } +} + +final class DiscardedNotificationCenterObserverRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscardedNotificationCenterObserverRule.description) + } +} + +final class DiscouragedAssertRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscouragedAssertRule.description) + } +} + +final class DiscouragedDirectInitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscouragedDirectInitRule.description) + } +} + +final class DiscouragedNoneNameRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscouragedNoneNameRule.description) + } +} + +final class DiscouragedObjectLiteralRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscouragedObjectLiteralRule.description) + } +} + +final class DiscouragedOptionalBooleanRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscouragedOptionalBooleanRule.description) + } +} + +final class DiscouragedOptionalCollectionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DiscouragedOptionalCollectionRule.description) + } +} + +final class DuplicateConditionsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DuplicateConditionsRule.description) + } +} + +final class DuplicateEnumCasesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DuplicateEnumCasesRule.description) + } +} + +final class DuplicateImportsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DuplicateImportsRule.description) + } +} + +final class DuplicatedKeyInDictionaryLiteralRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DuplicatedKeyInDictionaryLiteralRule.description) + } +} + +final class DynamicInlineRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(DynamicInlineRule.description) + } +} + +final class EmptyCollectionLiteralRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyCollectionLiteralRule.description) + } +} + +final class EmptyCountRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyCountRule.description) + } +} + +final class EmptyEnumArgumentsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyEnumArgumentsRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_03.swift b/Tests/GeneratedTests/GeneratedTests_03.swift new file mode 100644 index 0000000000..d90a7c1169 --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_03.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class EmptyParametersRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyParametersRule.description) + } +} + +final class EmptyParenthesesWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyParenthesesWithTrailingClosureRule.description) + } +} + +final class EmptyStringRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyStringRule.description) + } +} + +final class EmptyXCTestMethodRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EmptyXCTestMethodRule.description) + } +} + +final class EnumCaseAssociatedValuesLengthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(EnumCaseAssociatedValuesLengthRule.description) + } +} + +final class ExpiringTodoRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExpiringTodoRule.description) + } +} + +final class ExplicitACLRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExplicitACLRule.description) + } +} + +final class ExplicitEnumRawValueRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExplicitEnumRawValueRule.description) + } +} + +final class ExplicitInitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExplicitInitRule.description) + } +} + +final class ExplicitSelfRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExplicitSelfRule.description) + } +} + +final class ExplicitTopLevelACLRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExplicitTopLevelACLRule.description) + } +} + +final class ExplicitTypeInterfaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExplicitTypeInterfaceRule.description) + } +} + +final class ExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ExtensionAccessModifierRule.description) + } +} + +final class FallthroughRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FallthroughRule.description) + } +} + +final class FatalErrorMessageRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FatalErrorMessageRule.description) + } +} + +final class FileHeaderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FileHeaderRule.description) + } +} + +final class FileLengthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FileLengthRule.description) + } +} + +final class FileNameNoSpaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FileNameNoSpaceRule.description) + } +} + +final class FileNameRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FileNameRule.description) + } +} + +final class FileTypesOrderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FileTypesOrderRule.description) + } +} + +final class FinalTestCaseRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FinalTestCaseRule.description) + } +} + +final class FirstWhereRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FirstWhereRule.description) + } +} + +final class FlatMapOverMapReduceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FlatMapOverMapReduceRule.description) + } +} + +final class ForWhereRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ForWhereRule.description) + } +} + +final class ForceCastRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ForceCastRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_04.swift b/Tests/GeneratedTests/GeneratedTests_04.swift new file mode 100644 index 0000000000..4cdd72ea8c --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_04.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class ForceTryRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ForceTryRule.description) + } +} + +final class ForceUnwrappingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ForceUnwrappingRule.description) + } +} + +final class FunctionBodyLengthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FunctionBodyLengthRule.description) + } +} + +final class FunctionDefaultParameterAtEndRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FunctionDefaultParameterAtEndRule.description) + } +} + +final class FunctionParameterCountRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(FunctionParameterCountRule.description) + } +} + +final class GenericTypeNameRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(GenericTypeNameRule.description) + } +} + +final class IBInspectableInExtensionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(IBInspectableInExtensionRule.description) + } +} + +final class IdenticalOperandsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(IdenticalOperandsRule.description) + } +} + +final class IdentifierNameRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(IdentifierNameRule.description) + } +} + +final class ImplicitGetterRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ImplicitGetterRule.description) + } +} + +final class ImplicitReturnRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ImplicitReturnRule.description) + } +} + +final class ImplicitlyUnwrappedOptionalRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ImplicitlyUnwrappedOptionalRule.description) + } +} + +final class InclusiveLanguageRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(InclusiveLanguageRule.description) + } +} + +final class IndentationWidthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(IndentationWidthRule.description) + } +} + +final class InvalidSwiftLintCommandRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(InvalidSwiftLintCommandRule.description) + } +} + +final class IsDisjointRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(IsDisjointRule.description) + } +} + +final class JoinedDefaultParameterRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(JoinedDefaultParameterRule.description) + } +} + +final class LargeTupleRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LargeTupleRule.description) + } +} + +final class LastWhereRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LastWhereRule.description) + } +} + +final class LeadingWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LeadingWhitespaceRule.description) + } +} + +final class LegacyCGGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyCGGeometryFunctionsRule.description) + } +} + +final class LegacyConstantRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyConstantRule.description) + } +} + +final class LegacyConstructorRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyConstructorRule.description) + } +} + +final class LegacyHashingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyHashingRule.description) + } +} + +final class LegacyMultipleRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyMultipleRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_05.swift b/Tests/GeneratedTests/GeneratedTests_05.swift new file mode 100644 index 0000000000..751aa6087c --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_05.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class LegacyNSGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyNSGeometryFunctionsRule.description) + } +} + +final class LegacyObjcTypeRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyObjcTypeRule.description) + } +} + +final class LegacyRandomRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LegacyRandomRule.description) + } +} + +final class LetVarWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LetVarWhitespaceRule.description) + } +} + +final class LineLengthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LineLengthRule.description) + } +} + +final class LiteralExpressionEndIndentationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LiteralExpressionEndIndentationRule.description) + } +} + +final class LocalDocCommentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LocalDocCommentRule.description) + } +} + +final class LowerACLThanParentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(LowerACLThanParentRule.description) + } +} + +final class MarkRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MarkRule.description) + } +} + +final class MissingDocsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MissingDocsRule.description) + } +} + +final class ModifierOrderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ModifierOrderRule.description) + } +} + +final class MultilineArgumentsBracketsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultilineArgumentsBracketsRule.description) + } +} + +final class MultilineArgumentsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultilineArgumentsRule.description) + } +} + +final class MultilineFunctionChainsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultilineFunctionChainsRule.description) + } +} + +final class MultilineLiteralBracketsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultilineLiteralBracketsRule.description) + } +} + +final class MultilineParametersBracketsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultilineParametersBracketsRule.description) + } +} + +final class MultilineParametersRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultilineParametersRule.description) + } +} + +final class MultipleClosuresWithTrailingClosureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultipleClosuresWithTrailingClosureRule.description) + } +} + +final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NSLocalizedStringKeyRule.description) + } +} + +final class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NSLocalizedStringRequireBundleRule.description) + } +} + +final class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NSNumberInitAsFunctionReferenceRule.description) + } +} + +final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NSObjectPreferIsEqualRule.description) + } +} + +final class NestingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NestingRule.description) + } +} + +final class NimbleOperatorRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NimbleOperatorRule.description) + } +} + +final class NoEmptyBlockRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoEmptyBlockRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift new file mode 100644 index 0000000000..014ababfd4 --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class NoExtensionAccessModifierRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoExtensionAccessModifierRule.description) + } +} + +final class NoFallthroughOnlyRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoFallthroughOnlyRule.description) + } +} + +final class NoGroupingExtensionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoGroupingExtensionRule.description) + } +} + +final class NoMagicNumbersRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoMagicNumbersRule.description) + } +} + +final class NoSpaceInMethodCallRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoSpaceInMethodCallRule.description) + } +} + +final class NonOptionalStringDataConversionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NonOptionalStringDataConversionRule.description) + } +} + +final class NonOverridableClassDeclarationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NonOverridableClassDeclarationRule.description) + } +} + +final class NotificationCenterDetachmentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NotificationCenterDetachmentRule.description) + } +} + +final class NumberSeparatorRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NumberSeparatorRule.description) + } +} + +final class ObjectLiteralRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ObjectLiteralRule.description) + } +} + +final class OneDeclarationPerFileRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OneDeclarationPerFileRule.description) + } +} + +final class OpeningBraceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OpeningBraceRule.description) + } +} + +final class OperatorFunctionWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OperatorFunctionWhitespaceRule.description) + } +} + +final class OperatorUsageWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OperatorUsageWhitespaceRule.description) + } +} + +final class OptionalDataStringConversionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OptionalDataStringConversionRule.description) + } +} + +final class OptionalEnumCaseMatchingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OptionalEnumCaseMatchingRule.description) + } +} + +final class OrphanedDocCommentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OrphanedDocCommentRule.description) + } +} + +final class OverriddenSuperCallRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OverriddenSuperCallRule.description) + } +} + +final class OverrideInExtensionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OverrideInExtensionRule.description) + } +} + +final class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PatternMatchingKeywordsRule.description) + } +} + +final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PeriodSpacingRule.description) + } +} + +final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferConditionListRule.description) + } +} + +final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferKeyPathRule.description) + } +} + +final class PreferNimbleRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferNimbleRule.description) + } +} + +final class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferSelfInStaticReferencesRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift new file mode 100644 index 0000000000..e503444dae --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class PreferSelfTypeOverTypeOfSelfRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferSelfTypeOverTypeOfSelfRule.description) + } +} + +final class PreferTypeCheckingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferTypeCheckingRule.description) + } +} + +final class PreferZeroOverExplicitInitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferZeroOverExplicitInitRule.description) + } +} + +final class PrefixedTopLevelConstantRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrefixedTopLevelConstantRule.description) + } +} + +final class PrivateActionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateActionRule.description) + } +} + +final class PrivateOutletRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateOutletRule.description) + } +} + +final class PrivateOverFilePrivateRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateOverFilePrivateRule.description) + } +} + +final class PrivateSubjectRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateSubjectRule.description) + } +} + +final class PrivateSwiftUIStatePropertyRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateSwiftUIStatePropertyRule.description) + } +} + +final class PrivateUnitTestRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateUnitTestRule.description) + } +} + +final class ProhibitedInterfaceBuilderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ProhibitedInterfaceBuilderRule.description) + } +} + +final class ProhibitedSuperRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ProhibitedSuperRule.description) + } +} + +final class ProtocolPropertyAccessorsOrderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ProtocolPropertyAccessorsOrderRule.description) + } +} + +final class QuickDiscouragedCallRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(QuickDiscouragedCallRule.description) + } +} + +final class QuickDiscouragedFocusedTestRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(QuickDiscouragedFocusedTestRule.description) + } +} + +final class QuickDiscouragedPendingTestRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(QuickDiscouragedPendingTestRule.description) + } +} + +final class RawValueForCamelCasedCodableEnumRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RawValueForCamelCasedCodableEnumRule.description) + } +} + +final class ReduceBooleanRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ReduceBooleanRule.description) + } +} + +final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ReduceIntoRule.description) + } +} + +final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantDiscardableLetRule.description) + } +} + +final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantNilCoalescingRule.description) + } +} + +final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantObjcAttributeRule.description) + } +} + +final class RedundantOptionalInitializationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantOptionalInitializationRule.description) + } +} + +final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantSelfInClosureRule.description) + } +} + +final class RedundantSendableRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantSendableRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift new file mode 100644 index 0000000000..068ffab49e --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantSetAccessControlRule.description) + } +} + +final class RedundantStringEnumValueRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantStringEnumValueRule.description) + } +} + +final class RedundantTypeAnnotationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantTypeAnnotationRule.description) + } +} + +final class RedundantVoidReturnRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantVoidReturnRule.description) + } +} + +final class RequiredDeinitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RequiredDeinitRule.description) + } +} + +final class RequiredEnumCaseRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RequiredEnumCaseRule.description) + } +} + +final class ReturnArrowWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ReturnArrowWhitespaceRule.description) + } +} + +final class ReturnValueFromVoidFunctionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ReturnValueFromVoidFunctionRule.description) + } +} + +final class SelfBindingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SelfBindingRule.description) + } +} + +final class SelfInPropertyInitializationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SelfInPropertyInitializationRule.description) + } +} + +final class ShorthandArgumentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ShorthandArgumentRule.description) + } +} + +final class ShorthandOperatorRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ShorthandOperatorRule.description) + } +} + +final class ShorthandOptionalBindingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ShorthandOptionalBindingRule.description) + } +} + +final class SingleTestClassRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SingleTestClassRule.description) + } +} + +final class SortedEnumCasesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SortedEnumCasesRule.description) + } +} + +final class SortedFirstLastRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SortedFirstLastRule.description) + } +} + +final class SortedImportsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SortedImportsRule.description) + } +} + +final class StatementPositionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StatementPositionRule.description) + } +} + +final class StaticOperatorRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StaticOperatorRule.description) + } +} + +final class StaticOverFinalClassRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StaticOverFinalClassRule.description) + } +} + +final class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StrictFilePrivateRule.description) + } +} + +final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StrongIBOutletRule.description) + } +} + +final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SuperfluousElseRule.description) + } +} + +final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SwitchCaseAlignmentRule.description) + } +} + +final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SwitchCaseOnNewlineRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift new file mode 100644 index 0000000000..1d8dc7614d --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -0,0 +1,158 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SyntacticSugarRule.description) + } +} + +final class TestCaseAccessibilityRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TestCaseAccessibilityRule.description) + } +} + +final class TodoRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TodoRule.description) + } +} + +final class ToggleBoolRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ToggleBoolRule.description) + } +} + +final class TrailingClosureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TrailingClosureRule.description) + } +} + +final class TrailingCommaRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TrailingCommaRule.description) + } +} + +final class TrailingNewlineRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TrailingNewlineRule.description) + } +} + +final class TrailingSemicolonRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TrailingSemicolonRule.description) + } +} + +final class TrailingWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TrailingWhitespaceRule.description) + } +} + +final class TypeBodyLengthRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TypeBodyLengthRule.description) + } +} + +final class TypeContentsOrderRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TypeContentsOrderRule.description) + } +} + +final class TypeNameRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TypeNameRule.description) + } +} + +final class TypesafeArrayInitRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(TypesafeArrayInitRule.description) + } +} + +final class UnavailableConditionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnavailableConditionRule.description) + } +} + +final class UnavailableFunctionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnavailableFunctionRule.description) + } +} + +final class UnhandledThrowingTaskRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnhandledThrowingTaskRule.description) + } +} + +final class UnneededBreakInSwitchRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededBreakInSwitchRule.description) + } +} + +final class UnneededOverrideRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededOverrideRule.description) + } +} + +final class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededParenthesesInClosureArgumentRule.description) + } +} + +final class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededSynthesizedInitializerRule.description) + } +} + +final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnownedVariableCaptureRule.description) + } +} + +final class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UntypedErrorInCatchRule.description) + } +} + +final class UnusedClosureParameterRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedClosureParameterRule.description) + } +} + +final class UnusedControlFlowLabelRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedControlFlowLabelRule.description) + } +} + +final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedDeclarationRule.description) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift new file mode 100644 index 0000000000..a5ce334fc5 --- /dev/null +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -0,0 +1,116 @@ +// GENERATED FILE. DO NOT EDIT! + +// swiftlint:disable:next blanket_disable_command superfluous_disable_command +// swiftlint:disable single_test_class type_name + +@testable import SwiftLintBuiltInRules +@testable import SwiftLintCore +import TestHelpers + +final class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedEnumeratedRule.description) + } +} + +final class UnusedImportRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedImportRule.description) + } +} + +final class UnusedOptionalBindingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedOptionalBindingRule.description) + } +} + +final class UnusedParameterRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedParameterRule.description) + } +} + +final class UnusedSetterValueRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedSetterValueRule.description) + } +} + +final class ValidIBInspectableRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(ValidIBInspectableRule.description) + } +} + +final class VerticalParameterAlignmentOnCallRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VerticalParameterAlignmentOnCallRule.description) + } +} + +final class VerticalParameterAlignmentRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VerticalParameterAlignmentRule.description) + } +} + +final class VerticalWhitespaceBetweenCasesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VerticalWhitespaceBetweenCasesRule.description) + } +} + +final class VerticalWhitespaceClosingBracesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VerticalWhitespaceClosingBracesRule.description) + } +} + +final class VerticalWhitespaceOpeningBracesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VerticalWhitespaceOpeningBracesRule.description) + } +} + +final class VerticalWhitespaceRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VerticalWhitespaceRule.description) + } +} + +final class VoidFunctionInTernaryConditionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VoidFunctionInTernaryConditionRule.description) + } +} + +final class VoidReturnRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(VoidReturnRule.description) + } +} + +final class WeakDelegateRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(WeakDelegateRule.description) + } +} + +final class XCTFailMessageRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(XCTFailMessageRule.description) + } +} + +final class XCTSpecificMatcherRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(XCTSpecificMatcherRule.description) + } +} + +final class YodaConditionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(YodaConditionRule.description) + } +} diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 981d667680..f2ddd54d8e 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -377,6 +377,7 @@ file_length: file_name: severity: warning excluded: ["LinuxMain.swift", "main.swift"] + excluded_paths: [] prefix_pattern: "" suffix_pattern: "\+.*" nested_type_separator: "." @@ -692,6 +693,7 @@ nesting: check_nesting_in_closures_and_statements: true always_allow_one_type_in_functions: false ignore_typealiases_and_associatedtypes: false + ignore_coding_keys: false meta: opt-in: false correctable: false @@ -735,6 +737,7 @@ no_space_in_method_call: correctable: true non_optional_string_data_conversion: severity: warning + include_variables: false meta: opt-in: false correctable: false @@ -785,6 +788,7 @@ object_literal: correctable: false one_declaration_per_file: severity: warning + allowed_types: [] meta: opt-in: true correctable: false @@ -847,6 +851,11 @@ period_spacing: meta: opt-in: true correctable: true +prefer_condition_list: + severity: warning + meta: + opt-in: true + correctable: true prefer_key_path: severity: warning restrict_to_standard_functions: true @@ -1196,6 +1205,7 @@ trailing_whitespace: type_body_length: warning: 250 error: 350 + excluded_types: [extension, protocol] meta: opt-in: false correctable: false diff --git a/Tests/TestHelpers/SwiftLintTestCase.swift b/Tests/TestHelpers/SwiftLintTestCase.swift index 3a99c513fd..ad8b591e3b 100644 --- a/Tests/TestHelpers/SwiftLintTestCase.swift +++ b/Tests/TestHelpers/SwiftLintTestCase.swift @@ -1,62 +1,10 @@ import SwiftLintFramework import XCTest -// swiftlint:disable:next blanket_disable_command -// swiftlint:disable test_case_accessibility - // swiftlint:disable:next balanced_xctest_lifecycle open class SwiftLintTestCase: XCTestCase { override open class func setUp() { super.setUp() RuleRegistry.registerAllRulesOnce() } - - // swiftlint:disable:next identifier_name - public func AsyncAssertFalse(_ condition: @autoclosure () async -> Bool, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let condition = await condition() - XCTAssertFalse(condition, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertTrue(_ condition: @autoclosure () async throws -> Bool, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async rethrows { - let condition = try await condition() - XCTAssertTrue(condition, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertEqual(_ expression1: @autoclosure () async throws -> T, - _ expression2: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async rethrows { - let value1 = try await expression1() - let value2 = try await expression2() - XCTAssertEqual(value1, value2, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertNotEqual(_ expression1: @autoclosure () async -> T, - _ expression2: @autoclosure () async -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let value1 = await expression1() - let value2 = await expression2() - XCTAssertNotEqual(value1, value2, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertNil(_ expression: @autoclosure () async -> T?, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let value = await expression() - XCTAssertNil(value, message(), file: file, line: line) - } } diff --git a/Tests/TestHelpers/TestHelpers.swift b/Tests/TestHelpers/TestHelpers.swift index bc4cc1ca07..10cdc8b23d 100644 --- a/Tests/TestHelpers/TestHelpers.swift +++ b/Tests/TestHelpers/TestHelpers.swift @@ -47,10 +47,10 @@ 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 .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 } + 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 } return merged(withChild: typedConfiguration, rootDirectory: rootDirectory) } } @@ -156,7 +156,7 @@ private func render(violations: [StyleViolation], in contents: String) -> String 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 } + let character = violation.location.character else { continue } let message = String(repeating: " ", count: character - 1) + "^ " + [ "\(violation.severity.rawValue): ", @@ -267,15 +267,15 @@ private func testCorrection(_ correction: (Example, Example), #endif var config = configuration if let correctionConfiguration = correction.0.configuration, - 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) { + 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) } config.assertCorrection(correction.0, expected: correction.1) - if testMultiByteOffsets && correction.0.testMultiByteOffsets { + if testMultiByteOffsets, correction.0.testMultiByteOffsets { config.assertCorrection(addEmoji(correction.0), expected: addEmoji(correction.1)) } } @@ -307,11 +307,11 @@ public extension XCTestCase { } guard let config = makeConfig( - ruleConfiguration, - ruleDescription.identifier, - skipDisableCommandTests: skipDisableCommandTests) else { - XCTFail("Failed to create configuration", file: (file), line: line) - return + ruleConfiguration, + ruleDescription.identifier, + skipDisableCommandTests: skipDisableCommandTests) else { + XCTFail("Failed to create configuration", file: (file), line: line) + return } let disableCommands: [String] diff --git a/Tests/generated_tests.bzl b/Tests/generated_tests.bzl new file mode 100644 index 0000000000..4a515ad780 --- /dev/null +++ b/Tests/generated_tests.bzl @@ -0,0 +1,21 @@ +# GENERATED FILE. DO NOT EDIT! + +load(":test_macros.bzl", "generated_test_shard") + +def generated_tests(copts, strict_concurrency_copts): + """Creates all generated test targets for SwiftLint rules. + + Args: + copts: Common compiler options + strict_concurrency_copts: Strict concurrency compiler options + """ + generated_test_shard("01", copts, strict_concurrency_copts) + generated_test_shard("02", copts, strict_concurrency_copts) + generated_test_shard("03", copts, strict_concurrency_copts) + generated_test_shard("04", copts, strict_concurrency_copts) + generated_test_shard("05", copts, strict_concurrency_copts) + generated_test_shard("06", copts, strict_concurrency_copts) + generated_test_shard("07", copts, strict_concurrency_copts) + generated_test_shard("08", copts, strict_concurrency_copts) + generated_test_shard("09", copts, strict_concurrency_copts) + generated_test_shard("10", copts, strict_concurrency_copts) diff --git a/Tests/test_macros.bzl b/Tests/test_macros.bzl new file mode 100644 index 0000000000..d799fcc0ab --- /dev/null +++ b/Tests/test_macros.bzl @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library", "swift_test") + +def generated_test_shard(shard_number, copts, strict_concurrency_copts): + """Creates a single generated test shard with library and test targets. + + Args: + shard_number: The shard number as a string + copts: Common compiler options + strict_concurrency_copts: Strict concurrency compiler options + """ + swift_library( + name = "GeneratedTests_{}.library".format(shard_number), + testonly = True, + srcs = ["GeneratedTests/GeneratedTests_{}.swift".format(shard_number)], + module_name = "GeneratedTests_{}".format(shard_number), + package_name = "SwiftLint", + deps = [ + ":TestHelpers", + ], + copts = copts + strict_concurrency_copts, + ) + + swift_test( + name = "GeneratedTests_{}".format(shard_number), + visibility = ["//visibility:public"], + deps = [":GeneratedTests_{}.library".format(shard_number)], + ) diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index fd284170b3..0000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,102 +0,0 @@ -trigger: -- main - -variables: - SKIP_INTEGRATION_TESTS: 'true' - -jobs: -- job: spm_linux - displayName: 'SPM, Linux : Swift 6.1' - pool: - vmImage: 'ubuntu-24.04' # "Noble Numbat" - container: swift:6.1-noble - steps: - - template: .azure/templates/run-make.yml - parameters: - rule: spm_test - -- job: bazel_linux - displayName: 'Bazel, Linux : Swift 6.0' - pool: - vmImage: 'ubuntu-24.04' - steps: - - script: | - export PATH="/usr/share/swift/usr/bin:$PATH" - git apply --ignore-whitespace .bcr/patches/no-warnings-as-errors.patch - bazel build :swiftlint - displayName: Build SwiftLint with Bazel - env: - CC: "clang" - -- job: tests_macos - displayName: 'Tests, macOS' - strategy: - maxParallel: '10' - matrix: - '14 : Xcode 15.4': - image: 'macOS-14' - xcode: '15.4' - # '14 : Xcode 16.3': Runs on Buildkite. - '15 : Xcode 15.4': - image: 'macOS-15' - xcode: '15.4' - '15 : Xcode 16.2': - image: 'macOS-15' - xcode: '16.2' - pool: - vmImage: $(image) - variables: - DEVELOPER_DIR: /Applications/Xcode_$(xcode).app - steps: - - script: make spm_test - displayName: Run tests - -- job: plugins_linux # Plugins shall be able to run on older Swift versions. - displayName: 'Plugins, Linux' - pool: - vmImage: 'ubuntu-24.04' # "Noble Numbat" - strategy: - maxParallel: '10' - matrix: - ': Swift 5.9': - image: swift:5.9-focal - ': Swift 5.10': - image: swift:5.10-noble - ': Swift 6.0': - image: swift:6.0-noble - ': Swift 6.1': - image: swift:6.1-noble - container: $[ variables['image'] ] - steps: - - template: .azure/templates/run-make.yml - parameters: - rule: spm_build_plugins - -- job: Jazzy - pool: - vmImage: 'macOS-14' - variables: - DEVELOPER_DIR: /Applications/Xcode_15.4.app - steps: - - script: make docs - displayName: Run Jazzy - - script: > - if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then - echo "Undocumented declarations:" - cat docs/undocumented.json - exit 1 - fi - displayName: Validate documentation coverage - - task: PublishPipelineArtifact@0 - inputs: - artifactName: 'API Docs' - targetPath: 'docs' - displayName: Publish API docs - - task: DownloadSecureFile@1 - condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') - inputs: - secureFile: doc_deploy_key - displayName: Download deploy key - - script: ./tools/push-docs - displayName: Push documentation to GitHub Pages - condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') diff --git a/bazel/SWXMLHash.BUILD b/bazel/SWXMLHash.BUILD new file mode 100644 index 0000000000..42d6233a58 --- /dev/null +++ b/bazel/SWXMLHash.BUILD @@ -0,0 +1,11 @@ +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "swift_library", +) + +swift_library( + name = "SWXMLHash", + srcs = glob(["Source/**/*.swift"]), + module_name = "SWXMLHash", + visibility = ["//visibility:public"], +) diff --git a/bazel/SwiftArgumentParser.BUILD b/bazel/SwiftArgumentParser.BUILD new file mode 100644 index 0000000000..7db3aee8db --- /dev/null +++ b/bazel/SwiftArgumentParser.BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ArgumentParserToolInfo", + srcs = glob(["Sources/ArgumentParserToolInfo/**/*.swift"]), + module_name = "ArgumentParserToolInfo", +) + +swift_library( + name = "ArgumentParser", + srcs = glob(["Sources/ArgumentParser/**/*.swift"]), + module_name = "ArgumentParser", + visibility = ["//visibility:public"], + deps = [ + ":ArgumentParserToolInfo", + ], +) diff --git a/bazel/repos.bzl b/bazel/repos.bzl index 14a928c8fb..bdc6080bcd 100644 --- a/bazel/repos.bzl +++ b/bazel/repos.bzl @@ -5,38 +5,38 @@ def swiftlint_repos(bzlmod = False): if not bzlmod: http_archive( name = "com_github_jpsim_sourcekitten", - sha256 = "d9c559166f01627826505b0e655b56a59f86938389e1739259e6ce49c9fd95f0", - strip_prefix = "SourceKitten-0.37.0", - url = "https://github.com/jpsim/SourceKitten/releases/download/0.37.0/SourceKitten-0.37.0.tar.gz", + sha256 = "604d2e5e547ef4280c959760cba0c9bd9be759c9555796cf7a73d9e1c9bcfc90", + strip_prefix = "SourceKitten-0.37.2", + url = "https://github.com/jpsim/SourceKitten/releases/download/0.37.2/SourceKitten-0.37.2.tar.gz", ) http_archive( name = "SwiftSyntax", - sha256 = "6572f60ca3c75c2a40f8ccec98c5cd0d3994599a39402d69b433381aaf2c1712", - strip_prefix = "swift-syntax-600.0.0", - url = "https://github.com/swiftlang/swift-syntax/archive/refs/tags/600.0.0.tar.gz", + sha256 = "02450ab3fd1d676fffd3719f3263293c51d567cae741fc340c68930388781286", + strip_prefix = "swift-syntax-601.0.1", + url = "https://github.com/swiftlang/swift-syntax/archive/refs/tags/601.0.1.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", + name = "com_github_apple_swift_argument_parser", + url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.6.1.tar.gz", + build_file = "@SwiftLint//bazel:SwiftArgumentParser.BUILD", + sha256 = "d2fbb15886115bb2d9bfb63d4c1ddd4080cbb4bfef2651335c5d3b9dd5f3c8ba", + strip_prefix = "swift-argument-parser-1.6.1", ) http_archive( - name = "sourcekitten_com_github_jpsim_yams", - url = "https://github.com/jpsim/Yams/releases/download/5.3.0/Yams-5.3.0.tar.gz", - sha256 = "a81c6b93f5d26bae1b619b7f8babbfe7c8abacf95b85916961d488888df886fb", - strip_prefix = "Yams-5.3.0", + name = "com_github_jpsim_yams", + url = "https://github.com/jpsim/Yams/archive/refs/tags/6.0.2.tar.gz", + sha256 = "a1ae9733755f77fd56e4b01081baea2a756d8cd4b6b7ec58dd971b249318df48", + strip_prefix = "Yams-6.0.2", ) http_archive( - name = "sourcekitten_com_github_drmohundro_SWXMLHash", + name = "com_github_drmohundro_SWXMLHash", url = "https://github.com/drmohundro/SWXMLHash/archive/refs/tags/7.0.2.tar.gz", - build_file = "@com_github_jpsim_sourcekitten//bazel:SWXMLHash.BUILD", - sha256 = "bafa037a09aa296f180e5613206748db5053b79aa09258c78d093ae9f8102a18", + build_file = "@SwiftLint//bazel:SWXMLHash.BUILD", + sha256 = "d7d600f062d6840b037fc1fb2ac3afce7a1c43ae430d78e22d7bd6f8e02cfc9d", strip_prefix = "SWXMLHash-7.0.2", ) @@ -49,7 +49,7 @@ def swiftlint_repos(bzlmod = False): ) http_archive( - name = "com_github_johnsundell_collectionconcurrencykit", + name = "swiftlint_com_github_johnsundell_collectionconcurrencykit", sha256 = "9083fe6f8b4f820bfb5ef5c555b31953116f158ec113e94c6406686e78da34aa", build_file = "@SwiftLint//bazel:CollectionConcurrencyKit.BUILD", strip_prefix = "CollectionConcurrencyKit-0.2.0", @@ -57,11 +57,11 @@ def swiftlint_repos(bzlmod = False): ) http_archive( - name = "com_github_krzyzanowskim_cryptoswift", - sha256 = "69b23102ff453990d03aff4d3fabd172d0667b2b3ed95730021d60a0f8d50d14", + name = "swiftlint_com_github_krzyzanowskim_cryptoswift", + sha256 = "81b1ba186e2edcff47bcc2a3b6a242df083ba2f64bfb42209f79090cb8d7f889", build_file = "@SwiftLint//bazel:CryptoSwift.BUILD", - strip_prefix = "CryptoSwift-1.8.4", - url = "https://github.com/krzyzanowskim/CryptoSwift/archive/refs/tags/1.8.4.tar.gz", + strip_prefix = "CryptoSwift-1.9.0", + url = "https://github.com/krzyzanowskim/CryptoSwift/archive/refs/tags/1.9.0.tar.gz", ) def _swiftlint_repos_bzlmod(_): diff --git a/tools/oss-check b/tools/oss-check index bb91df6bda..75f0c64fb6 100755 --- a/tools/oss-check +++ b/tools/oss-check @@ -297,6 +297,7 @@ def diff_and_report_changes_to_danger severity_changed = [] rule_id_changed = [] column_changed = [] + remaining_violations = [] new_violations.each do |line| fixed = fixed_violations.find { |other| line.equal_to?(other, [:message]) } @@ -315,8 +316,11 @@ def diff_and_report_changes_to_danger if fixed next column_changed << Change.new(:column, line, fixed) end + remaining_violations << line end + remaining_fixed = fixed_violations - (message_changed + severity_changed + rule_id_changed + column_changed).map(&:old) + # Print new and fixed violations to be processed by Danger. new_violations.each { |line| warn "This PR introduced a violation in #{repo.name}: #{line.to_full_message_with_linked_path(repo)}" @@ -346,13 +350,13 @@ def diff_and_report_changes_to_danger summary.puts column_changed.each { |change| change.print_as_diff(repo, summary) } summary.puts - summary.puts "### Fixed violations (#{fixed_violations.count})" + summary.puts "### Other fixed violations (#{remaining_fixed.count})" summary.puts - fixed_violations.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } + remaining_fixed.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } summary.puts - summary.puts "### New violations (#{new_violations.count})" + summary.puts "### Other new violations (#{remaining_violations.count})" summary.puts - new_violations.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } + remaining_violations.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } summary.puts summary.string @@ -416,6 +420,12 @@ def report_binary_size end end +def warmup + %w[branch main].each do |branch| + perform("../builds/swiftlint-#{branch} lint --no-cache --enable-all-rules", dir: "#{$working_dir}/Aerial") + end +end + ################################ # Script ################################ @@ -474,6 +484,7 @@ unless @options[:force] end setup_repos +warmup %w[branch main].each do |branch| generate_reports(branch) diff --git a/tools/push-docs b/tools/push-docs deleted file mode 100755 index 4c9e090e7b..0000000000 --- a/tools/push-docs +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -mkdir -p ~/.ssh && mv $DOWNLOADSECUREFILE_SECUREFILEPATH ~/.ssh/id_rsa -chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_rsa -ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts - -source_sha="$(git rev-parse HEAD)" -user="swiftlintbot@jpsim.com" -git config --global user.email "$user" -git config --global user.name "$user" -git clone git@github.com:realm/SwiftLint.git out - -cd out -git checkout gh-pages -git rm -rf . -rm -rf Carthage -cd .. - -cp -a docs/. out/. -cd out - -git add -A -git commit -m "Automated deployment to GitHub Pages: ${source_sha}" --allow-empty - -git push origin gh-pages