Compare commits

..

1 Commits

Author SHA1 Message Date
Marcelo Fabri a999c78155 Migrate `ibinspectable_in_extension` to SwiftSyntax 2022-10-04 01:06:23 -07:00
763 changed files with 16895 additions and 24645 deletions

View File

@ -1 +0,0 @@
.build

View File

@ -1,14 +1,8 @@
common --enable_bzlmod
try-import %workspace%/ci.bazelrc try-import %workspace%/ci.bazelrc
try-import %workspace%/user.bazelrc try-import %workspace%/user.bazelrc
build --macos_minimum_os=12.0 --host_macos_minimum_os=12.0 build --macos_minimum_os=12.0 --host_macos_minimum_os=12.0
build --disk_cache=~/.bazel_cache build --disk_cache=~/.bazel_cache
build --experimental_remote_cache_compression
build --experimental_remote_build_event_upload=minimal
build --nolegacy_important_outputs
build --swiftcopt=-warnings-as-errors
build:release \ build:release \
--compilation_mode=opt \ --compilation_mode=opt \

View File

@ -1 +1 @@
6.2.0 5.3.0

View File

@ -1,3 +0,0 @@
fixedReleaser:
login: jpsim
email: jp@jpsim.com

View File

@ -1,15 +0,0 @@
{
"homepage": "https://github.com/realm/SwiftLint",
"maintainers": [
{
"email": "jp@jpsim.com",
"github": "jpsim",
"name": "JP Simard"
}
],
"repository": [
"github:realm/SwiftLint"
],
"versions": [],
"yanked_versions": {}
}

View File

@ -1,24 +0,0 @@
shell_commands: &shell_commands
- "echo --- Downloading and extracting Swift $SWIFT_VERSION to $SWIFT_HOME"
- "mkdir $SWIFT_HOME"
- "curl https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2004/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu20.04.tar.gz | tar xvz --strip-components=1 -C $SWIFT_HOME"
tasks:
verify_targets_linux:
name: Verify targets (Linux)
platform: ubuntu2004
environment:
CC: "clang"
SWIFT_VERSION: "5.7.2"
SWIFT_HOME: "$HOME/swift-$SWIFT_VERSION"
PATH: "$PATH:$SWIFT_HOME/usr/bin"
shell_commands: *shell_commands
build_flags:
- "--action_env=PATH"
build_targets:
- '@SwiftLint//:swiftlint'
verify_targets_macos:
name: Verify targets (macOS)
platform: macos
build_targets:
- '@SwiftLint//:swiftlint'

View File

@ -1,5 +0,0 @@
{
"url": "https://github.com/realm/SwiftLint/releases/download/{TAG}/bazel.tar.gz",
"integrity": "",
"strip_prefix": ""
}

View File

@ -5,25 +5,17 @@ steps:
- bazel build :swiftlint - bazel build :swiftlint
- echo "+++ Test" - echo "+++ Test"
- bazel test --test_output=errors //Tests/... - bazel test --test_output=errors //Tests/...
- label: "SwiftPM"
commands:
- echo "+++ Test"
- swift test --parallel -Xswiftc -DDISABLE_FOCUSED_EXAMPLES
- label: "Danger" - label: "Danger"
commands: commands:
- echo "--- Install Bundler" - echo "--- Build Danger"
- gem install bundler - bazel build //tools:danger
- echo "--- Bundle Install"
- bundle install
- echo "+++ Run Danger" - echo "+++ Run Danger"
- bundle exec danger --verbose - ./bazel-bin/tools/danger --verbose
- label: "TSan Tests" - label: "Analyze"
commands:
- echo "+++ Analyze"
- bazel test -c opt --test_output=streamed --test_timeout=1800 --spawn_strategy=local analyze
- label: "TSan"
commands: commands:
- echo "+++ Test" - echo "+++ Test"
- bazel test --test_output=errors --build_tests_only --features=tsan --test_timeout=1000 //Tests/... - bazel test --xcode_version_config=//bazel:xcode_config --test_output=streamed --build_tests_only --features=tsan --test_timeout=600 //Tests/...
- label: "Sourcery"
commands:
- echo "+++ Run Sourcery"
- make --always-make sourcery
- echo "+++ Diff Files"
- git diff --quiet HEAD

View File

@ -1,5 +1,4 @@
* *
!Plugins
!Source !Source
!Tests !Tests
!Package.* !Package.*

View File

@ -9,10 +9,10 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Extract DOCKER_TAG using tag name - name: Extract DOCKER_TAG using tag name
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
# Xcode # Xcode
# #
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated ## Build generated
build/ build/
@ -21,7 +22,6 @@ xcuserdata
*.moved-aside *.moved-aside
*.xcuserstate *.xcuserstate
*.xcscmblueprint *.xcscmblueprint
default.profraw
## Obj-C/Swift specific ## Obj-C/Swift specific
*.hmap *.hmap

View File

@ -1,4 +1,4 @@
module: SwiftLintCore module: SwiftLintFramework
author: JP Simard, SwiftLint Contributors author: JP Simard, SwiftLint Contributors
author_url: https://jpsim.com author_url: https://jpsim.com
root_url: https://realm.github.io/SwiftLint/ root_url: https://realm.github.io/SwiftLint/
@ -7,14 +7,13 @@ github_file_prefix: https://github.com/realm/SwiftLint/tree/main
swift_build_tool: spm swift_build_tool: spm
theme: fullwidth theme: fullwidth
clean: true clean: true
copyright: '© 2023 [JP Simard](https://jpsim.com) under MIT.' copyright: '© 2020 [JP Simard](https://jpsim.com) under MIT.'
documentation: rule_docs/*.md documentation: rule_docs/*.md
hide_unlisted_documentation: true hide_unlisted_documentation: true
custom_categories_unlisted_prefix: '' custom_categories_unlisted_prefix: ''
exclude: exclude:
# TODO: Document extensions - Source/SwiftLintFramework/Rules/**/*.swift
- Source/SwiftLintCore/Extensions/*.swift
custom_categories: custom_categories:
- name: Rules - name: Rules
children: children:
@ -23,7 +22,7 @@ custom_categories:
children: children:
- CSVReporter - CSVReporter
- CheckstyleReporter - CheckstyleReporter
- CodeClimateReporter - CodeclimateReporter
- EmojiReporter - EmojiReporter
- GitHubActionsLoggingReporter - GitHubActionsLoggingReporter
- HTMLReporter - HTMLReporter

View File

@ -1,14 +1,11 @@
@testable import SwiftLintBuiltInRules import SwiftLintFramework
@_spi(TestHelper) import XCTest
@testable import SwiftLintCore
import SwiftLintTestHelpers
// swiftlint:disable:next blanket_disable_command
// swiftlint:disable file_length single_test_class type_name // swiftlint:disable file_length single_test_class type_name
{% for rule in types.structs %} {% for rule in types.structs %}
{% if rule.name|hasSuffix:"Rule" %} {% if rule.name|hasSuffix:"Rule" %}
class {{ rule.name }}GeneratedTests: SwiftLintTestCase { class {{ rule.name }}GeneratedTests: XCTestCase {
func testWithDefaultConfiguration() { func testWithDefaultConfiguration() {
verifyRule({{ rule.name }}.description) verifyRule({{ rule.name }}.description)
} }

View File

@ -1,5 +1,4 @@
/// The rule list containing all available rules built into SwiftLint. /// The rule list containing all available rules built into SwiftLint.
public let builtInRules: [Rule.Type] = [ public let primaryRuleList = RuleList(rules: [
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %} {% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
{% endfor %}] {% endfor %}] + extraRules())

View File

@ -1,7 +0,0 @@
/// The reporters list containing all the reporters built into SwiftLint.
public let reportersList: [Reporter.Type] = [
{% for reporter in types.structs where reporter.name|hasSuffix:"Reporter" %}
{{ reporter.name }}.self{% if not forloop.last %},{% endif %}
{% endfor %}
]

View File

@ -1,5 +1,4 @@
included: included:
- Plugins
- Source - Source
- Tests - Tests
excluded: excluded:
@ -8,55 +7,75 @@ analyzer_rules:
- unused_declaration - unused_declaration
- unused_import - unused_import
opt_in_rules: opt_in_rules:
- all - array_init
disabled_rules: - attributes
- anonymous_argument_in_multiline_closure - closure_end_indentation
- anyobject_protocol - closure_spacing
- closure_body_length - collection_alignment
- conditional_returns_on_newline - contains_over_filter_count
- convenience_type - contains_over_filter_is_empty
- discouraged_optional_collection - contains_over_first_not_nil
- explicit_acl - contains_over_range_nil_comparison
- explicit_enum_raw_value - discouraged_none_name
- explicit_top_level_acl - discouraged_object_literal
- explicit_type_interface - empty_collection_literal
- file_types_order - empty_count
- force_unwrapping - empty_string
- function_default_parameter_at_end - empty_xctest_method
- implicit_return - enum_case_associated_values_count
- implicitly_unwrapped_optional - explicit_init
- indentation_width - extension_access_modifier
- inert_defer - fallthrough
- missing_docs - fatal_error_message
- multiline_arguments - file_header
- multiline_arguments_brackets - file_name
- multiline_function_chains - first_where
- multiline_literal_brackets - flatmap_over_map_reduce
- multiline_parameters - identical_operands
- multiline_parameters_brackets - joined_default_parameter
- no_extension_access_modifier - last_where
- no_fallthrough_only - legacy_multiple
- no_grouping_extension - literal_expression_end_indentation
- no_magic_numbers - lower_acl_than_parent
- prefer_nimble - modifier_order
- nimble_operator
- nslocalizedstring_key
- number_separator
- object_literal
- operator_usage_whitespace
- overridden_super_call
- override_in_extension
- pattern_matching_keywords
- prefer_self_in_static_references - prefer_self_in_static_references
- prefixed_toplevel_constant - prefer_self_type_over_type_of_self
- redundant_self_in_closure - private_action
- required_deinit - private_outlet
- self_binding - prohibited_interface_builder
- sorted_enum_cases - prohibited_super_call
- strict_fileprivate - quick_discouraged_call
- superfluous_else - quick_discouraged_focused_test
- switch_case_on_newline - quick_discouraged_pending_test
- todo - reduce_into
- trailing_closure - redundant_nil_coalescing
- type_contents_order - redundant_type_annotation
- unused_capture_list - return_value_from_void_function
- vertical_whitespace_between_cases - single_test_class
- sorted_first_last
- sorted_imports
- static_operator
- strong_iboutlet
- test_case_accessibility
- toggle_bool
- unavailable_function
- unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- untyped_error_in_catch
- vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
- xct_specific_matcher
- yoda_condition
attributes:
always_on_line_above:
- "@OptionGroup"
identifier_name: identifier_name:
excluded: excluded:
- id - id
@ -65,27 +84,15 @@ number_separator:
minimum_length: 5 minimum_length: 5
file_name: file_name:
excluded: excluded:
- Exports.swift
- GeneratedTests.swift - GeneratedTests.swift
- SwiftSyntax+SwiftLint.swift
- TestHelpers.swift - TestHelpers.swift
balanced_xctest_lifecycle: &unit_test_configuration
test_parent_classes:
- SwiftLintTestCase
- XCTestCase
empty_xctest_method: *unit_test_configuration
single_test_class: *unit_test_configuration
function_body_length: 60
type_body_length: 400
custom_rules: custom_rules:
rule_id: rule_id:
included: Source/SwiftLintBuiltInRules/Rules/.+/\w+\.swift included: Source/SwiftLintFramework/Rules/.+/\w+\.swift
name: Rule ID name: Rule ID
message: Rule IDs must be all lowercase, snake case and not end with `rule` message: Rule IDs must be all lowercase, snake case and not end with `rule`
regex: ^\s+identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*") regex: identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
severity: error severity: error
fatal_error: fatal_error:
name: Fatal Error name: Fatal Error
@ -105,4 +112,3 @@ custom_rules:
unused_import: unused_import:
always_keep_imports: always_keep_imports:
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes - SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
- SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused

127
BUILD
View File

@ -1,78 +1,39 @@
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_universal_binary")
load( load(
"@build_bazel_rules_swift//swift:swift.bzl", "@build_bazel_rules_swift//swift:swift.bzl",
"swift_binary", "swift_binary",
"swift_library", "swift_library",
) )
load( load(
"@rules_xcodeproj//xcodeproj:defs.bzl", "@com_github_buildbuddy_io_rules_xcodeproj//xcodeproj:xcodeproj.bzl",
"xcode_schemes", "xcode_schemes",
"xcodeproj", "xcodeproj",
) )
# Targets # Targets
swift_library(
name = "SwiftLintCore",
srcs = glob(["Source/SwiftLintCore/**/*.swift"]),
module_name = "SwiftLintCore",
visibility = ["//visibility:public"],
deps = [
"@SwiftSyntax//:SwiftIDEUtils_opt",
"@SwiftSyntax//:SwiftOperators_opt",
"@SwiftSyntax//:SwiftParserDiagnostics_opt",
"@SwiftSyntax//:SwiftSyntaxBuilder_opt",
"@SwiftSyntax//:SwiftSyntax_opt",
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
"@sourcekitten_com_github_jpsim_yams//:Yams",
"@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable",
] + select({
"@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"],
"//conditions:default": [":DyldWarningWorkaround"],
}),
)
swift_library(
name = "SwiftLintBuiltInRules",
srcs = glob(["Source/SwiftLintBuiltInRules/**/*.swift"]),
module_name = "SwiftLintBuiltInRules",
visibility = ["//visibility:public"],
deps = [
":SwiftLintCore",
],
)
swift_library(
name = "SwiftLintExtraRules",
srcs = [
"Source/SwiftLintExtraRules/Exports.swift",
"@swiftlint_extra_rules//:extra_rules",
],
module_name = "SwiftLintExtraRules",
visibility = ["//visibility:public"],
deps = [
":SwiftLintCore",
],
)
swift_library( swift_library(
name = "SwiftLintFramework", name = "SwiftLintFramework",
srcs = glob( srcs = glob(
["Source/SwiftLintFramework/**/*.swift"], ["Source/SwiftLintFramework/**/*.swift"],
), exclude = ["Source/SwiftLintFramework/Rules/ExcludedFromBazel/ExtraRules.swift"],
) + ["@swiftlint_extra_rules//:extra_rules"],
module_name = "SwiftLintFramework", module_name = "SwiftLintFramework",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
":SwiftLintBuiltInRules", "@com_github_jpsim_sourcekitten//:SourceKittenFramework",
":SwiftLintCore", "@com_github_apple_swift_syntax//:SwiftSyntax",
":SwiftLintExtraRules", "@com_github_apple_swift_syntax//:SwiftSyntaxBuilder",
], "@com_github_apple_swift_syntax//:SwiftParser",
"@sourcekitten_com_github_jpsim_yams//:Yams",
] + select({
"@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"],
"//conditions:default": [],
}),
) )
swift_library( swift_binary(
name = "swiftlint.library", name = "swiftlint",
srcs = glob(["Source/swiftlint/**/*.swift"]), srcs = glob(["Source/swiftlint/**/*.swift"]),
module_name = "swiftlint",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
":SwiftLintFramework", ":SwiftLintFramework",
@ -82,41 +43,6 @@ swift_library(
], ],
) )
swift_binary(
name = "swiftlint",
visibility = ["//visibility:public"],
deps = [
":swiftlint.library",
],
)
apple_universal_binary(
name = "universal_swiftlint",
binary = ":swiftlint",
forced_cpus = [
"x86_64",
"arm64",
],
minimum_os_version = "12.0",
platform_type = "macos",
visibility = ["//visibility:public"],
)
filegroup(
name = "DyldWarningWorkaroundSources",
srcs = [
"Source/DyldWarningWorkaround/DyldWarningWorkaround.c",
"Source/DyldWarningWorkaround/include/objc_dupclass.h",
],
)
cc_library(
name = "DyldWarningWorkaround",
srcs = ["//:DyldWarningWorkaroundSources"],
includes = ["Source/DyldWarningWorkaround/include"],
alwayslink = True,
)
# Linting # Linting
filegroup( filegroup(
@ -135,8 +61,6 @@ filegroup(
srcs = [ srcs = [
"BUILD", "BUILD",
"LICENSE", "LICENSE",
"MODULE.bazel",
"//:DyldWarningWorkaroundSources",
"//:LintInputs", "//:LintInputs",
"//Tests:BUILD", "//Tests:BUILD",
"//bazel:release_files", "//bazel:release_files",
@ -168,37 +92,35 @@ shasum -a 256 "$${outs[0]}" > "$${outs[1]}"
xcodeproj( xcodeproj(
name = "xcodeproj", name = "xcodeproj",
build_mode = "bazel",
project_name = "SwiftLint", project_name = "SwiftLint",
schemes = [ schemes = [
xcode_schemes.scheme( xcode_schemes.scheme(
name = "SwiftLint", name = "SwiftLint",
launch_action = xcode_schemes.launch_action( launch_action = xcode_schemes.launch_action("//:swiftlint"),
"swiftlint",
args = [
"--progress",
],
),
test_action = xcode_schemes.test_action([ test_action = xcode_schemes.test_action([
"//Tests:CLITests",
"//Tests:SwiftLintFrameworkTests", "//Tests:SwiftLintFrameworkTests",
"//Tests:GeneratedTests",
"//Tests:IntegrationTests",
"//Tests:ExtraRulesTests", "//Tests:ExtraRulesTests",
]), ]),
), ),
], ],
top_level_targets = [ top_level_targets = [
"//:swiftlint", "//:swiftlint",
"//Tests:CLITests",
"//Tests:SwiftLintFrameworkTests", "//Tests:SwiftLintFrameworkTests",
"//Tests:GeneratedTests",
"//Tests:IntegrationTests",
"//Tests:ExtraRulesTests", "//Tests:ExtraRulesTests",
], ],
) )
# Analyze # Analyze
filegroup(
name = "SourceAndTestFiles",
srcs = glob([
"Source/**",
"Tests/SwiftLintFrameworkTests/**",
]),
)
sh_test( sh_test(
name = "analyze", name = "analyze",
srcs = ["//tools:test-analyze.sh"], srcs = ["//tools:test-analyze.sh"],
@ -206,6 +128,7 @@ sh_test(
"Package.resolved", "Package.resolved",
"Package.swift", "Package.swift",
":LintInputs", ":LintInputs",
":SourceAndTestFiles",
":swiftlint", ":swiftlint",
], ],
) )

View File

@ -2,557 +2,6 @@
#### Breaking #### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* Mention a rule's identifier in the console message that is printed when the
rule's associated configuration entry contains invalid values.
[SimplyDanny](https://github.com/SimplyDanny)
* Silence `xct_specific_matcher` rule on "one argument asserts" if there are
potential types or tuples involved in the comparison as types and tuples do
not conform to `Equatable`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4990](https://github.com/realm/SwiftLint/issues/4990)
* Add `grouping` option to the `sorted_imports` rule allowing
to sort groups of imports defined by their preceding attributes
(e.g. `@testable`, `@_exported`, ...).
[hiltonc](https://github.com/hiltonc)
* Do not trigger `redundant_self_in_closure` rule when another idenfier `x` in
scope shadows the field accessed by `self.x` to avoid semantical changes.
[SimplyDanny](https://github.com/SimplyDanny)
[#5010](https://github.com/realm/SwiftLint/issues/5010)
#### Bug Fixes
* The option `validates_start_with_lowercase` can now be disabled by setting it
to `off`.
[SimplyDanny](https://github.com/SimplyDanny)
[#5036](https://github.com/realm/SwiftLint/issues/5036)
* Do not trigger `prefer_self_in_static_references` rule on `typealias`
declarations in classes.
[SimplyDanny](https://github.com/SimplyDanny)
[#5009](https://github.com/realm/SwiftLint/issues/5009)
* Do not trigger `prefer_self_in_static_references` rule on collection types in
classes, but on initializers like `[C]()` in all types.
[SimplyDanny](https://github.com/SimplyDanny)
[#5042](https://github.com/realm/SwiftLint/issues/5042)
* Fix false positives on `redundant_objc_attribute` rule for enums
and private members.
[Martin Redington](https://github.com/mildm8nnered)
[#4633](https://github.com/realm/SwiftLint/issues/4633)
* Fix autocorrect for `CGIntersectionRect` in `legacy_cggeometry_functions`
rule.
[Haoocen](https://github.com/Haoocen)
[#5023](https://github.com/realm/SwiftLint/pull/5023)
* Fix false positives on `sorted_first_last` rule when `first`/`last` have
a predicate.
[woxtu](https://github.com/woxtu)
[#3023](https://github.com/realm/SwiftLint/issues/3023)
## 0.52.2: Crisper Clearer Pleats
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* Exclude simple assignments of the form `self.x = x` from being reported by
the `redundant_self_in_closure` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4988](https://github.com/realm/SwiftLint/issues/4988)
#### Bug Fixes
* Make `unhandled_throwing_task` opt-in instead of enabled by default. The rule
is still prone to false positives at this point, so this makes enabling the
rule a conscious decision by end-users.
[JP Simard](https://github.com/jpsim)
[#4987](https://github.com/realm/SwiftLint/issues/4987)
* Fix `unhandled_throwing_task` false positives when the `Task` is returned or
where the throwing code is handled in a `Result` initializer.
[JP Simard](https://github.com/jpsim)
[#4987](https://github.com/realm/SwiftLint/issues/4987)
## 0.52.1: Crisp Clear Pleats
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* None.
#### Bug Fixes
* Let the `validates_start_with_lowercase` option in name configurations
expect a severity (warning or error). Not setting it disables the check.
Boolean values are now deprecated. A `true` value enables the check as an
error for the time being to keep the previous behavior.
[SimplyDanny](https://github.com/SimplyDanny)
[#2180](https://github.com/realm/SwiftLint/issues/2180)
* Fixed a false positive in `unhandled_throwing_task`.
[kylebshr](https://github.com/kylebshr)
[#4984](https://github.com/realm/SwiftLint/issues/4984)
* Fix Bazel release tarball for compiling on macOS.
[JP Simard](https://github.com/jpsim)
[#4985](https://github.com/realm/SwiftLint/issues/4985)
## 0.52.0: Crisp Clear Pleats
#### Breaking
* The `attributes` rule now expects attributes with arguments to be placed
on their own line above the declaration they are supposed to influence.
This applies to attributes with any kinds of arguments including single
key path arguments which were previously handled in a different way. This
behavior can be turned off by setting `attributes_with_arguments_always_on_line_above`
to `false.
[SimplyDanny](https://github.com/SimplyDanny)
[#4843](https://github.com/realm/SwiftLint/issues/4843)
* The internal module structure for SwiftLint has changed to split the
monolithic `SwiftLintFramework` into new `SwiftLintCore` for core linter
infrastructure, `SwiftLintBuiltInRules` for built-in rules and
`SwiftLintExtraRules` to add your own native rules to SwiftLint.
[JP Simard](https://github.com/jpsim)
#### Experimental
* None.
#### Enhancements
* Add new `superfluous_else` rule that triggers on `if`-statements when an
attached `else`-block can be removed, because all branches of the previous
`if`-block(s) would certainly exit the current scope already.
[SimplyDanny](https://github.com/SimplyDanny)
* Add `sorted_enum_cases` rule which warns when enum cases are not sorted.
[kimdv](https://github.com/kimdv)
* Add new `redundant_self_in_closure` rule that triggers in closures on
explicitly used `self` when it's actually not needed due to:
* Strongly captured `self` (`{ [self] in ... }`)
* Closure used in a struct declaration (`self` can always be omitted)
* Anonymous closures that are directly called (`{ ... }()`) as they are
definitly not escaping
* Weakly captured `self` with explicit unwrapping
[SimplyDanny](https://github.com/SimplyDanny)
[#59](https://github.com/realm/SwiftLint/issues/59)
* Extend `xct_specific_matcher` rule to check for boolean asserts on (un)equal
comparisons. The rule can be configured with the matchers that should trigger
rule violations. By default, all matchers trigger, but that can be limited to
just `one-argument-asserts` or `two-argument-asserts`.
[SimplyDanny](https://github.com/SimplyDanny)
[JP Simard](https://github.com/jpsim)
[#3726](https://github.com/realm/SwiftLint/issues/3726)
* Trigger `prefer_self_in_static_references` rule on more type references.
[SimplyDanny](https://github.com/SimplyDanny)
* Adds a new `reporters` command, to improve discoverability of reporters.
[Martin Redington](https://github.com/mildm8nnered)
[#4819](https://github.com/realm/SwiftLint/issues/4819)
* Adds `test_parent_classes` option to the `no_magic_numbers` rule.
Violations within test classes will now be ignored by default.
[Martin Redington](https://github.com/mildm8nnered)
[#4896](https://github.com/realm/SwiftLint/issues/4896)
* Stop enforcing calls to super from the override functions `setUp()`,
`tearDown()`, `setUpWithError()`, and `tearDownWithError()` in `XCTestCase`
subclasses.
[AndrewDMontgomery](https://github.com/andrewdmontgomery)
[#4875](https://github.com/realm/SwiftLint/pull/4875)
* Prepend `warning: ` to error messages so that they show in Xcode.
[whiteio](https://github.com/whiteio)
[#4923](https://github.com/realm/SwiftLint/issues/4923)
* The `attributes` rule received a new boolean option
`attributes_with_arguments_always_on_line_above` which is `true` by default.
Setting it to `false` ensures that attributes with arguments like
`@Persisted(primaryKey: true)` don't violate the rule if they are on the same
line with the variable declaration.
[SimplyDanny](https://github.com/SimplyDanny)
[#4843](https://github.com/realm/SwiftLint/issues/4843)
* Add new `unhandled_throwing_task` rule that triggers when a Task with an
implicit error type has unhandled trys or errors thrown inside its body.
This results in errors being silently discarded, which may be unexpected.
See this forum thread for more details: https://forums.swift.org/t/56066
[kylebshr](https://github.com/kylebshr)
#### Bug Fixes
* Fix `lower_acl_than_parent` rule rewriter by preserving leading whitespace.
[SimplyDanny](https://github.com/SimplyDanny)
[#4860](https://github.com/realm/SwiftLint/issues/4860)
* Ignore block comments in `let_var_whitespace` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4871](https://github.com/realm/SwiftLint/issues/4871)
* Fix false positives in `indentation_width` rule.
[Sven Münnich](https://github.com/svenmuennich)
* Do not trigger `reduce_boolean` on `reduce` methods with a first named
argument that is different from `into`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4894](https://github.com/realm/SwiftLint/issues/4894)
* Work around dyld warning about duplicate SwiftSyntax classes.
[keith](https://github.com/keith)
[#4782](https://github.com/realm/SwiftLint/issues/4782)
* Improve lint times of SwiftLintPlugin by moving the
`excludedPaths(fileManager:)` operation out of the linting iterations.
[andyyhope](https://github.com/andyyhope)
[#4844](https://github.com/realm/SwiftLint/issues/4844)
## 0.51.0: bzllint
#### Breaking
* Deprecate the `unused_capture_list` rule in favor of the Swift compiler
warning. At the same time, make it an opt-in rule.
[Cyberbeni](https://github.com/Cyberbeni)
[#4656](https://github.com/realm/SwiftLint/issues/4656)
* Deprecate the `inert_defer` rule in favor of the Swift compiler warning.
At the same time, make it an opt-in rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4615](https://github.com/realm/SwiftLint/issues/4615)
* Interpret strings in `excluded` option of `identifier_name`,
`type_name` and `generic_type_name` rules as regular expression. Existing
configurations should remain working without notice as long as they don't
contain characters that must be escaped in regular expression.
[Moly](https://github.com/kyounh12)
[#4655](https://github.com/realm/SwiftLint/pull/4655)
#### Experimental
* None.
#### Enhancements
* Add `duplicate_conditions` rule which warns when a condition is duplicated
in separate branches of the same branching statement (if-else, or switch).
[1in1](https://github.com/1in1)
[#4666](https://github.com/realm/SwiftLint/issues/4666)
* Add local links to rule descriptions to every rule listed
in `Rule Directory.md`.
[kattouf](https://github.com/kattouf)
* Make forceExclude work with directly specified files.
[jimmya](https://github.com/jimmya)
[#4609](https://github.com/realm/SwiftLint/issues/4609)
* Adds `all` pseudo-rule for `opt_in_rules` - enables all opt in rules
that are not listed in `disabled_rules`
[Martin Redington](https://github.com/mildm8nnered)
[#4540](https://github.com/realm/SwiftLint/issues/4540)
* Separate analyzer rules as an independent section in the rule directory of
the reference.
[Ethan Wong](https://github.com/GetToSet)
[#4664](https://github.com/realm/SwiftLint/pull/4664)
* Add rule identifier to output of Emoji reporter.
[SimplyDanny](https://github.com/SimplyDanny)
[#4707](https://github.com/realm/SwiftLint/issues/4707)
* Add new `direct_return` rule that triggers on `return` statements returning
variables that have been declared in the statement before only.
[SimplyDanny](https://github.com/SimplyDanny)
* Add `period_spacing` opt-in rule that checks periods are not followed
by 2 or more spaces in comments.
[Julioacarrettoni](https://github.com/Julioacarrettoni)
[#4624](https://github.com/realm/SwiftLint/pull/4624)
* Allow to pass a rule identifier to the `swiftlint docs` command to open its
specific documentation website, e.g. `swiftlint docs for_where`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4707](https://github.com/realm/SwiftLint/issues/4707)
* Allow new Quick APIs `aroundEach` and `justBeforeEach` for
`quick_discouraged_call`.
[David Steinacher](https://github.com/stonko1994)
[#4626](https://github.com/realm/SwiftLint/issues/4626)
* Add `relative-path` reporter to generate reports with relative file paths.
[Roya1v](https://github.com/roya1v)
[#4660](https://github.com/realm/SwiftLint/issues/4660)
* Let `number_separator` rule trigger on misplaced separators, e.g. `10_00`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4637](https://github.com/realm/SwiftLint/issues/4637)
* Rewrite `multiline_arguments` rule using SwiftSyntax, ignoring trailing
closures.
[Marcelo Fabri](https://github.com/marcelofabri)
[#3399](https://github.com/realm/SwiftLint/issues/3399)
[#3605](https://github.com/realm/SwiftLint/issues/3605)
* Speed up linting by up to 6% updating to use a newer version of
`SwiftSyntax`.
[JP Simard](https://github.com/jpsim)
* Catch more valid `legacy_multiple` violations.
[JP Simard](https://github.com/jpsim)
* Catch more valid `no_magic_numbers` violations.
[JP Simard](https://github.com/jpsim)
* Add `blanket_disable_command` rule that checks whether
rules are re-enabled after being disabled.
[Martin Redington](https://github.com/mildm8nnered)
[#4731](https://github.com/realm/SwiftLint/pull/4731)
* Add `invalid_swiftlint_command` rule that validates
`// swiftlint:enable` and `disable` commands.
[Martin Redington](https://github.com/mildm8nnered)
[#4546](https://github.com/realm/SwiftLint/pull/4546)
* Improve `identifier_name` documentation.
[Martin Redington](https://github.com/mildm8nnered)
[#4767](https://github.com/realm/SwiftLint/issues/4767)
* Adds `include_multiline_strings` option to `indentation_width` rule.
[Martin Redington](https://github.com/mildm8nnered)
[#4248](https://github.com/realm/SwiftLint/issues/4248)
* Adds a new `summary` reporter, that displays the number of violations
of each rule in a text table.
[Martin Redington](https://github.com/mildm8nnered)
#### Bug Fixes
* Report violations in all `<scope>_length` rules when the error threshold is
smaller than the warning threshold.
[SimplyDanny](https://github.com/SimplyDanny)
[#4645](https://github.com/realm/SwiftLint/issues/4645)
* Consider custom attributes in `attributes` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4599](https://github.com/realm/SwiftLint/issues/4599)
* Fix whitespaces issue in auto-fix of `redundant_optional_initialization`
rule when multiple variable declaration are involved.
[SimplyDanny](https://github.com/SimplyDanny)
[#4794](https://github.com/realm/SwiftLint/issues/4794)
* Stop triggering `strict_fileprivate` rule on symbols implementing a protocol
in the same file.
[SimplyDanny](https://github.com/SimplyDanny)
[#4692](https://github.com/realm/SwiftLint/issues/4692)
* Fix false positives on `private_subject` rule when using
subjects inside functions.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4643](https://github.com/realm/SwiftLint/issues/4643)
* Fix for compiler directives masking subsequent `opening_brace`
violations.
[Martin Redington](https://github.com/mildm8nnered)
[#3712](https://github.com/realm/SwiftLint/issues/3712)
* Rewrite `explicit_type_interface` rule with SwiftSyntax fixing a
false-positive in if-case-let statements.
[SimplyDanny](https://github.com/SimplyDanny)
[#4548](https://github.com/realm/SwiftLint/issues/4548)
* Stop triggering `unused_capture_list` on captured variable that is only
referenced by a shorthand optional binding (`if let capturedVar { ... }`).
[SimplyDanny](https://github.com/SimplyDanny)
[#4804](https://github.com/realm/SwiftLint/issues/4804)
* Ensure that negative literals in initializers do not trigger
`no_magic_numbers` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4677](https://github.com/realm/SwiftLint/issues/4677)
* Fix caching of `indentation_width` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4121](https://github.com/realm/SwiftLint/issues/4121)
* Updated JUnit reporter to output error count and warning count.
[patricks](https://github.com/patricks)
[#4725](https://github.com/realm/SwiftLint/pull/4725)
* Fix correction on `lower_acl_than_parent` rule for `open` declarations.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4753](https://github.com/realm/SwiftLint/issues/4753)
* Fix `void_return` rule to support async and async throws functions.
[Mathias Schreck](https://github.com/lo1tuma)
[#4772](https://github.com/realm/SwiftLint/issues/4772)
* Fix false positives in `attributes` rule when using property wrappers
with keypath arguments.
[JP Simard](https://github.com/jpsim)
* Fix for `superfluous_disable_command` not being completely disabled
by `disable` commands.
[Martin Redington](https://github.com/mildm8nnered)
[#4788](https://github.com/realm/SwiftLint/issues/4788)
* Fixed correction for `trailing_comma` rule wrongly removing trailing
comments.
[Martin Redington](https://github.com/mildm8nnered)
[#4814](https://github.com/realm/SwiftLint/issues/4814)
## 0.50.3: Bundle of Towels
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* The `SwiftLintPlugin` SwiftPM plugin now uses a prebuilt binary on
macOS.
[Tony Arnold](https://github.com/tonyarnold)
[JP Simard](https://github.com/jpsim)
[#4558](https://github.com/realm/SwiftLint/issues/4558)
* Don't trigger `shorthand_operator` violations inside a shorthand
operator function declaration.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4611](https://github.com/realm/SwiftLint/issues/4611)
* The `balanced_xctest_lifecycle`, `single_test_class`,
`empty_xctest_method` and `test_case_accessibility` rules will now be
applied to subclasses of `QuickSpec`, as well as `XCTestCase`, by
default.
[Martin Redington](https://github.com/mildm8nnered)
* Add `test_parent_classes` option to `balanced_xctest_lifecycle`,
`single_test_class` and `empty_xctest_method` rules.
[Martin Redington](https://github.com/mildm8nnered)
[#4200](https://github.com/realm/SwiftLint/issues/4200)
* Show warnings in the console for Analyzer rules that are listed in the
`opt_in_rules` configuration section.
[SimplyDanny](https://github.com/SimplyDanny)
[#4612](https://github.com/realm/SwiftLint/issues/4612)
#### Bug Fixes
* Fix configuration parsing error in `unused_declaration` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4612](https://github.com/realm/SwiftLint/issues/4612)
* Skip `defer` statements being last in an `#if` block if the `#if`
statement is not itself the last statement in a block.
[SimplyDanny](https://github.com/SimplyDanny)
[#4615](https://github.com/realm/SwiftLint/issues/4615)
* Fix false positives in `empty_enum_arguments` when the called
expression is an identifier or an init call.
[Steffen Matthischke](https://github.com/heeaad)
[#4597](https://github.com/realm/SwiftLint/issues/4597)
* Fix correction issue in `comma` when there was too much whitespace
following the comma.
[JP Simard](https://github.com/jpsim)
## 0.50.1: Artisanal Clothes Pegs Fixup Edition
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* Moved the validation of doc comments in local scopes out of
`orphaned_doc_comment` and into a new opt-in `local_doc_comment` rule.
[JP Simard](https://github.com/jpsim)
[#4573](https://github.com/realm/SwiftLint/issues/4573)
* SwiftLint's Swift Package Build Tool Plugin will now only scan files
in the target being built.
[Tony Arnold](https://github.com/tonyarnold)
[#4406](https://github.com/realm/SwiftLint/pull/4406)
#### Bug Fixes
* Fix building with `swift build -c release`.
[JP Simard](https://github.com/jpsim)
[#4559](https://github.com/realm/SwiftLint/issues/4559)
[#4560](https://github.com/realm/SwiftLint/issues/4560)
* Fix false positives in `lower_acl_than_parent` when the nominal parent
is an extension.
[Steffen Matthischke](https://github.com/heeaad)
[#4564](https://github.com/realm/SwiftLint/issues/4564)
* Fix `minimum_fraction_length` handling in `number_separator`.
[JP Simard](https://github.com/jpsim)
[#4576](https://github.com/realm/SwiftLint/issues/4576)
* Fix false positives in `closure_spacing`.
[JP Simard](https://github.com/jpsim)
[#4565](https://github.com/realm/SwiftLint/issues/4565)
[#4582](https://github.com/realm/SwiftLint/issues/4582)
* Fix line count calculation for multiline string literals.
[JP Simard](https://github.com/jpsim)
[#4585](https://github.com/realm/SwiftLint/issues/4585)
* Fix false positives in `unused_closure_parameter` when using
identifiers with backticks.
[JP Simard](https://github.com/jpsim)
[#4588](https://github.com/realm/SwiftLint/issues/4588)
* Fix `type_name` regression where names with backticks would trigger
violations.
[JP Simard](https://github.com/jpsim)
[#4571](https://github.com/realm/SwiftLint/issues/4571)
## 0.50.0: Artisanal Clothes Pegs
#### Breaking
* SwiftLint now requires Swift 5.7 or higher to build. * SwiftLint now requires Swift 5.7 or higher to build.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpsim)
@ -563,12 +12,7 @@
* The `anyobject_protocol` rule is now deprecated and will be completely removed * The `anyobject_protocol` rule is now deprecated and will be completely removed
in a future release because it is now handled by the Swift compiler. in a future release because it is now handled by the Swift compiler.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpims)
* Built-in SwiftLint rules are no longer marked as `public` in
SwiftLintFramework. This only impacts the programmatic API for the
SwiftLintFramework module.
[JP Simard](https://github.com/jpsim)
#### Experimental #### Experimental
@ -585,178 +29,40 @@
If you notice any unexpected changes to lint results, please file an issue on If you notice any unexpected changes to lint results, please file an issue on
the SwiftLint issue tracker. We can look into it and if it's a SwiftSyntax the SwiftLint issue tracker. We can look into it and if it's a SwiftSyntax
parser regression we can re-file it upstream. parser regression we can re-file it upstream.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpims)
[#4031](https://github.com/realm/SwiftLint/issues/4031) [#4031](https://github.com/realm/SwiftLint/issues/4031)
* Rewrite some rules with SwiftSyntax, fixing some false positives and catching
more violations:
- `anonymous_argument_in_multiline_closure`
- `array_init`
- `attributes`
- `balanced_xctest_lifecycle`
- `block_based_kvo`
- `class_delegate_protocol`
- `closing_brace`
- `closure_body_length`
- `closure_parameter_position`
- `collection_alignment`
- `comment_spacing`
- `computed_accessors_order`
- `conditional_returns_on_newline`
- `contains_over_filter_count`
- `contains_over_filter_is_empty`
- `contains_over_first_not_nil`
- `contains_over_range_nil_comparison`
- `convenience_type`
- `deployment_target`
- `discarded_notification_center_observer`
- `discouraged_assert`
- `discouraged_direct_init`
- `discouraged_none_name`
- `discouraged_object_literal`
- `discouraged_optional_boolean`
- `duplicate_enum_cases`
- `duplicated_key_in_dictionary_literal`
- `dynamic_inline`
- `empty_collection_literal`
- `empty_count`
- `empty_enum_arguments`
- `empty_parameters`
- `empty_parentheses_with_trailing_closure`
- `empty_string`
- `enum_case_associated_values_count`
- `explicit_enum_raw_value`
- `explicit_init`
- `explicit_top_level_acl`
- `fallthrough`
- `file_name`
- `first_where`
- `flatmap_over_map_reduce`
- `for_where`
- `force_try`
- `force_unwrapping`
- `function_body_length`
- `function_default_parameter_at_end`
- `function_parameter_count`
- `generic_type_name`
- `ibinspectable_in_extension`
- `identical_operands`
- `implicit_getter`
- `implicitly_unwrapped_optional`
- `inclusive_language`
- `inert_defer`
- `is_disjoint`
- `joined_default_parameter`
- `large_tuple`
- `last_where`
- `legacy_cggeometry_functions`
- `legacy_constant`
- `legacy_constructor`
- `legacy_hashing`
- `legacy_multiple`
- `legacy_nsgeometry_functions`
- `legacy_objc_type`
- `legacy_random`
- `lower_acl_than_parent`
- `multiline_arguments_brackets`
- `multiline_parameters`
- `multiple_closures_with_trailing_closure`
- `no_extension_access_modifier`
- `no_fallthrough_only`
- `no_space_in_method_call`
- `notification_center_detachment`
- `nslocalizedstring_key`
- `nslocalizedstring_require_bundle`
- `nsobject_prefer_isequal`
- `number_separator`
- `object_literal`
- `operator_whitespace`
- `optional_enum_case_matching`
- `orphaned_doc_comment`
- `overridden_super_call`
- `override_in_extension`
- `pattern_matching_keywords`
- `prefer_nimble`
- `prefer_self_in_static_references`
- `prefer_self_type_over_type_of_self`
- `prefer_zero_over_explicit_init`
- `prefixed_toplevel_constant`
- `private_action`
- `private_outlet`
- `private_over_fileprivate`
- `private_subject`
- `private_unit_test`
- `prohibited_interface_builder`
- `prohibited_super_call`
- `protocol_property_accessors_order`
- `quick_discouraged_focused_test`
- `quick_discouraged_pending_test`
- `raw_value_for_camel_cased_codable_enum`
- `reduce_boolean`
- `reduce_into`
- `redundant_discardable_let`
- `redundant_nil_coalescing`
- `redundant_objc_attribute`
- `redundant_optional_initialization`
- `redundant_set_access_control`
- `redundant_string_enum_value`
- `required_deinit`
- `required_enum_case`
- `return_arrow_whitespace`
- `self_in_property_initialization`
- `shorthand_operator`
- `single_test_class`
- `sorted_first_last`
- `static_operator`
- `strict_fileprivate`
- `strong_iboutlet`
- `switch_case_alignment`
- `switch_case_on_newline`
- `test_case_accessibility`
- `toggle_bool`
- `trailing_comma`
- `trailing_semicolon`
- `type_body_length`
- `type_name`
- `unneeded_break_in_switch`
- `unneeded_parentheses_in_closure_argument`
- `unowned_variable_capture`
- `untyped_error_in_catch`
- `unused_capture_list`
- `unused_closure_parameter`
- `unused_control_flow_label`
- `unused_enumerated`
- `unused_optional_binding`
- `unused_setter_value`
- `valid_ibinspectable`
- `vertical_parameter_alignment`
- `weak_delegate`
- `xct_specific_matcher`
- `xctfail_message`
[Marcelo Fabri](https://github.com/marcelofabri)
[SimplyDanny](https://github.com/SimplyDanny)
[JP Simard](https://github.com/jpsim)
[#2915](https://github.com/realm/SwiftLint/issues/2915)
* The "body length" family of rules have changed how they calculate body
line count to be significantly more correct and intuitive. However,
this is likely to require adjustments to your configuration or disable
commands to account for the changes.
[JP Simard](https://github.com/jpsim)
* Add ability to filter rules for `generate-docs` subcommand.
[kattouf](https://github.com/kattouf)
* Add new `excludes_trivial_init` configuration for `missing_docs` rule * Add new `excludes_trivial_init` configuration for `missing_docs` rule
to exclude initializers without any parameters. to exclude initializers without any parameters.
[Marcelo Fabri](https://github.com/marcelofabri) [Marcelo Fabri](https://github.com/marcelofabri)
[#4107](https://github.com/realm/SwiftLint/issues/4107) [#4107](https://github.com/realm/SwiftLint/issues/4107)
* Add new `ns_number_init_as_function_reference` rule to catch `NSNumber.init` * Rewrite some rules with SwiftSyntax, fixing some false positives and catching
and `NSDecimalNumber.init` being used as function references since it more violations:
can cause the wrong initializer to be used, causing crashes. See - `anyobject_protocol`
https://github.com/apple/swift/issues/51036 for more info. - `array_init`
[Marcelo Fabri](https://github.com/marcelofabri) - `block_based_kvo`
- `class_delegate_protocol`
- `closing_brace`
- `computed_accessors_order`
- `discouraged_optional_boolean`
- `empty_string`
- `flatmap_over_map_reduce`
- `force_try`
- `force_unwrapping`
- `implicit_getter`
- `large_tuple`
- `multiple_closures_with_trailing_closure`
- `redundant_nil_coalescing`
- `toggle_bool`
- `unneeded_break_in_switch`
- `unneeded_parentheses_in_closure_argument`
- `unowned_variable_capture`
- `untyped_error_in_catch`
- `xctfail_message`
[Marcelo Fabri](https://github.com/marcelofabri)
[JP Simard](https://github.com/jpims)
[#2915](https://github.com/realm/SwiftLint/issues/2915)
* Add `accessibility_trait_for_button` rule to warn if a SwiftUI * Add `accessibility_trait_for_button` rule to warn if a SwiftUI
View has a tap gesture added to it without having the button or View has a tap gesture added to it without having the button or
@ -764,16 +70,12 @@
[Ryan Cole](https://github.com/rcole34) [Ryan Cole](https://github.com/rcole34)
* Add methods from SE-0348 to `UnusedDeclarationRule`. * Add methods from SE-0348 to `UnusedDeclarationRule`.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpims)
* Include the configured `bind_identifier` in `self_binding` violation * Include the configured `bind_identifier` in `self_binding` violation
messages. messages.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpims)
* The `self_binding` rule now catches shorthand optional bindings (for example
`if let self {}`) when using a `bind_identifier` different than `self`.
[Marcelo Fabri](https://github.com/marcelofabri)
* Add `library_content_provider` file type to `file_types_order` rule * Add `library_content_provider` file type to `file_types_order` rule
to allow `LibraryContentProvider` to be ordered independent from `main_type`. to allow `LibraryContentProvider` to be ordered independent from `main_type`.
[dahlborn](https://github.com/dahlborn) [dahlborn](https://github.com/dahlborn)
@ -783,86 +85,11 @@
[Martin Redington](https://github.com/mildm8nnered) [Martin Redington](https://github.com/mildm8nnered)
[#4200](https://github.com/realm/SwiftLint/issues/4200) [#4200](https://github.com/realm/SwiftLint/issues/4200)
* Add a new `shorthand_optional_binding` opt-in rule that triggers in Swift 5.7 * Add a new `if_let_shadowing` opt-in rule that triggers in Swift 5.7 when a
when a shadowing optional binding is created in an `if` or `guard` statement. shadowing optional binding is created in an if- or guard-statement.
[SimplyDanny](https://github.com/SimplyDanny) [SimplyDanny](https://github.com/SimplyDanny)
[#4202](https://github.com/realm/SwiftLint/issues/4202) [#4202](https://github.com/realm/SwiftLint/issues/4202)
* Use SwiftSyntax instead of SourceKit to determine if a file has parser errors
before applying corrections. This speeds up corrections significantly when
none of the rules use SourceKit.
[JP Simard](https://github.com/jpsim)
* Add Swift Package Build Tool Plugin with support for Swift Packages
and Xcode projects.
[Johannes Ebeling](https://github.com/technocidal)
[#3679](https://github.com/realm/SwiftLint/issues/3679)
[#3840](https://github.com/realm/SwiftLint/issues/3840)
* Make `private_unit_test` rule correctable.
[SimplyDanny](https://github.com/SimplyDanny)
* Disregard whitespace differences in `identical_operands` rule. That is, the rule
now also triggers if the left-hand side and the right-hand side of an operation
only differ in trivia.
[SimplyDanny](https://github.com/SimplyDanny)
* Print violations in realtime if `--progress` and `--output` are both set.
[JP Simard](https://github.com/jpsim)
* Trigger `prefer_self_in_static_references` rule on more type references like:
* Key paths (e.g. `\MyType.myVar` -> `\Self.myVar`)
* Computed properties (e.g. `var i: Int { MyType.myVar )` -> `var i: Int { Self.myVar }`)
* Constructor calls (e.g. `MyType()` -> `Self()`)
[SimplyDanny](https://github.com/SimplyDanny)
* Update `for_where` rule, adding a new configuration
`allow_for_as_filter` to allow using `for in` with a single `if` inside
when there's a `return` statement inside the `if`'s body.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4040](https://github.com/realm/SwiftLint/issues/4040)
* `quick_discouraged_call`, `quick_discouraged_focused_test` and
`quick_discouraged_pending_test` rules now trigger on subclasses of
`QuickSpec`.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4420](https://github.com/realm/SwiftLint/issues/4420)
* The `type_name` rule now validates protocol declarations by default.
You can opt-out by using the `validate_protocols` key in your configuration:
```yml
type_name:
validate_protocols: false
```
[Marcelo Fabri](https://github.com/marcelofabri)
[#4430](https://github.com/realm/SwiftLint/issues/4430)
* Report how much memory was used when `--benchmark` is specified.
[JP Simard](https://github.com/jpsim)
* Adds `NSError` to the list of types in `discouraged_direct_init`.
[jszumski](https://github.com/jszumski)
[#4508](https://github.com/realm/SwiftLint/issues/4508)
* Fix SwiftLint support on Xcode Cloud.
[JagCesar](https://github.com/JagCesar)
[westerlund](https://github.com/westerlund)
[#4484](https://github.com/realm/SwiftLint/issues/4484)
* Add `no_magic_numbers` rule to avoid "Magic Numbers".
[Henrik Storch](https://github.com/thisisthefoxe)
[#4031](https://github.com/realm/SwiftLint/issues/4024)
* Add new option `only_enforce_before_trivial_lines` to
`vertical_whitespace_closing_braces` rule. It restricts
the rule to apply only before trivial lines (containing
only closing braces, brackets and parentheses). This
allows empty lines before non-trivial lines of code
(e.g. if-else-statements).
[benjamin-kramer](https://github.com/benjamin-kramer)
[#3940](https://github.com/realm/SwiftLint/issues/3940)
#### Bug Fixes #### Bug Fixes
* Respect `validates_start_with_lowercase` option when linting function names. * Respect `validates_start_with_lowercase` option when linting function names.
@ -889,27 +116,6 @@
[SimplyDanny](https://github.com/SimplyDanny) [SimplyDanny](https://github.com/SimplyDanny)
[#4208](https://github.com/realm/SwiftLint/issues/4208) [#4208](https://github.com/realm/SwiftLint/issues/4208)
* Add column for SourceKit usage to `rules` command.
[JP Simard](https://github.com/jpsim)
* Make `nsobject_prefer_isequal` rule work for nested `@objc` classes. Also consider
the `@objcMembers` annotation.
[SimplyDanny](https://github.com/SimplyDanny)
* Print fixed content at most once to STDOUT.
[SimplyDanny](https://github.com/SimplyDanny)
[#4211](https://github.com/realm/SwiftLint/issues/4211)
* Fix fatal error when content given via STDIN is corrected in the
`trailing_newline` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4234](https://github.com/realm/SwiftLint/issues/4234)
* Fix false-positives from `multiline_arguments_brackets` when a function call has a
single line trailing closure.
[CraigSiemens](https://github.com/CraigSiemens)
[#4510](https://github.com/realm/SwiftLint/issues/4510)
## 0.49.1: Buanderie Principale ## 0.49.1: Buanderie Principale
_Note: The default branch for the SwiftLint git repository was renamed from _Note: The default branch for the SwiftLint git repository was renamed from
@ -1194,6 +400,15 @@ macOS < 12.
#### Enhancements #### Enhancements
* Add new option `only_enforce_before_trivial_lines` to
`vertical_whitespace_closing_braces` rule. It restricts
the rule to apply only before trivial lines (containing
only closing braces, brackets and parentheses). This
allows empty lines before non-trivial lines of code
(e.g. if-else-statements).
[benjamin-kramer](https://github.com/benjamin-kramer)
[#3940](https://github.com/realm/SwiftLint/issues/3940)
* Add type-checked analyzer rule version of `ArrayInitRule` named * Add type-checked analyzer rule version of `ArrayInitRule` named
`TypesafeArrayInitRule` with identifier `typesafe_array_init` that `TypesafeArrayInitRule` with identifier `typesafe_array_init` that
avoids the false positives present in the lint rule. avoids the false positives present in the lint rule.

View File

@ -1,9 +1,3 @@
## Tutorial
If you'd like to write a SwiftLint rule but aren't sure how to start,
please watch and follow along with
[this video tutorial](https://vimeo.com/819268038).
## Pull Requests ## Pull Requests
All changes, no matter how trivial, must be done via pull request. Commits All changes, no matter how trivial, must be done via pull request. Commits
@ -64,7 +58,7 @@ $ make docker_test
## Rules ## Rules
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory. New rules should be added in the `Source/SwiftLintFramework/Rules` directory.
Rules should conform to either the `Rule` or `ASTRule` protocols. Rules should conform to either the `Rule` or `ASTRule` protocols.
@ -104,11 +98,11 @@ configuration object via the `configuration` property:
* If none of the provided `RuleConfiguration`s are applicable, you can create one * If none of the provided `RuleConfiguration`s are applicable, you can create one
specifically for your rule. specifically for your rule.
See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceCastRule.swift) See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintFramework/Rules/Idiomatic/ForceCastRule.swift)
for a rule that allows severity configuration, for a rule that allows severity configuration,
[`FileLengthRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift) [`FileLengthRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintFramework/Rules/Metrics/FileLengthRule.swift)
for a rule that has multiple severity levels, for a rule that has multiple severity levels,
[`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift) [`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintFramework/Rules/Style/IdentifierNameRule.swift)
for a rule that allows name evaluation configuration: for a rule that allows name evaluation configuration:
``` yaml ``` yaml
@ -177,8 +171,15 @@ To bring up a new Buildkite worker from MacStadium:
1. Update macOS to the latest version 1. Update macOS to the latest version
1. Install Homebrew: https://brew.sh 1. Install Homebrew: https://brew.sh
1. Install Buildkite agent and other tools via Homebrew: 1. Install Buildkite agent and other tools via Homebrew:
`brew install aria2 bazelisk htop buildkite/buildkite/buildkite-agent robotsandpencils/made/xcodes` `brew install buildkite/buildkite/buildkite-agent aria2 htop`
1. Install latest Xcode version: `xcodes update && xcodes install 14.0.0` 1. Install the xcodes CLI by downloading the zip and moving it to `/usr/local/bin`:
1. Add `DANGER_GITHUB_API_TOKEN` and `HOME` to `/opt/homebrew/etc/buildkite-agent/hooks/environment` https://github.com/RobotsAndPencils/xcodes/releases
1. Install latest Xcode version: `xcodes install --latest`
1. Add `DANGER_GITHUB_API_TOKEN` to `/opt/homebrew/etc/buildkite-agent/hooks/environment`
1. Add `echo "build --remote_cache=grpc://<creds>@swiftlint-ci.jpsim.com:9092" > ci.bazelrc`
to `/opt/homebrew/etc/buildkite-agent/hooks/pre-command`, replacing `<creds>` with the
bazel-remote credentials
1. Configure and launch buildkite agent: `brew info buildkite-agent` / 1. Configure and launch buildkite agent: `brew info buildkite-agent` /
https://buildkite.com/organizations/swiftlint/agents#setup-macos https://buildkite.com/organizations/swiftlint/agents#setup-macos
1. On the `swiftlint-ci.jpsim.com` machine only:
`docker run -d -u 1000:1000 -v /tmp/swiftlint-bazel-remote-cache:/data -v ~/Desktop/bazel-remote.htpasswd:/etc/bazel-remote/htpasswd -p 9090:8080 -p 9092:9092 buchgr/bazel-remote-cache --htpasswd_file=/etc/bazel-remote/htpasswd --max_size=5`

View File

@ -14,9 +14,9 @@ modified_files = git.modified_files + git.added_files
# including in a CHANGELOG for example # including in a CHANGELOG for example
has_app_changes = !modified_files.grep(/Source/).empty? has_app_changes = !modified_files.grep(/Source/).empty?
has_test_changes = !modified_files.grep(/Tests/).empty? has_test_changes = !modified_files.grep(/Tests/).empty?
has_danger_changes = !modified_files.grep(/Dangerfile|tools\/oss-check|Gemfile/).empty? has_danger_changes = !modified_files.grep(/Dangerfile|script\/oss-check|Gemfile/).empty?
has_package_changes = !modified_files.grep(/Package\.swift/).empty? has_package_changes = !modified_files.grep(/Package\.swift/).empty?
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD|MODULE\.bazel/).empty? has_bazel_changes = !modified_files.grep(/\.bazelrc|WORKSPACE|bazel\/|BUILD/).empty?
# Add a CHANGELOG entry for app changes # Add a CHANGELOG entry for app changes
if !modified_files.include?('CHANGELOG.md') && has_app_changes if !modified_files.include?('CHANGELOG.md') && has_app_changes
@ -51,8 +51,7 @@ end
file = Tempfile.new('violations') file = Tempfile.new('violations')
force_flag = has_danger_changes ? "--force" : "" Open3.popen3("tools/oss-check -v 2> #{file.path}") do |_, stdout, _, _|
Open3.popen3("tools/oss-check -v #{force_flag} 2> #{file.path}") do |_, stdout, _, _|
while char = stdout.getc while char = stdout.getc
print char print char
end end

View File

@ -9,16 +9,18 @@ RUN apt-get update && apt-get install -y \
libxml2-dev \ libxml2-dev \
&& rm -r /var/lib/apt/lists/* && rm -r /var/lib/apt/lists/*
WORKDIR /workdir/ WORKDIR /workdir/
COPY Plugins Plugins/
COPY Source Source/ COPY Source Source/
COPY Tests Tests/ COPY Tests Tests/
COPY Package.* ./ COPY Package.* ./
RUN swift package update RUN ln -s /usr/lib/swift/_InternalSwiftSyntaxParser .
ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -lCFURLSessionInterface -Xlinker -lCFXMLInterface -Xlinker -lcurl -Xlinker -lxml2 -Xswiftc -I. -Xlinker -fuse-ld=lld -Xlinker -L/usr/lib/swift/linux" ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -lCFURLSessionInterface -Xlinker -lCFXMLInterface -Xlinker -lcurl -Xlinker -lxml2 -Xswiftc -I. -Xlinker -fuse-ld=lld -Xlinker -L/usr/lib/swift/linux"
RUN swift build $SWIFT_FLAGS --product swiftlint RUN swift build $SWIFT_FLAGS
RUN mkdir -p /executables RUN mkdir -p /executables
RUN install -v `swift build $SWIFT_FLAGS --show-bin-path`/swiftlint /executables RUN for executable in $(swift package completion-tool list-executables); do \
install -v `swift build $SWIFT_FLAGS --show-bin-path`/$executable /executables; \
done
# runtime image # runtime image
FROM ${RUNTIME_IMAGE} FROM ${RUNTIME_IMAGE}
@ -30,10 +32,9 @@ RUN apt-get update && apt-get install -y \
COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib
COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib
COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib
COPY --from=builder /usr/lib/swift/linux/libswiftCore.so /usr/lib COPY --from=builder /usr/lib/swift/linux/lib_InternalSwiftSyntaxParser.so /usr/lib
COPY --from=builder /executables/* /usr/bin COPY --from=builder /executables/* /usr/bin
RUN swiftlint version RUN swiftlint version
RUN echo "_ = 0" | swiftlint --use-stdin
CMD ["swiftlint"] CMD ["swiftlint"]

View File

@ -1,14 +1,15 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.6) CFPropertyList (3.0.5)
rexml rexml
activesupport (7.0.4.3) activesupport (6.1.6.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.4) zeitwerk (~> 2.3)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5) algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3) httpclient (~> 2.8, >= 2.8.3)
@ -19,15 +20,15 @@ GEM
cork cork
nap nap
open4 (~> 1.3) open4 (~> 1.3)
cocoapods (1.12.1) cocoapods (1.11.3)
addressable (~> 2.8) addressable (~> 2.8)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.12.1) cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.6.0, < 2.0) cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
escape (~> 0.0.4) escape (~> 0.0.4)
@ -35,10 +36,10 @@ GEM
gh_inspector (~> 1.0) gh_inspector (~> 1.0)
molinillo (~> 0.8.0) molinillo (~> 0.8.0)
nap (~> 1.0) nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0) ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0) xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.12.1) cocoapods-core (1.11.3)
activesupport (>= 5.0, < 8) activesupport (>= 5.0, < 7)
addressable (~> 2.8) addressable (~> 2.8)
algoliasearch (~> 1.0) algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
@ -57,42 +58,61 @@ GEM
netrc (~> 0.11) netrc (~> 0.11)
cocoapods-try (1.2.0) cocoapods-try (1.2.0)
colored2 (3.1.2) colored2 (3.1.2)
concurrent-ruby (1.2.2) concurrent-ruby (1.1.10)
cork (0.3.0) cork (0.3.0)
colored2 (~> 3.1) colored2 (~> 3.1)
danger (9.2.0) danger (8.6.1)
claide (~> 1.0) claide (~> 1.0)
claide-plugins (>= 0.9.2) claide-plugins (>= 0.9.2)
colored2 (~> 3.1) colored2 (~> 3.1)
cork (~> 0.1) cork (~> 0.1)
faraday (>= 0.9.0, < 3.0) faraday (>= 0.9.0, < 2.0)
faraday-http-cache (~> 2.0) faraday-http-cache (~> 2.0)
git (~> 1.7) git (~> 1.7)
kramdown (~> 2.3) kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.0) kramdown-parser-gfm (~> 1.0)
no_proxy_fix no_proxy_fix
octokit (~> 5.0) octokit (~> 4.7)
terminal-table (>= 1, < 4) terminal-table (>= 1, < 4)
escape (0.0.4) escape (0.0.4)
ethon (0.16.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
faraday (2.7.4) faraday (1.10.2)
faraday-net_http (>= 2.0, < 3.1) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4) ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-http-cache (2.4.1) faraday-http-cache (2.4.1)
faraday (>= 0.8) faraday (>= 0.8)
faraday-net_http (3.0.2) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
ffi (1.15.5) ffi (1.15.5)
fourflusher (2.3.1) fourflusher (2.3.1)
fuzzy_match (2.0.4) fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
git (1.18.0) git (1.12.0)
addressable (~> 2.8) addressable (~> 2.8)
rchardet (~> 1.8) rchardet (~> 1.8)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.12.0) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jazzy (0.14.3) jazzy (0.14.2)
cocoapods (~> 1.5) cocoapods (~> 1.5)
mustache (~> 1.1) mustache (~> 1.1)
open4 (~> 1.3) open4 (~> 1.3)
@ -102,26 +122,27 @@ GEM
sassc (~> 2.1) sassc (~> 2.1)
sqlite3 (~> 1.3) sqlite3 (~> 1.3)
xcinvoke (~> 0.3.0) xcinvoke (~> 0.3.0)
json (2.6.3) json (2.6.2)
kramdown (2.4.0) kramdown (2.4.0)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
liferaft (0.0.6) liferaft (0.0.6)
minitest (5.18.0) minitest (5.16.3)
molinillo (0.8.0) molinillo (0.8.0)
multipart-post (2.2.3)
mustache (1.1.1) mustache (1.1.1)
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0) nap (1.1.0)
netrc (0.11.0) netrc (0.11.0)
no_proxy_fix (0.1.2) no_proxy_fix (0.1.2)
octokit (5.6.1) octokit (4.25.1)
faraday (>= 1, < 3) faraday (>= 1, < 3)
sawyer (~> 0.9) sawyer (~> 0.9)
open4 (1.3.4) open4 (1.3.4)
public_suffix (4.0.7) public_suffix (4.0.7)
rchardet (1.8.0) rchardet (1.8.0)
redcarpet (3.6.0) redcarpet (3.5.1)
rexml (3.2.5) rexml (3.2.5)
rouge (3.30.0) rouge (3.30.0)
ruby-macho (2.5.1) ruby-macho (2.5.1)
@ -131,14 +152,14 @@ GEM
sawyer (0.9.2) sawyer (0.9.2)
addressable (>= 2.3.5) addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3) faraday (>= 0.17.3, < 3)
sqlite3 (1.6.2-arm64-darwin) sqlite3 (1.4.4)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
typhoeus (1.4.0) typhoeus (1.4.0)
ethon (>= 0.9.0) ethon (>= 0.9.0)
tzinfo (2.0.6) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2) unicode-display_width (2.2.0)
xcinvoke (0.3.0) xcinvoke (0.3.0)
liferaft (~> 0.0.6) liferaft (~> 0.0.6)
xcodeproj (1.22.0) xcodeproj (1.22.0)
@ -148,10 +169,11 @@ GEM
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.3.0)
rexml (~> 3.2.4) rexml (~> 3.2.4)
zeitwerk (2.6.0)
PLATFORMS PLATFORMS
arm64-darwin-21 arm64-darwin-21
arm64-darwin-22 ruby
DEPENDENCIES DEPENDENCIES
cocoapods cocoapods
@ -159,4 +181,4 @@ DEPENDENCIES
jazzy jazzy
BUNDLED WITH BUNDLED WITH
2.4.12 2.3.8

View File

@ -1,27 +0,0 @@
module(
name = "swiftlint",
version = "0.52.2",
compatibility_level = 1,
repo_name = "SwiftLint",
)
bazel_dep(name = "bazel_skylib", version = "1.4.1", dev_dependency = True)
bazel_dep(name = "platforms", version = "0.0.6")
bazel_dep(name = "rules_apple", version = "2.3.0", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "1.8.0", repo_name = "build_bazel_rules_swift")
bazel_dep(name = "rules_xcodeproj", version = "1.7.0")
bazel_dep(name = "sourcekitten", version = "0.34.1", repo_name = "com_github_jpsim_sourcekitten")
bazel_dep(name = "swift_argument_parser", version = "1.2.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser")
bazel_dep(name = "yams", version = "5.0.5", repo_name = "sourcekitten_com_github_jpsim_yams")
swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod")
use_repo(
swiftlint_repos,
"SwiftSyntax",
"com_github_johnsundell_collectionconcurrencykit",
"com_github_krzyzanowskim_cryptoswift",
"swiftlint_com_github_scottrhoyt_swifty_text_table",
)
extra_rules = use_extension("//bazel:extensions.bzl", "extra_rules")
use_repo(extra_rules, "swiftlint_extra_rules")

View File

@ -7,7 +7,10 @@ XCODEFLAGS=-scheme 'swiftlint' \
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
SWIFT_BUILD_FLAGS=--configuration release -Xlinker -dead_strip SWIFT_BUILD_FLAGS=--configuration release -Xlinker -dead_strip
UNAME=$(shell uname)
SWIFTLINT_EXECUTABLE_X86=$(shell swift build $(SWIFT_BUILD_FLAGS) --arch x86_64 --show-bin-path)/swiftlint
SWIFTLINT_EXECUTABLE_ARM64=$(shell swift build $(SWIFT_BUILD_FLAGS) --arch arm64 --show-bin-path)/swiftlint
SWIFTLINT_EXECUTABLE_PARENT=.build/universal SWIFTLINT_EXECUTABLE_PARENT=.build/universal
SWIFTLINT_EXECUTABLE=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint SWIFTLINT_EXECUTABLE=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint
@ -30,19 +33,15 @@ VERSION_STRING=$(shell ./tools/get-version)
all: build all: build
sourcery: Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift Source/SwiftLintCore/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift sourcery: Source/SwiftLintFramework/Models/PrimaryRuleList.swift Tests/SwiftLintFrameworkTests/GeneratedTests.swift
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil Tests/SwiftLintFrameworkTests/GeneratedTests.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/GeneratedTests.stencil
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.swift mv .sourcery/GeneratedTests.generated.swift Tests/SwiftLintFrameworkTests/GeneratedTests.swift
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLint*/Rules/**/*.swift .sourcery/GeneratedTests.stencil
./tools/sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
test: clean_xcode test: clean_xcode
$(BUILD_TOOL) $(XCODEFLAGS) test $(BUILD_TOOL) $(XCODEFLAGS) test
@ -63,18 +62,25 @@ analyze_autocorrect: write_xcodebuild_log
clean: clean:
rm -f "$(OUTPUT_PACKAGE)" rm -f "$(OUTPUT_PACKAGE)"
rm -rf "$(TEMPORARY_FOLDER)" rm -rf "$(TEMPORARY_FOLDER)"
rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256" rm -f "./*.zip"
swift package clean swift package clean
clean_xcode: clean_xcode:
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean $(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
build: build_x86_64:
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)" swift build $(SWIFT_BUILD_FLAGS) --arch x86_64
bazel build --config release universal_swiftlint
$(eval SWIFTLINT_BINARY := $(shell bazel cquery --config release --output=files universal_swiftlint)) build_arm64:
mv "$(SWIFTLINT_BINARY)" "$(SWIFTLINT_EXECUTABLE)" swift build $(SWIFT_BUILD_FLAGS) --arch arm64
chmod +w "$(SWIFTLINT_EXECUTABLE)"
build: clean build_x86_64 build_arm64
# Need to build for each arch independently to work around https://bugs.swift.org/browse/SR-15802
mkdir -p $(SWIFTLINT_EXECUTABLE_PARENT)
lipo -create -output \
"$(SWIFTLINT_EXECUTABLE)" \
"$(SWIFTLINT_EXECUTABLE_X86)" \
"$(SWIFTLINT_EXECUTABLE_ARM64)"
strip -rSTX "$(SWIFTLINT_EXECUTABLE)" strip -rSTX "$(SWIFTLINT_EXECUTABLE)"
build_with_disable_sandbox: build_with_disable_sandbox:
@ -117,11 +123,10 @@ zip_linux: docker_image
zip_linux_release: zip_linux_release:
$(eval TMP_FOLDER := $(shell mktemp -d)) $(eval TMP_FOLDER := $(shell mktemp -d))
docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint" docker run "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
chmod +x "$(TMP_FOLDER)/swiftlint" chmod +x "$(TMP_FOLDER)/swiftlint"
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)" cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip" (cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
gh release upload "$(VERSION_STRING)" swiftlint_linux.zip
package: build package: build
$(eval PACKAGE_ROOT := $(shell mktemp -d)) $(eval PACKAGE_ROOT := $(shell mktemp -d))
@ -137,11 +142,13 @@ bazel_release:
bazel build :release bazel build :release
mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 . mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 .
release: bazel_release package portable_zip spm_artifactbundle_macos zip_linux_release
docker_image: docker_image:
docker build --platform linux/amd64 --force-rm --tag swiftlint . docker build --platform linux/amd64 --force-rm --tag swiftlint .
docker_test: docker_test:
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.7-focal swift test --parallel docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.6-focal swift test --parallel
docker_htop: docker_htop:
docker run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset docker run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset
@ -152,8 +159,8 @@ display_compilation_time:
publish: publish:
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
bundle install # Workaround for https://github.com/CocoaPods/CocoaPods/issues/11185
bundle exec pod trunk push SwiftLint.podspec arch -arch x86_64 pod trunk push SwiftLint.podspec
docs: docs:
swift run swiftlint generate-docs swift run swiftlint generate-docs
@ -163,30 +170,18 @@ docs:
get_version: get_version:
@echo "$(VERSION_STRING)" @echo "$(VERSION_STRING)"
release: push_version:
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),) ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
$(error git state is not clean) $(error git state is not clean)
endif endif
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS))) $(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' )) $(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
@sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md @sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintCore/Models/Version.swift @sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
@sed -e '3s/.*/ version = "$(NEW_VERSION)",/' -i '' MODULE.bazel
make clean
make package
make bazel_release
make portable_zip
make spm_artifactbundle_macos
./tools/update-artifact-bundle.sh "$(NEW_VERSION)"
git commit -a -m "release $(NEW_VERSION)" git commit -a -m "release $(NEW_VERSION)"
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)" git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
git push origin HEAD git push origin HEAD
git push origin $(NEW_VERSION) git push origin $(NEW_VERSION)
./tools/create-github-release.sh "$(NEW_VERSION)"
make publish
./tools/add-new-changelog-section.sh
git commit -a -m "Add new changelog section"
git push origin HEAD
%: %:
@: @:

View File

@ -1,77 +1,70 @@
{ {
"pins" : [ "object": {
{ "pins": [
"identity" : "collectionconcurrencykit", {
"kind" : "remoteSourceControl", "package": "CollectionConcurrencyKit",
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", "repositoryURL": "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
"state" : { "state": {
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", "branch": null,
"version" : "0.2.0" "revision": "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version": "0.2.0"
}
},
{
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
"state": {
"branch": null,
"revision": "b5f9bb749057dd396e93f97956bef64672bc2a04",
"version": "0.33.0"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
"state": {
"branch": null,
"revision": "9f39744e025c7d377987f30b03770805dcb0bcd1",
"version": "1.1.4"
}
},
{
"package": "SwiftSyntax",
"repositoryURL": "https://github.com/apple/swift-syntax.git",
"state": {
"branch": null,
"revision": "093e5ee151d206454e2c1950d81333c4d4a4472e",
"version": null
}
},
{
"package": "SwiftyTextTable",
"repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git",
"state": {
"branch": null,
"revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
"version": "0.9.0"
}
},
{
"package": "SWXMLHash",
"repositoryURL": "https://github.com/drmohundro/SWXMLHash.git",
"state": {
"branch": null,
"revision": "4d0f62f561458cbe1f732171e625f03195151b60",
"version": "7.0.1"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "01835dc202670b5bb90d07f3eae41867e9ed29f6",
"version": "5.0.1"
}
} }
}, ]
{ },
"identity" : "cryptoswift", "version": 1
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f",
"version" : "1.7.2"
}
},
{
"identity" : "sourcekitten",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/SourceKitten.git",
"state" : {
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
"version" : "0.34.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
"version" : "1.2.1"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5",
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"
}
},
{
"identity" : "swiftytexttable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scottrhoyt/SwiftyTextTable.git",
"state" : {
"revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
"version" : "0.9.0"
}
},
{
"identity" : "swxmlhash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/drmohundro/SWXMLHash.git",
"state" : {
"revision" : "4d0f62f561458cbe1f732171e625f03195151b60",
"version" : "7.0.1"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a",
"version" : "5.0.5"
}
}
],
"version" : 2
} }

View File

@ -1,32 +1,37 @@
// swift-tools-version:5.7 // swift-tools-version:5.5
import PackageDescription import PackageDescription
#if os(macOS)
private let addCryptoSwift = false
#else
private let addCryptoSwift = true
#endif
let frameworkDependencies: [Target.Dependency] = [
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "SwiftSyntax", package: "SwiftSyntax"),
.product(name: "SwiftSyntaxBuilder", package: "SwiftSyntax"),
.product(name: "SwiftParser", package: "SwiftSyntax"),
"Yams",
]
+ (addCryptoSwift ? ["CryptoSwift"] : [])
let package = Package( let package = Package(
name: "SwiftLint", name: "SwiftLint",
platforms: [.macOS(.v12)], platforms: [.macOS(.v12)],
products: [ products: [
.executable(name: "swiftlint", targets: ["swiftlint"]), .executable(name: "swiftlint", targets: ["swiftlint"]),
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]), .library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"])
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")), .package(name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.1.3")),
.package(url: "https://github.com/apple/swift-syntax.git", exact: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"), .package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax.git", .revision("093e5ee151d206454e2c1950d81333c4d4a4472e")),
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")), .package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.33.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"), .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.1"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0")
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.7.2")) ] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.6.0"))] : []),
],
targets: [ targets: [
.plugin(
name: "SwiftLintPlugin",
capability: .buildTool(),
dependencies: [
.target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),
.target(name: "swiftlint", condition: .when(platforms: [.linux]))
]
),
.executableTarget( .executableTarget(
name: "swiftlint", name: "swiftlint",
dependencies: [ dependencies: [
@ -36,86 +41,18 @@ let package = Package(
"SwiftyTextTable", "SwiftyTextTable",
] ]
), ),
.testTarget(
name: "CLITests",
dependencies: [
"swiftlint"
]
),
.target(
name: "SwiftLintCore",
dependencies: [
.product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux])),
.target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])),
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftyTextTable", package: "SwiftyTextTable"),
.product(name: "Yams", package: "Yams"),
]
),
.target(
name: "SwiftLintBuiltInRules",
dependencies: ["SwiftLintCore"]
),
.target(
name: "SwiftLintExtraRules",
dependencies: ["SwiftLintCore"]
),
.target( .target(
name: "SwiftLintFramework", name: "SwiftLintFramework",
dependencies: [ dependencies: frameworkDependencies
"SwiftLintBuiltInRules",
"SwiftLintCore",
"SwiftLintExtraRules"
]
),
.target(name: "DyldWarningWorkaround"),
.target(
name: "SwiftLintTestHelpers",
dependencies: [
"SwiftLintFramework"
],
path: "Tests/SwiftLintTestHelpers"
), ),
.testTarget( .testTarget(
name: "SwiftLintFrameworkTests", name: "SwiftLintFrameworkTests",
dependencies: [ dependencies: [
"SwiftLintFramework", "SwiftLintFramework"
"SwiftLintTestHelpers"
], ],
exclude: [ exclude: [
"Resources", "Resources",
] ]
), ),
.testTarget(
name: "GeneratedTests",
dependencies: [
"SwiftLintFramework",
"SwiftLintTestHelpers"
]
),
.testTarget(
name: "IntegrationTests",
dependencies: [
"SwiftLintFramework",
"SwiftLintTestHelpers"
]
),
.testTarget(
name: "ExtraRulesTests",
dependencies: [
"SwiftLintFramework",
"SwiftLintTestHelpers"
]
),
.binaryTarget(
name: "SwiftLintBinary",
url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"
)
] ]
) )

View File

@ -1,42 +0,0 @@
import Foundation
import PackagePlugin
#if os(Linux)
import Glibc
#else
import Darwin
#endif
extension Path {
/// Scans the receiver, then all of its parents looking for a configuration file with the name ".swiftlint.yml".
///
/// - returns: Path to the configuration file, or nil if one cannot be found.
func firstConfigurationFileInParentDirectories() -> Path? {
let defaultConfigurationFileName = ".swiftlint.yml"
let proposedDirectory = sequence(
first: self,
next: { path in
guard path.stem.count > 1 else {
// Check we're not at the root of this filesystem, as `removingLastComponent()`
// will continually return the root from itself.
return nil
}
return path.removingLastComponent()
}
).first { path in
let potentialConfigurationFile = path.appending(subpath: defaultConfigurationFileName)
return potentialConfigurationFile.isAccessible()
}
return proposedDirectory?.appending(subpath: defaultConfigurationFileName)
}
/// Safe way to check if the file is accessible from within the current process sandbox.
private func isAccessible() -> Bool {
let result = string.withCString { pointer in
access(pointer, R_OK)
}
return result == 0
}
}

View File

@ -1,76 +0,0 @@
import Foundation
import PackagePlugin
@main
struct SwiftLintPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
guard let sourceTarget = target as? SourceModuleTarget else {
return []
}
return createBuildCommands(
inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),
packageDirectory: context.package.directory,
workingDirectory: context.pluginWorkDirectory,
tool: try context.tool(named: "swiftlint")
)
}
private func createBuildCommands(
inputFiles: [Path],
packageDirectory: Path,
workingDirectory: Path,
tool: PluginContext.Tool
) -> [Command] {
if inputFiles.isEmpty {
// Don't lint anything if there are no Swift source files in this target
return []
}
var arguments = [
"lint",
"--quiet",
// We always pass all of the Swift source files in the target to the tool,
// so we need to ensure that any exclusion rules in the configuration are
// respected.
"--force-exclude",
"--cache-path", "\(workingDirectory)"
]
// Manually look for configuration files, to avoid issues when the plugin does not execute our tool from the
// package source directory.
if let configuration = packageDirectory.firstConfigurationFileInParentDirectories() {
arguments.append(contentsOf: ["--config", "\(configuration.string)"])
}
arguments += inputFiles.map(\.string)
// We are not producing output files and this is needed only to not include cache files into bundle
let outputFilesDirectory = workingDirectory.appending("Output")
return [
.prebuildCommand(
displayName: "SwiftLint",
executable: tool.path,
arguments: arguments,
outputFilesDirectory: outputFilesDirectory
)
]
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
extension SwiftLintPlugin: XcodeBuildToolPlugin {
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
let inputFilePaths = target.inputFiles
.filter { $0.type == .source && $0.path.extension == "swift" }
.map(\.path)
return createBuildCommands(
inputFiles: inputFilePaths,
packageDirectory: context.xcodeProject.directory,
workingDirectory: context.pluginWorkDirectory,
tool: try context.tool(named: "swiftlint")
)
}
}
#endif

124
README.md
View File

@ -1,6 +1,6 @@
# SwiftLint # SwiftLint
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide). A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Ray Wenderlich's Swift Style Guide](https://github.com/raywenderlich/swift-style-guide).
SwiftLint hooks into [Clang](http://clang.llvm.org) and SwiftLint hooks into [Clang](http://clang.llvm.org) and
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the [SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
@ -64,25 +64,19 @@ You can also build and install from source by cloning this project and running
### Using Bazel ### Using Bazel
Put this in your `MODULE.bazel`: Put this in your `WORKSPACE`:
```bzl
bazel_dep(name = "swiftlint", version = "0.50.4", repo_name = "SwiftLint")
```
Or put this in your `WORKSPACE`:
<details> <details>
<summary>WORKSPACE</summary> <summary>WORKSPACE</summary>
```bzl ```python
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive( http_archive(
name = "build_bazel_rules_apple", name = "build_bazel_rules_apple",
sha256 = "f94e6dddf74739ef5cb30f000e13a2a613f6ebfa5e63588305a71fce8a8a9911", sha256 = "36072d4f3614d309d6a703da0dfe48684ec4c65a89611aeb9590b45af7a3e592",
url = "https://github.com/bazelbuild/rules_apple/releases/download/1.1.3/rules_apple.1.1.3.tar.gz", url = "https://github.com/bazelbuild/rules_apple/releases/download/1.0.1/rules_apple.1.0.1.tar.gz",
) )
load( load(
@ -136,7 +130,7 @@ bazel run -c opt @SwiftLint//:swiftlint
To get a high-level overview of recommended ways to integrate SwiftLint into your project, To get a high-level overview of recommended ways to integrate SwiftLint into your project,
we encourage you to watch this presentation or read the transcript: we encourage you to watch this presentation or read the transcript:
[![Presentation](assets/presentation.svg)](https://youtu.be/9Z1nTMTejqU) [![Presentation](assets/presentation.svg)](https://academy.realm.io/posts/slug-jp-simard-swiftlint/)
### Xcode ### Xcode
@ -158,10 +152,7 @@ folder by default. To instruct Xcode where to find SwiftLint, you can either add
`/opt/homebrew/bin` to the `PATH` environment variable in your build phase `/opt/homebrew/bin` to the `PATH` environment variable in your build phase
```bash ```bash
if [[ "$(uname -m)" == arm64 ]]; then export PATH="$PATH:/opt/homebrew/bin"
export PATH="/opt/homebrew/bin:$PATH"
fi
if which swiftlint > /dev/null; then if which swiftlint > /dev/null; then
swiftlint swiftlint
else else
@ -192,52 +183,12 @@ If you've installed SwiftLint via CocoaPods the script should look like this:
"${PODS_ROOT}/SwiftLint/swiftlint" "${PODS_ROOT}/SwiftLint/swiftlint"
``` ```
### Plug-in Support ### AppCode
SwiftLint can be used as a build tool plug-in for both Xcode projects as well as To integrate SwiftLint with AppCode, install
Swift packages. [this plugin](https://plugins.jetbrains.com/plugin/9175) and configure
SwiftLint's installed path in the plugin's preferences.
> Due to limitations with Swift Package Manager Plug-ins this is only The `fix` action is available via `⌥⏎`.
recommended for projects that have a SwiftLint configuration in their root directory as
there is currently no way to pass any additional options to the SwiftLint executable.
#### Xcode
You can integrate SwiftLint as an Xcode Build Tool Plug-in if you're working
with a project in Xcode.
Add SwiftLint as a package dependency to your project without linking any of the
products.
Select the target you want to add linting to and open the `Build Phases` inspector.
Open `Run Build Tool Plug-ins` and select the `+` button.
Select `SwiftLintPlugin` from the list and add it to the project.
![](assets/select-swiftlint-plugin.png)
For unattended use (e.g. on CI), you can disable the package validation dialog by
* individually passing `-skipPackagePluginValidation` to `xcodebuild` or
* globally setting `defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES`
for that user.
_Note: This implicitly trusts all Xcode package plugins and bypasses Xcode's package validation
dialogs, which has security implications._
#### Swift Package
You can integrate SwiftLint as a Swift Package Manager Plug-in if you're working with
a Swift Package with a `Package.swift` manifest.
Add SwiftLint as a package dependency to your `Package.swift` file.
Add SwiftLint to a target using the `plugins` parameter.
```swift
.target(
...
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
),
```
### Visual Studio Code ### Visual Studio Code
@ -308,7 +259,6 @@ SUBCOMMANDS:
docs Open SwiftLint documentation website in the default web browser docs Open SwiftLint documentation website in the default web browser
generate-docs Generates markdown documentation for all rules generate-docs Generates markdown documentation for all rules
lint (default) Print lint warnings and errors lint (default) Print lint warnings and errors
reporters Display the list of reporters and their identifiers
rules Display the list of rules and their identifiers rules Display the list of rules and their identifiers
version Display the current version of SwiftLint version Display the current version of SwiftLint
@ -376,21 +326,12 @@ Once [installed](https://pre-commit.com/#install), add this to the
```yaml ```yaml
repos: repos:
- repo: https://github.com/realm/SwiftLint - repo: https://github.com/realm/SwiftLint
rev: 0.50.3 rev: 0.44.0
hooks: hooks:
- id: swiftlint - id: swiftlint
``` ```
Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` can be used to update to the current version. Adjust `rev` to the SwiftLint version of your choice.
SwiftLint can be configured using `entry` to apply fixes and fail on errors:
```yaml
- repo: https://github.com/realm/SwiftLint
rev: 0.50.3
hooks:
- id: swiftlint
entry: swiftlint --fix --strict
```
## Rules ## Rules
@ -401,7 +342,7 @@ continues to contribute more over time.
You can find an updated list of rules and more information about them You can find an updated list of rules and more information about them
[here](https://realm.github.io/SwiftLint/rule-directory.html). [here](https://realm.github.io/SwiftLint/rule-directory.html).
You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules) You can also check [Source/SwiftLintFramework/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintFramework/Rules)
directory to see their implementation. directory to see their implementation.
### Opt-In Rules ### Opt-In Rules
@ -479,9 +420,7 @@ run SwiftLint from. The following parameters can be configured:
Rule inclusion: Rule inclusion:
* `disabled_rules`: Disable rules from the default enabled set. * `disabled_rules`: Disable rules from the default enabled set.
* `opt_in_rules`: Enable rules that are not part of the default set. The * `opt_in_rules`: Enable rules that are not part of the default set.
special `all` identifier will enable all opt in linter rules, except the ones
listed in `disabled_rules`.
* `only_rules`: Only the rules specified in this list will be enabled. * `only_rules`: Only the rules specified in this list will be enabled.
Cannot be specified alongside `disabled_rules` or `opt_in_rules`. Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
* `analyzer_rules`: This is an entirely separate list of rules that are only * `analyzer_rules`: This is an entirely separate list of rules that are only
@ -503,9 +442,6 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
# - empty_parameters # - empty_parameters
# - vertical_whitespace # - vertical_whitespace
analyzer_rules: # Rules run by `swiftlint analyze`
- explicit_self
included: # paths to include during linting. `--path` is ignored if present. included: # paths to include during linting. `--path` is ignored if present.
- Source - Source
excluded: # paths to ignore during linting. Takes precedence over `included`. excluded: # paths to ignore during linting. Takes precedence over `included`.
@ -514,9 +450,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Source/ExcludedFolder - Source/ExcludedFolder
- Source/ExcludedFile.swift - Source/ExcludedFile.swift
- Source/*/ExcludedFile.swift # Exclude files with a wildcard - Source/*/ExcludedFile.swift # Exclude files with a wildcard
analyzer_rules: # Rules run by `swiftlint analyze`
# If true, SwiftLint will not fail if no lintable files are found. - explicit_self
allow_zero_lintable_files: false
# configurable rules can be customized from this configuration file # configurable rules can be customized from this configuration file
# binary rules can set their severity level # binary rules can set their severity level
@ -550,29 +485,13 @@ identifier_name:
- id - id
- URL - URL
- GlobalAPIKey - GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
``` ```
You can also use environment variables in your configuration file, You can also use environment variables in your configuration file,
by using `${SOME_VARIABLE}` in a string. by using `${SOME_VARIABLE}` in a string.
### Defining Custom Rules #### Defining Custom Rules
In addition to the rules that the main SwiftLint project ships with, SwiftLint
can also run two types of custom rules that you can define yourself in your own
projects:
#### 1. Swift Custom Rules
These rules are written the same way as the Swift-based rules that ship with
SwiftLint so they're fast, accurate, can leverage SwiftSyntax, can be unit
tested, and more.
Using these requires building SwiftLint with Bazel as described in
[this video](https://vimeo.com/820572803) or its associated code in
[github.com/jpsim/swiftlint-bazel-example](https://github.com/jpsim/swiftlint-bazel-example).
#### 2. Regex Custom Rules
You can define custom regex-based rules in your configuration file using the You can define custom regex-based rules in your configuration file using the
following syntax: following syntax:
@ -637,9 +556,6 @@ which match to `keyword` and `identifier` in the above list.
If using custom rules in combination with `only_rules`, make sure to add If using custom rules in combination with `only_rules`, make sure to add
`custom_rules` as an item under `only_rules`. `custom_rules` as an item under `only_rules`.
Unlike Swift custom rules, you can use official SwiftLint builds
(e.g. from Homebrew) to run regex custom rules.
### Auto-correct ### Auto-correct
SwiftLint can automatically correct certain violations. Files on disk are SwiftLint can automatically correct certain violations. Files on disk are

View File

@ -1,6 +1,6 @@
# SwiftLint # SwiftLint
SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [Kodeco's Swift 代码风格指南](https://github.com/kodecocodes/swift-style-guide)为基础。 SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [Ray Wenderlich's Swift 代码风格指南](https://github.com/raywenderlich/swift-style-guide)为基础。
SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。 SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。
@ -167,7 +167,7 @@ SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。 你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
你也可以检视 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules) 目录来查看它们的实现。 你也可以检视 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules) 目录来查看它们的实现。
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。 `opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。

View File

@ -1,6 +1,6 @@
# SwiftLint # SwiftLint
SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [Kodeco 스위프트 스타일 가이드](https://github.com/kodecocodes/swift-style-guide)에 대략적인 기반을 두고 있습니다. SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [Ray Wenderlich 스위프트 스타일 가이드](https://github.com/raywenderlich/swift-style-guide)에 대략적인 기반을 두고 있습니다.
SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다. SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다.
@ -75,10 +75,7 @@ fi
그 이유는, 애플 실리콘 기반 맥에서 Homebrew는 기본적으로 바이너리들을 `/opt/homebrew/bin`에 저장하기 때문입니다. SwiftLint가 어디 있는지 찾는 것을 Xcode에 알려주기 위해, build phase에서 `/opt/homebrew/bin``PATH` 환경 변수에 동시에 추가하여야 합니다. 그 이유는, 애플 실리콘 기반 맥에서 Homebrew는 기본적으로 바이너리들을 `/opt/homebrew/bin`에 저장하기 때문입니다. SwiftLint가 어디 있는지 찾는 것을 Xcode에 알려주기 위해, build phase에서 `/opt/homebrew/bin``PATH` 환경 변수에 동시에 추가하여야 합니다.
```bash ```bash
if [[ "$(uname -m)" == arm64 ]]; then export PATH="$PATH:/opt/homebrew/bin"
export PATH="/opt/homebrew/bin:$PATH"
fi
if which swiftlint > /dev/null; then if which swiftlint > /dev/null; then
swiftlint swiftlint
else else
@ -183,9 +180,9 @@ $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
## 룰 ## 룰
SwiftLint에는 200개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다. SwiftLint에는 75개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다.
현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules)를 살펴보세요. 현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)를 살펴보세요.
`opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.) `opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)

View File

@ -7,8 +7,19 @@ For SwiftLint contributors, follow these steps to cut a release:
* FabricSoftenerRule * FabricSoftenerRule
* Top Loading * Top Loading
* Fresh Out Of The Dryer * Fresh Out Of The Dryer
1. Push new version: `make push_version "0.2.0: Tumble Dry"`
1. Make sure you have the latest stable Xcode version installed and 1. Make sure you have the latest stable Xcode version installed and
`xcode-select`ed `xcode-select`ed.
1. Release new version: `make release "0.2.0: Tumble Dry"` 1. Create the pkg installer, framework zip, portable zip,
1. Wait for the Docker CI job to finish then run: `make zip_linux_release` macos artifactbundle zip, and Linux zip:
`make release`
1. Create a GitHub release: https://github.com/realm/SwiftLint/releases/new
* Specify the tag you just pushed from the dropdown.
* Set the release title to the new version number & release name.
* Add the changelog section to the release description text box.
* Upload the bazel tarball & SHA-256 signature, pkg installer,
framework zip, portable zip, macos artifactbundle zip, and Linux zip
you just built to the GitHub release binaries.
* Click "Publish release".
1. Publish to Homebrew and CocoaPods trunk: `make publish`
1. Celebrate. :tada: 1. Celebrate. :tada:

View File

@ -1,15 +0,0 @@
#ifdef __APPLE__
#include "objc_dupclass.h"
OBJC_DUPCLASS(_TtC11SwiftSyntax11SyntaxArena);
OBJC_DUPCLASS(_TtC11SwiftSyntax13SyntaxVisitor);
OBJC_DUPCLASS(_TtC11SwiftSyntax14SyntaxRewriter);
OBJC_DUPCLASS(_TtC11SwiftSyntax16BumpPtrAllocator);
OBJC_DUPCLASS(_TtC11SwiftSyntax16SyntaxAnyVisitor);
OBJC_DUPCLASS(_TtC11SwiftSyntax18ParsingSyntaxArena);
OBJC_DUPCLASS(_TtC11SwiftSyntax23SourceLocationConverter);
OBJC_DUPCLASS(_TtC11SwiftSyntax26IncrementalParseTransition);
OBJC_DUPCLASS(_TtC11SwiftSyntax35IncrementalParseReusedNodeCollector);
#endif // __APPLE__

View File

@ -1,19 +0,0 @@
// https://github.com/keith/objc_dupclass
#include <stdint.h>
// TODO: This isn't entirely accurate, but I'm not sure how to more accurately determine
#if (defined(__arm64__) || defined(DUPCLASS_FORCE_DATA_CONST)) && !defined(DUPCLASS_FORCE_DATA)
#define SECTION "__DATA_CONST"
#else
#define SECTION "__DATA"
#endif
// Struct layout from https://github.com/apple-oss-distributions/objc4/blob/8701d5672d3fd3cd817aeb84db1077aafe1a1604/runtime/objc-abi.h#L175-L183
#define OBJC_DUPCLASS(kclass) \
__attribute__((used)) __attribute__((visibility("hidden"))) \
static struct { uint32_t version; uint32_t flags; const char name[64]; } \
const __duplicate_class_##kclass = { 0, 0, #kclass }; \
\
__attribute__((used)) __attribute__((visibility("hidden"))) \
__attribute__((section (SECTION",__objc_dupclass"))) \
const void* __set___objc_dupclass_sym___duplicate_class_##kclass = &__duplicate_class_##kclass

View File

@ -1 +0,0 @@
@_exported import SwiftLintCore

View File

@ -1,109 +0,0 @@
import SwiftSyntax
import SwiftSyntaxBuilder
/// A helper to hold a visitor and rewriter that can lint and correct legacy NS/CG functions to a more modern syntax.
enum LegacyFunctionRuleHelper {
final class Visitor: ViolationsSyntaxVisitor {
private let legacyFunctions: [String: RewriteStrategy]
init(legacyFunctions: [String: RewriteStrategy]) {
self.legacyFunctions = legacyFunctions
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionCallExprSyntax) {
if node.isLegacyFunctionExpression(legacyFunctions: legacyFunctions) {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
enum RewriteStrategy {
case equal
case property(name: String)
case function(name: String, argumentLabels: [String], reversed: Bool = false)
var expectedInitialArguments: Int {
switch self {
case .equal:
return 2
case .property:
return 1
case .function(name: _, argumentLabels: let argumentLabels, reversed: _):
return argumentLabels.count + 1
}
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let locationConverter: SourceLocationConverter
private let disabledRegions: [SourceRange]
private let legacyFunctions: [String: RewriteStrategy]
init(
legacyFunctions: [String: RewriteStrategy],
locationConverter: SourceLocationConverter,
disabledRegions: [SourceRange]
) {
self.legacyFunctions = legacyFunctions
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
guard
node.isLegacyFunctionExpression(legacyFunctions: legacyFunctions),
let funcName = node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let trimmedArguments = node.argumentList.map { $0.trimmingTrailingComma() }
let rewriteStrategy = legacyFunctions[funcName]
let expr: ExprSyntax
switch rewriteStrategy {
case .equal:
expr = "\(trimmedArguments[0]) == \(trimmedArguments[1])"
case let .property(name: propertyName):
expr = "\(trimmedArguments[0]).\(raw: propertyName)"
case let .function(name: functionName, argumentLabels: argumentLabels, reversed: reversed):
let arguments = reversed ? trimmedArguments.reversed() : trimmedArguments
let params = zip(argumentLabels, arguments.dropFirst())
.map { $0.isEmpty ? "\($1)" : "\($0): \($1)" }
.joined(separator: ", ")
expr = "\(arguments[0]).\(raw: functionName)(\(raw: params))"
case .none:
return super.visit(node)
}
return expr
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
}
}
}
private extension FunctionCallExprSyntax {
func isLegacyFunctionExpression(legacyFunctions: [String: LegacyFunctionRuleHelper.RewriteStrategy]) -> Bool {
guard
let calledExpression = calledExpression.as(IdentifierExprSyntax.self),
let rewriteStrategy = legacyFunctions[calledExpression.identifier.text],
argumentList.count == rewriteStrategy.expectedInitialArguments
else {
return false
}
return true
}
}
private extension TupleExprElementSyntax {
func trimmingTrailingComma() -> TupleExprElementSyntax {
self.trimmed.with(\.trailingComma, nil).trimmed
}
}

View File

@ -1,60 +0,0 @@
import SwiftSyntax
struct AnonymousArgumentInMultilineClosureRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "anonymous_argument_in_multiline_closure",
name: "Anonymous Argument in Multiline Closure",
description: "Use named arguments in multiline closures",
kind: .idiomatic,
nonTriggeringExamples: [
Example("closure { $0 }"),
Example("closure { print($0) }"),
Example("""
closure { arg in
print(arg)
}
"""),
Example("""
closure { arg in
nestedClosure { $0 + arg }
}
""")
],
triggeringExamples: [
Example("""
closure {
print($0)
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(locationConverter: file.locationConverter)
}
}
private extension AnonymousArgumentInMultilineClosureRule {
final class Visitor: ViolationsSyntaxVisitor {
private let locationConverter: SourceLocationConverter
init(locationConverter: SourceLocationConverter) {
self.locationConverter = locationConverter
super.init(viewMode: .sourceAccurate)
}
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
let startLocation = locationConverter.location(for: node.leftBrace.positionAfterSkippingLeadingTrivia)
let endLocation = locationConverter.location(for: node.rightBrace.endPositionBeforeTrailingTrivia)
return startLocation.line == endLocation.line ? .skipChildren : .visitChildren
}
override func visitPost(_ node: IdentifierExprSyntax) {
if case .dollarIdentifier = node.identifier.tokenKind {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
}

View File

@ -1,232 +0,0 @@
import SwiftSyntax
struct ConvenienceTypeRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "convenience_type",
name: "Convenience Type",
description: "Types used for hosting only static members should be implemented as a caseless enum " +
"to avoid instantiation",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
enum Math { // enum
public static let pi = 3.14
}
"""),
Example("""
// class with inheritance
class MathViewController: UIViewController {
public static let pi = 3.14
}
"""),
Example("""
@objc class Math: NSObject { // class visible to Obj-C
public static let pi = 3.14
}
"""),
Example("""
struct Math { // type with non-static declarations
public static let pi = 3.14
public let randomNumber = 2
}
"""),
Example("class DummyClass {}"),
Example("""
class Foo: NSObject { // class with Obj-C class property
class @objc let foo = 1
}
"""),
Example("""
class Foo: NSObject { // class with Obj-C static property
static @objc let foo = 1
}
"""),
Example("""
class Foo { // @objc class func can't exist on an enum
@objc class func foo() {}
}
"""),
Example("""
class Foo { // @objc static func can't exist on an enum
@objc static func foo() {}
}
"""),
Example("""
@objcMembers class Foo { // @objc static func can't exist on an enum
static func foo() {}
}
"""),
Example("""
final class Foo { // final class, but @objc class func can't exist on an enum
@objc class func foo() {}
}
"""),
Example("""
final class Foo { // final class, but @objc static func can't exist on an enum
@objc static func foo() {}
}
"""),
Example("""
@globalActor actor MyActor {
static let shared = MyActor()
}
""")
],
triggeringExamples: [
Example("""
struct Math {
public static let pi = 3.14
}
"""),
Example("""
struct Math {
public static let pi = 3.14
@available(*, unavailable) init() {}
}
"""),
Example("""
final class Foo { // final class can't be inherited
class let foo = 1
}
"""),
// Intentional false positives. Non-final classes could be
// subclassed, but we figure it is probably rare enough that it is
// more important to catch these cases, and manually disable the
// rule if needed.
Example("""
class Foo {
class let foo = 1
}
"""),
Example("""
class Foo {
final class let foo = 1
}
"""),
Example("""
class SomeClass {
static func foo() {}
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension ConvenienceTypeRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
override func visitPost(_ node: StructDeclSyntax) {
if hasViolation(
inheritance: node.inheritanceClause,
attributes: node.attributes,
members: node.memberBlock
) {
violations.append(node.structKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: ClassDeclSyntax) {
if hasViolation(
inheritance: node.inheritanceClause,
attributes: node.attributes,
members: node.memberBlock
) {
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
}
}
private func hasViolation(inheritance: TypeInheritanceClauseSyntax?,
attributes: AttributeListSyntax?,
members: MemberDeclBlockSyntax) -> Bool {
guard inheritance.isNilOrEmpty,
!attributes.containsObjcMembers,
!attributes.containsObjc,
!members.members.isEmpty else {
return false
}
return ConvenienceTypeCheckVisitor(viewMode: .sourceAccurate)
.walk(tree: members, handler: \.canBeConvenienceType)
}
}
}
private class ConvenienceTypeCheckVisitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
private(set) var canBeConvenienceType = true
override func visitPost(_ node: VariableDeclSyntax) {
if node.isInstanceVariable {
canBeConvenienceType = false
} else if node.attributes.containsObjc {
canBeConvenienceType = false
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
if node.modifiers.containsStaticOrClass {
if node.attributes.containsObjc {
canBeConvenienceType = false
}
} else {
canBeConvenienceType = false
}
}
override func visitPost(_ node: InitializerDeclSyntax) {
if !node.attributes.hasUnavailableAttribute {
canBeConvenienceType = false
}
}
override func visitPost(_ node: SubscriptDeclSyntax) {
if !node.modifiers.containsStaticOrClass {
canBeConvenienceType = false
}
}
}
private extension TypeInheritanceClauseSyntax? {
var isNilOrEmpty: Bool {
self?.inheritedTypeCollection.isEmpty ?? true
}
}
private extension AttributeListSyntax? {
var containsObjcMembers: Bool {
contains(attributeNamed: "objcMembers")
}
var containsObjc: Bool {
contains(attributeNamed: "objc")
}
}
private extension AttributeListSyntax? {
var hasUnavailableAttribute: Bool {
guard let attrs = self else {
return false
}
return attrs.contains { elem in
guard let attr = elem.as(AttributeSyntax.self),
let arguments = attr.argument?.as(AvailabilitySpecListSyntax.self) else {
return false
}
return attr.attributeNameText == "available" && arguments.contains { arg in
arg.entry.as(TokenSyntax.self)?.tokenKind.isUnavailableKeyword == true
}
}
}
}

View File

@ -1,45 +0,0 @@
import SwiftSyntax
struct DiscouragedAssertRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "discouraged_assert",
name: "Discouraged Assert",
description: "Prefer `assertionFailure()` and/or `preconditionFailure()` over `assert(false)`",
kind: .idiomatic,
nonTriggeringExamples: [
Example(#"assert(true)"#),
Example(#"assert(true, "foobar")"#),
Example(#"assert(true, "foobar", file: "toto", line: 42)"#),
Example(#"assert(false || true)"#),
Example(#"XCTAssert(false)"#)
],
triggeringExamples: [
Example(#"↓assert(false)"#),
Example(#"↓assert(false, "foobar")"#),
Example(#"↓assert(false, "foobar", file: "toto", line: 42)"#),
Example(#"↓assert( false , "foobar")"#)
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension DiscouragedAssertRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text == "assert",
let firstArg = node.argumentList.first,
firstArg.label == nil,
let boolExpr = firstArg.expression.as(BooleanLiteralExprSyntax.self),
boolExpr.booleanLiteral.tokenKind == .keyword(.false) else {
return
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}

View File

@ -1,105 +0,0 @@
import SwiftSyntax
struct ExplicitEnumRawValueRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "explicit_enum_raw_value",
name: "Explicit Enum Raw Value",
description: "Enums should be explicitly assigned their raw values",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
enum Numbers {
case int(Int)
case short(Int16)
}
"""),
Example("""
enum Numbers: Int {
case one = 1
case two = 2
}
"""),
Example("""
enum Numbers: Double {
case one = 1.1
case two = 2.2
}
"""),
Example("""
enum Numbers: String {
case one = "one"
case two = "two"
}
"""),
Example("""
protocol Algebra {}
enum Numbers: Algebra {
case one
}
""")
],
triggeringExamples: [
Example("""
enum Numbers: Int {
case one = 10, two, three = 30
}
"""),
Example("""
enum Numbers: NSInteger {
case one
}
"""),
Example("""
enum Numbers: String {
case one
case two
}
"""),
Example("""
enum Numbers: String {
case one, two = "two"
}
"""),
Example("""
enum Numbers: Decimal {
case one, two
}
"""),
Example("""
enum Outer {
enum Numbers: Decimal {
case one, two
}
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension ExplicitEnumRawValueRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
override func visitPost(_ node: EnumCaseElementSyntax) {
if node.rawValue == nil, node.enclosingEnum()?.supportsRawValues == true {
violations.append(node.identifier.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension SyntaxProtocol {
func enclosingEnum() -> EnumDeclSyntax? {
if let node = self.as(EnumDeclSyntax.self) {
return node
}
return parent?.enclosingEnum()
}
}

View File

@ -1,118 +0,0 @@
import SwiftSyntax
struct ExplicitTopLevelACLRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "explicit_top_level_acl",
name: "Explicit Top Level ACL",
description: "Top-level declarations should specify Access Control Level keywords explicitly",
kind: .idiomatic,
nonTriggeringExamples: [
Example("internal enum A {}\n"),
Example("public final class B {}\n"),
Example("private struct C {}\n"),
Example("internal enum A {\n enum B {}\n}"),
Example("internal final class Foo {}"),
Example("internal\nclass Foo {}"),
Example("internal func a() {}\n"),
Example("extension A: Equatable {}"),
Example("extension A {}")
],
triggeringExamples: [
Example("↓enum A {}\n"),
Example("final ↓class B {}\n"),
Example("↓struct C {}\n"),
Example("↓func a() {}\n"),
Example("internal let a = 0\n↓func b() {}\n")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension ExplicitTopLevelACLRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: ClassDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: StructDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.structKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: EnumDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.enumKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: ProtocolDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.protocolKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: ActorDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.actorKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: TypealiasDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.typealiasKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: VariableDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
private func hasViolation(modifiers: ModifierListSyntax?) -> Bool {
guard let modifiers else {
return true
}
return !modifiers.contains(where: \.isACLModifier)
}
}
}
private extension DeclModifierSyntax {
var isACLModifier: Bool {
let aclModifiers: Set<TokenKind> = [
.keyword(.private),
.keyword(.fileprivate),
.keyword(.internal),
.keyword(.public),
.keyword(.open)
]
return detail == nil && aclModifiers.contains(name.tokenKind)
}
}

View File

@ -1,142 +0,0 @@
import SwiftSyntax
struct ExplicitTypeInterfaceRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = ExplicitTypeInterfaceConfiguration()
static let description = RuleDescription(
identifier: "explicit_type_interface",
name: "Explicit Type Interface",
description: "Properties should have a type interface",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
class Foo {
var myVar: Int? = 0
}
"""),
Example("""
class Foo {
let myVar: Int? = 0, s: String = ""
}
"""),
Example("""
class Foo {
static var myVar: Int? = 0
}
"""),
Example("""
class Foo {
class var myVar: Int? = 0
}
"""),
Example("""
func f() {
if case .failure(let error) = errorCompletion {}
}
""", excludeFromDocumentation: true)
],
triggeringExamples: [
Example("""
class Foo {
var myVar = 0
}
"""),
Example("""
class Foo {
let mylet = 0
}
"""),
Example("""
class Foo {
static var myStaticVar = 0
}
"""),
Example("""
class Foo {
class var myClassVar = 0
}
"""),
Example("""
class Foo {
let myVar = Int(0), s = ""
}
"""),
Example("""
class Foo {
let myVar = Set<Int>(0)
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(configuration: configuration)
}
}
private class Visitor: ViolationsSyntaxVisitor {
let configuration: ExplicitTypeInterfaceConfiguration
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
init(configuration: ExplicitTypeInterfaceConfiguration) {
self.configuration = configuration
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: VariableDeclSyntax) {
if node.modifiers.isClass {
if configuration.allowedKinds.contains(.class) {
checkViolation(node)
}
} else if node.modifiers.isStatic {
if configuration.allowedKinds.contains(.static) {
checkViolation(node)
}
} else if node.parent?.is(MemberDeclListItemSyntax.self) == true {
if configuration.allowedKinds.contains(.instance) {
checkViolation(node)
}
} else if node.parent?.is(CodeBlockItemSyntax.self) == true {
if configuration.allowedKinds.contains(.local) {
checkViolation(node)
}
}
}
private func checkViolation(_ node: VariableDeclSyntax) {
for binding in node.bindings {
if configuration.allowRedundancy, let initializer = binding.initializer,
initializer.isTypeConstructor || initializer.isTypeReference {
continue
}
if binding.typeAnnotation == nil {
violations.append(binding.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension InitializerClauseSyntax {
var isTypeConstructor: Bool {
if value.as(FunctionCallExprSyntax.self)?.callsPotentialType == true {
return true
}
if let tryExpr = value.as(TryExprSyntax.self),
tryExpr.expression.as(FunctionCallExprSyntax.self)?.callsPotentialType == true {
return true
}
return false
}
var isTypeReference: Bool {
value.as(MemberAccessExprSyntax.self)?.name.tokenKind == .keyword(.self)
}
}
private extension FunctionCallExprSyntax {
var callsPotentialType: Bool {
let name = calledExpression.debugDescription
return name.first?.isUppercase == true || (name.first == "[" && name.last == "]")
}
}

View File

@ -1,63 +0,0 @@
import SwiftSyntax
struct FatalErrorMessageRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "fatal_error_message",
name: "Fatal Error Message",
description: "A fatalError call should have a message",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
func foo() {
fatalError("Foo")
}
"""),
Example("""
func foo() {
fatalError(x)
}
""")
],
triggeringExamples: [
Example("""
func foo() {
fatalError("")
}
"""),
Example("""
func foo() {
fatalError()
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension FatalErrorMessageRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard let expression = node.calledExpression.as(IdentifierExprSyntax.self),
expression.identifier.text == "fatalError",
node.argumentList.isEmptyOrEmptyString else {
return
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
private extension TupleExprElementListSyntax {
var isEmptyOrEmptyString: Bool {
if isEmpty {
return true
}
return count == 1 && first?.expression.as(StringLiteralExprSyntax.self)?.isEmptyString == true
}
}

View File

@ -1,84 +0,0 @@
import SwiftSyntax
struct FileNameRule: ConfigurationProviderRule, OptInRule, SourceKitFreeRule {
var configuration = FileNameConfiguration()
static let description = RuleDescription(
identifier: "file_name",
name: "File Name",
description: "File name should match a type or extension declared in the file (if any)",
kind: .idiomatic
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
guard let filePath = file.path,
case let fileName = filePath.bridge().lastPathComponent,
!configuration.excluded.contains(fileName) else {
return []
}
let prefixRegex = regex("\\A(?:\(configuration.prefixPattern))")
let suffixRegex = regex("(?:\(configuration.suffixPattern))\\z")
var typeInFileName = fileName.bridge().deletingPathExtension
// Process prefix
if let match = prefixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange),
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) {
typeInFileName.removeSubrange(range)
}
// Process nested type separator
let allDeclaredTypeNames = TypeNameCollectingVisitor(viewMode: .sourceAccurate)
.walk(tree: file.syntaxTree, handler: \.names)
.map {
$0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator)
}
guard allDeclaredTypeNames.isNotEmpty, !allDeclaredTypeNames.contains(typeInFileName) else {
return []
}
return [StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: filePath, line: 1))]
}
}
private class TypeNameCollectingVisitor: SyntaxVisitor {
private(set) var names: Set<String> = []
override func visitPost(_ node: ClassDeclSyntax) {
names.insert(node.identifier.text)
}
override func visitPost(_ node: ActorDeclSyntax) {
names.insert(node.identifier.text)
}
override func visitPost(_ node: StructDeclSyntax) {
names.insert(node.identifier.text)
}
override func visitPost(_ node: TypealiasDeclSyntax) {
names.insert(node.identifier.text)
}
override func visitPost(_ node: EnumDeclSyntax) {
names.insert(node.identifier.text)
}
override func visitPost(_ node: ProtocolDeclSyntax) {
names.insert(node.identifier.text)
}
override func visitPost(_ node: ExtensionDeclSyntax) {
names.insert(node.extendedType.trimmedDescription)
}
}

View File

@ -1,189 +0,0 @@
import SwiftSyntax
struct ForWhereRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = ForWhereConfiguration()
static let description = RuleDescription(
identifier: "for_where",
name: "Prefer For-Where",
description: "`where` clauses are preferred over a single `if` inside a `for`",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
for user in users where user.id == 1 { }
"""),
// if let
Example("""
for user in users {
if let id = user.id { }
}
"""),
// if var
Example("""
for user in users {
if var id = user.id { }
}
"""),
// if with else
Example("""
for user in users {
if user.id == 1 { } else { }
}
"""),
// if with else if
Example("""
for user in users {
if user.id == 1 {
} else if user.id == 2 { }
}
"""),
// if is not the only expression inside for
Example("""
for user in users {
if user.id == 1 { }
print(user)
}
"""),
// if a variable is used
Example("""
for user in users {
let id = user.id
if id == 1 { }
}
"""),
// if something is after if
Example("""
for user in users {
if user.id == 1 { }
return true
}
"""),
// condition with multiple clauses
Example("""
for user in users {
if user.id == 1 && user.age > 18 { }
}
"""),
Example("""
for user in users {
if user.id == 1, user.age > 18 { }
}
"""),
// if case
Example("""
for (index, value) in array.enumerated() {
if case .valueB(_) = value {
return index
}
}
"""),
Example("""
for user in users {
if user.id == 1 { return true }
}
""", configuration: ["allow_for_as_filter": true]),
Example("""
for user in users {
if user.id == 1 {
let derivedValue = calculateValue(from: user)
return derivedValue != 0
}
}
""", configuration: ["allow_for_as_filter": true])
],
triggeringExamples: [
Example("""
for user in users {
if user.id == 1 { return true }
}
"""),
Example("""
for subview in subviews {
if !(subview is UIStackView) {
subview.removeConstraints(subview.constraints)
subview.removeFromSuperview()
}
}
"""),
Example("""
for subview in subviews {
if !(subview is UIStackView) {
subview.removeConstraints(subview.constraints)
subview.removeFromSuperview()
}
}
""", configuration: ["allow_for_as_filter": true])
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(allowForAsFilter: configuration.allowForAsFilter)
}
}
private extension ForWhereRule {
final class Visitor: ViolationsSyntaxVisitor {
private let allowForAsFilter: Bool
init(allowForAsFilter: Bool) {
self.allowForAsFilter = allowForAsFilter
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: ForInStmtSyntax) {
guard node.whereClause == nil,
let onlyExprStmt = node.body.statements.onlyElement?.item.as(ExpressionStmtSyntax.self),
let ifExpr = onlyExprStmt.expression.as(IfExprSyntax.self),
ifExpr.elseBody == nil,
!ifExpr.containsOptionalBinding,
!ifExpr.containsPatternCondition,
let condition = ifExpr.conditions.onlyElement,
!condition.containsMultipleConditions else {
return
}
if allowForAsFilter, ifExpr.containsReturnStatement {
return
}
violations.append(ifExpr.positionAfterSkippingLeadingTrivia)
}
}
}
private extension IfExprSyntax {
var containsOptionalBinding: Bool {
conditions.contains { element in
element.condition.is(OptionalBindingConditionSyntax.self)
}
}
var containsPatternCondition: Bool {
conditions.contains { element in
element.condition.is(MatchingPatternConditionSyntax.self)
}
}
var containsReturnStatement: Bool {
body.statements.contains { element in
element.item.is(ReturnStmtSyntax.self)
}
}
}
private extension ConditionElementSyntax {
var containsMultipleConditions: Bool {
guard let condition = condition.as(SequenceExprSyntax.self) else {
return false
}
return condition.elements.contains { expr in
guard let binaryExpr = expr.as(BinaryOperatorExprSyntax.self) else {
return false
}
let operators: Set = ["&&", "||"]
return operators.contains(binaryExpr.operatorToken.text)
}
}
}

View File

@ -1,34 +0,0 @@
import SwiftSyntax
struct ForceCastRule: ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.error)
static let description = RuleDescription(
identifier: "force_cast",
name: "Force Cast",
description: "Force casts should be avoided",
kind: .idiomatic,
nonTriggeringExamples: [
Example("NSNumber() as? Int\n")
],
triggeringExamples: [ Example("NSNumber() ↓as! Int\n") ]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
ForceCastRuleVisitor(viewMode: .sourceAccurate)
}
}
private final class ForceCastRuleVisitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: AsExprSyntax) {
if node.questionOrExclamationMark?.tokenKind == .exclamationMark {
violations.append(node.asTok.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: UnresolvedAsExprSyntax) {
if node.questionOrExclamationMark?.tokenKind == .exclamationMark {
violations.append(node.asTok.positionAfterSkippingLeadingTrivia)
}
}
}

View File

@ -1,132 +0,0 @@
import SwiftSyntax
struct FunctionDefaultParameterAtEndRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "function_default_parameter_at_end",
name: "Function Default Parameter at End",
description: "Prefer to locate parameters with defaults toward the end of the parameter list",
kind: .idiomatic,
nonTriggeringExamples: [
Example("func foo(baz: String, bar: Int = 0) {}"),
Example("func foo(x: String, y: Int = 0, z: CGFloat = 0) {}"),
Example("func foo(bar: String, baz: Int = 0, z: () -> Void) {}"),
Example("func foo(bar: String, z: () -> Void, baz: Int = 0) {}"),
Example("func foo(bar: Int = 0) {}"),
Example("func foo() {}"),
Example("""
class A: B {
override func foo(bar: Int = 0, baz: String) {}
"""),
Example("func foo(bar: Int = 0, completion: @escaping CompletionHandler) {}"),
Example("""
func foo(a: Int, b: CGFloat = 0) {
let block = { (error: Error?) in }
}
"""),
Example("""
func foo(a: String, b: String? = nil,
c: String? = nil, d: @escaping AlertActionHandler = { _ in }) {}
"""),
Example("override init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) {}"),
Example("""
func handleNotification(_ userInfo: NSDictionary,
userInteraction: Bool = false,
completionHandler: ((UIBackgroundFetchResult) -> Void)?) {}
"""),
Example("""
func write(withoutNotifying tokens: [NotificationToken] = {}, _ block: (() throws -> Int)) {}
"""),
Example("""
func expect<T>(file: String = #file, _ expression: @autoclosure () -> (() throws -> T)) -> Expectation<T> {}
""", excludeFromDocumentation: true)
],
triggeringExamples: [
Example("↓func foo(bar: Int = 0, baz: String) {}"),
Example("private ↓func foo(bar: Int = 0, baz: String) {}"),
Example("public ↓init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) {}")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension FunctionDefaultParameterAtEndRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionDeclSyntax) {
guard !node.modifiers.containsOverride, node.signature.containsViolation else {
return
}
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
override func visitPost(_ node: InitializerDeclSyntax) {
guard !node.modifiers.containsOverride, node.signature.containsViolation else {
return
}
violations.append(node.initKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private extension FunctionSignatureSyntax {
var containsViolation: Bool {
let params = input.parameterList.filter { param in
!param.isClosure
}
guard params.isNotEmpty else {
return false
}
let defaultParams = params.filter { param in
param.defaultArgument != nil
}
guard defaultParams.isNotEmpty else {
return false
}
let lastParameters = params.suffix(defaultParams.count)
let lastParametersWithDefaultValue = lastParameters.filter { param in
param.defaultArgument != nil
}
return lastParameters.count != lastParametersWithDefaultValue.count
}
}
private extension FunctionParameterSyntax {
var isClosure: Bool {
if isEscaping || type.is(FunctionTypeSyntax.self) {
return true
}
if let optionalType = type.as(OptionalTypeSyntax.self),
let tuple = optionalType.wrappedType.as(TupleTypeSyntax.self) {
return tuple.elements.onlyElement?.type.as(FunctionTypeSyntax.self) != nil
}
if let tuple = type.as(TupleTypeSyntax.self) {
return tuple.elements.onlyElement?.type.as(FunctionTypeSyntax.self) != nil
}
if let attrType = type.as(AttributedTypeSyntax.self) {
return attrType.baseType.is(FunctionTypeSyntax.self)
}
return false
}
var isEscaping: Bool {
guard let attrType = type.as(AttributedTypeSyntax.self) else {
return false
}
return attrType.attributes.contains(attributeNamed: "escaping")
}
}

View File

@ -1,71 +0,0 @@
import SwiftSyntax
struct ImplicitlyUnwrappedOptionalRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = ImplicitlyUnwrappedOptionalConfiguration()
static let description = RuleDescription(
identifier: "implicitly_unwrapped_optional",
name: "Implicitly Unwrapped Optional",
description: "Implicitly unwrapped optionals should be avoided when possible",
kind: .idiomatic,
nonTriggeringExamples: [
Example("@IBOutlet private var label: UILabel!"),
Example("@IBOutlet var label: UILabel!"),
Example("@IBOutlet var label: [UILabel!]"),
Example("if !boolean {}"),
Example("let int: Int? = 42"),
Example("let int: Int? = nil"),
Example("""
class MyClass {
@IBOutlet
weak var bar: SomeObject!
}
""", configuration: ["mode": "all_except_iboutlets"], excludeFromDocumentation: true)
],
triggeringExamples: [
Example("let label: ↓UILabel!"),
Example("let IBOutlet: ↓UILabel!"),
Example("let labels: [↓UILabel!]"),
Example("var ints: [↓Int!] = [42, nil, 42]"),
Example("let label: ↓IBOutlet!"),
Example("let int: ↓Int! = 42"),
Example("let int: ↓Int! = nil"),
Example("var int: ↓Int! = 42"),
Example("let collection: AnyCollection<↓Int!>"),
Example("func foo(int: ↓Int!) {}"),
Example("""
class MyClass {
weak var bar: SomeObject!
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(mode: configuration.mode)
}
}
private extension ImplicitlyUnwrappedOptionalRule {
final class Visitor: ViolationsSyntaxVisitor {
private let mode: ConfigurationType.ImplicitlyUnwrappedOptionalModeConfiguration
init(mode: ConfigurationType.ImplicitlyUnwrappedOptionalModeConfiguration) {
self.mode = mode
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: ImplicitlyUnwrappedOptionalTypeSyntax) {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
switch mode {
case .all:
return .visitChildren
case .allExceptIBOutlets:
return node.isIBOutlet ? .skipChildren : .visitChildren
}
}
}
}

View File

@ -1,43 +0,0 @@
import SwiftSyntax
struct IsDisjointRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "is_disjoint",
name: "Is Disjoint",
description: "Prefer using `Set.isDisjoint(with:)` over `Set.intersection(_:).isEmpty`",
kind: .idiomatic,
nonTriggeringExamples: [
Example("_ = Set(syntaxKinds).isDisjoint(with: commentAndStringKindsSet)"),
Example("let isObjc = !objcAttributes.isDisjoint(with: dictionary.enclosedSwiftAttributes)"),
Example("_ = Set(syntaxKinds).intersection(commentAndStringKindsSet)"),
Example("_ = !objcAttributes.intersection(dictionary.enclosedSwiftAttributes)")
],
triggeringExamples: [
Example("_ = Set(syntaxKinds).↓intersection(commentAndStringKindsSet).isEmpty"),
Example("let isObjc = !objcAttributes.↓intersection(dictionary.enclosedSwiftAttributes).isEmpty")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension IsDisjointRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: MemberAccessExprSyntax) {
guard
node.name.text == "isEmpty",
let firstBase = node.base?.asFunctionCall,
let firstBaseCalledExpression = firstBase.calledExpression.as(MemberAccessExprSyntax.self),
firstBaseCalledExpression.name.text == "intersection"
else {
return
}
violations.append(firstBaseCalledExpression.name.positionAfterSkippingLeadingTrivia)
}
}
}

View File

@ -1,96 +0,0 @@
import SwiftSyntax
struct JoinedDefaultParameterRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "joined_default_parameter",
name: "Joined Default Parameter",
description: "Discouraged explicit usage of the default separator",
kind: .idiomatic,
nonTriggeringExamples: [
Example("let foo = bar.joined()"),
Example("let foo = bar.joined(separator: \",\")"),
Example("let foo = bar.joined(separator: toto)")
],
triggeringExamples: [
Example("let foo = bar.joined(↓separator: \"\")"),
Example("""
let foo = bar.filter(toto)
.joined(separator: ""),
"""),
Example("""
func foo() -> String {
return ["1", "2"].joined(separator: "")
}
""")
],
corrections: [
Example("let foo = bar.joined(↓separator: \"\")"): Example("let foo = bar.joined()"),
Example("let foo = bar.filter(toto)\n.joined(↓separator: \"\")"):
Example("let foo = bar.filter(toto)\n.joined()"),
Example("func foo() -> String {\n return [\"1\", \"2\"].joined(↓separator: \"\")\n}"):
Example("func foo() -> String {\n return [\"1\", \"2\"].joined()\n}"),
Example("class C {\n#if true\nlet foo = bar.joined(↓separator: \"\")\n#endif\n}"):
Example("class C {\n#if true\nlet foo = bar.joined()\n#endif\n}")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension JoinedDefaultParameterRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
if let violationPosition = node.violationPosition {
violations.append(violationPosition)
}
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let locationConverter: SourceLocationConverter
private let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
guard let violationPosition = node.violationPosition,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
correctionPositions.append(violationPosition)
let newNode = node.with(\.argumentList, [])
return super.visit(newNode)
}
}
}
private extension FunctionCallExprSyntax {
var violationPosition: AbsolutePosition? {
guard let argument = argumentList.first,
let memberExp = calledExpression.as(MemberAccessExprSyntax.self),
memberExp.name.text == "joined",
argument.label?.text == "separator",
let strLiteral = argument.expression.as(StringLiteralExprSyntax.self),
strLiteral.isEmptyString else {
return nil
}
return argument.positionAfterSkippingLeadingTrivia
}
}

View File

@ -1,98 +0,0 @@
import SwiftSyntax
import SwiftSyntaxBuilder
struct LegacyConstantRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "legacy_constant",
name: "Legacy Constant",
description: "Struct-scoped constants are preferred over legacy global constants",
kind: .idiomatic,
nonTriggeringExamples: LegacyConstantRuleExamples.nonTriggeringExamples,
triggeringExamples: LegacyConstantRuleExamples.triggeringExamples,
corrections: LegacyConstantRuleExamples.corrections
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension LegacyConstantRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: IdentifierExprSyntax) {
if LegacyConstantRuleExamples.patterns.keys.contains(node.identifier.text) {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: FunctionCallExprSyntax) {
if node.isLegacyPiExpression {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: IdentifierExprSyntax) -> ExprSyntax {
guard
let correction = LegacyConstantRuleExamples.patterns[node.identifier.text],
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
return ("\(raw: correction)" as ExprSyntax)
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
}
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
guard
node.isLegacyPiExpression,
let calledExpression = node.calledExpression.as(IdentifierExprSyntax.self),
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
return ("\(raw: calledExpression.identifier.text).pi" as ExprSyntax)
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
}
}
}
private extension FunctionCallExprSyntax {
var isLegacyPiExpression: Bool {
guard
let calledExpression = calledExpression.as(IdentifierExprSyntax.self),
calledExpression.identifier.text == "CGFloat" || calledExpression.identifier.text == "Float",
let argument = argumentList.onlyElement?.expression.as(IdentifierExprSyntax.self),
argument.identifier.text == "M_PI"
else {
return false
}
return true
}
}

View File

@ -1,84 +0,0 @@
import SwiftSyntax
struct LegacyMultipleRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "legacy_multiple",
name: "Legacy Multiple",
description: "Prefer using the `isMultiple(of:)` function instead of using the remainder operator (`%`)",
kind: .idiomatic,
nonTriggeringExamples: [
Example("cell.contentView.backgroundColor = indexPath.row.isMultiple(of: 2) ? .gray : .white"),
Example("guard count.isMultiple(of: 2) else { throw DecodingError.dataCorrupted(...) }"),
Example("sanityCheck(bytes > 0 && bytes.isMultiple(of: 4), \"capacity must be multiple of 4 bytes\")"),
Example("guard let i = reversedNumbers.firstIndex(where: { $0.isMultiple(of: 2) }) else { return }"),
Example("""
let constant = 56
let isMultiple = value.isMultiple(of: constant)
"""),
Example("""
let constant = 56
let secret = value % constant == 5
"""),
Example("let secretValue = (value % 3) + 2")
],
triggeringExamples: [
Example("cell.contentView.backgroundColor = indexPath.row ↓% 2 == 0 ? .gray : .white"),
Example("cell.contentView.backgroundColor = 0 == indexPath.row ↓% 2 ? .gray : .white"),
Example("cell.contentView.backgroundColor = indexPath.row ↓% 2 != 0 ? .gray : .white"),
Example("guard count ↓% 2 == 0 else { throw DecodingError.dataCorrupted(...) }"),
Example("sanityCheck(bytes > 0 && bytes ↓% 4 == 0, \"capacity must be multiple of 4 bytes\")"),
Example("guard let i = reversedNumbers.firstIndex(where: { $0 ↓% 2 == 0 }) else { return }"),
Example("""
let constant = 56
let isMultiple = value % constant == 0
""")
]
)
func preprocess(file: SwiftLintFile) -> SourceFileSyntax? {
file.foldedSyntaxTree
}
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension LegacyMultipleRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: InfixOperatorExprSyntax) {
guard let operatorNode = node.operatorOperand.as(BinaryOperatorExprSyntax.self),
operatorNode.operatorToken.tokenKind == .binaryOperator("%"),
let parent = node.parent?.as(InfixOperatorExprSyntax.self),
let parentOperatorNode = parent.operatorOperand.as(BinaryOperatorExprSyntax.self),
parentOperatorNode.isEqualityOrInequalityOperator else {
return
}
let isExprEqualTo0 = {
parent.leftOperand.as(InfixOperatorExprSyntax.self) == node &&
parent.rightOperand.as(IntegerLiteralExprSyntax.self)?.isZero == true
}
let is0EqualToExpr = {
parent.leftOperand.as(IntegerLiteralExprSyntax.self)?.isZero == true &&
parent.rightOperand.as(InfixOperatorExprSyntax.self) == node
}
guard isExprEqualTo0() || is0EqualToExpr() else {
return
}
violations.append(node.operatorOperand.positionAfterSkippingLeadingTrivia)
}
}
}
private extension BinaryOperatorExprSyntax {
var isEqualityOrInequalityOperator: Bool {
operatorToken.tokenKind == .binaryOperator("==") ||
operatorToken.tokenKind == .binaryOperator("!=")
}
}

View File

@ -1,98 +0,0 @@
import SwiftSyntax
private let legacyObjcTypes = [
"NSAffineTransform",
"NSArray",
"NSCalendar",
"NSCharacterSet",
"NSData",
"NSDateComponents",
"NSDateInterval",
"NSDate",
"NSDecimalNumber",
"NSDictionary",
"NSIndexPath",
"NSIndexSet",
"NSLocale",
"NSMeasurement",
"NSNotification",
"NSNumber",
"NSPersonNameComponents",
"NSSet",
"NSString",
"NSTimeZone",
"NSURL",
"NSURLComponents",
"NSURLQueryItem",
"NSURLRequest",
"NSUUID"
]
struct LegacyObjcTypeRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "legacy_objc_type",
name: "Legacy Objective-C Reference Type",
description: "Prefer Swift value types to bridged Objective-C reference types",
kind: .idiomatic,
nonTriggeringExamples: [
Example("var array = Array<Int>()\n"),
Example("var calendar: Calendar? = nil"),
Example("var formatter: NSDataDetector"),
Example("var className: String = NSStringFromClass(MyClass.self)"),
Example("_ = URLRequest.CachePolicy.reloadIgnoringLocalCacheData"),
Example(#"_ = Notification.Name("com.apple.Music.playerInfo")"#)
],
triggeringExamples: [
Example("var array = ↓NSArray()"),
Example("var calendar: ↓NSCalendar? = nil"),
Example("_ = ↓NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData"),
Example(#"_ = ↓NSNotification.Name("com.apple.Music.playerInfo")"#),
Example(#"""
let keyValuePair: (Int) -> (NSString, NSString) = {
let n = "\($0)" as NSString; return (n, n)
}
dictionary = [NSString: NSString](uniqueKeysWithValues:
(1...10_000).lazy.map(keyValuePair))
"""#),
Example("""
extension Foundation.Notification.Name {
static var reachabilityChanged: Foundation.NSNotification.Name {
return Foundation.Notification.Name("org.wordpress.reachability.changed")
}
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension LegacyObjcTypeRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: SimpleTypeIdentifierSyntax) {
if let typeName = node.typeName, legacyObjcTypes.contains(typeName) {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: IdentifierExprSyntax) {
if legacyObjcTypes.contains(node.identifier.text) {
violations.append(node.identifier.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: MemberTypeIdentifierSyntax) {
guard node.baseType.as(SimpleTypeIdentifierSyntax.self)?.typeName == "Foundation",
legacyObjcTypes.contains(node.name.text)
else {
return
}
violations.append(node.name.positionAfterSkippingLeadingTrivia)
}
}
}

View File

@ -1,43 +0,0 @@
import SwiftSyntax
struct LegacyRandomRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "legacy_random",
name: "Legacy Random",
description: "Prefer using `type.random(in:)` over legacy functions",
kind: .idiomatic,
nonTriggeringExamples: [
Example("Int.random(in: 0..<10)\n"),
Example("Double.random(in: 8.6...111.34)\n"),
Example("Float.random(in: 0 ..< 1)\n")
],
triggeringExamples: [
Example("↓arc4random()\n"),
Example("↓arc4random_uniform(83)\n"),
Example("↓drand48()\n")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension LegacyRandomRule {
final class Visitor: ViolationsSyntaxVisitor {
private static let legacyRandomFunctions: Set<String> = [
"arc4random",
"arc4random_uniform",
"drand48"
]
override func visitPost(_ node: FunctionCallExprSyntax) {
if let function = node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text,
Self.legacyRandomFunctions.contains(function) {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
}

View File

@ -1,39 +0,0 @@
import SwiftSyntax
struct NoExtensionAccessModifierRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.error)
static let description = RuleDescription(
identifier: "no_extension_access_modifier",
name: "No Extension Access Modifier",
description: "Prefer not to use extension access modifiers",
kind: .idiomatic,
nonTriggeringExamples: [
Example("extension String {}"),
Example("\n\n extension String {}")
],
triggeringExamples: [
Example("↓private extension String {}"),
Example("↓public \n extension String {}"),
Example("↓open extension String {}"),
Example("↓internal extension String {}"),
Example("↓fileprivate extension String {}")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension NoExtensionAccessModifierRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: ExtensionDeclSyntax) {
if let modifiers = node.modifiers, modifiers.isNotEmpty {
violations.append(modifiers.positionAfterSkippingLeadingTrivia)
}
}
}
}

View File

@ -1,42 +0,0 @@
import SwiftSyntax
struct NoFallthroughOnlyRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "no_fallthrough_only",
name: "No Fallthrough only",
description: "Fallthroughs can only be used if the `case` contains at least one other statement",
kind: .idiomatic,
nonTriggeringExamples: NoFallthroughOnlyRuleExamples.nonTriggeringExamples,
triggeringExamples: NoFallthroughOnlyRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension NoFallthroughOnlyRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: SwitchCaseListSyntax) {
let cases = node.compactMap { $0.as(SwitchCaseSyntax.self) }
let localViolations = cases.enumerated()
.compactMap { index, element -> AbsolutePosition? in
if let fallthroughStmt = element.statements.onlyElement?.item.as(FallthroughStmtSyntax.self) {
if case let nextCaseIndex = cases.index(after: index),
nextCaseIndex < cases.endIndex,
case let nextCase = cases[nextCaseIndex],
nextCase.unknownAttr != nil {
return nil
}
return fallthroughStmt.positionAfterSkippingLeadingTrivia
}
return nil
}
violations.append(contentsOf: localViolations)
}
}
}

View File

@ -1,129 +0,0 @@
import SwiftSyntax
struct NoMagicNumbersRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = NoMagicNumbersConfiguration()
static let description = RuleDescription(
identifier: "no_magic_numbers",
name: "No Magic Numbers",
description: "Magic numbers should be replaced by named constants",
kind: .idiomatic,
nonTriggeringExamples: [
Example("var foo = 123"),
Example("static let bar: Double = 0.123"),
Example("let a = b + 1.0"),
Example("array[0] + array[1] "),
Example("let foo = 1_000.000_01"),
Example("// array[1337]"),
Example("baz(\"9999\")"),
Example("""
func foo() {
let x: Int = 2
let y = 3
let vector = [x, y, -1]
}
"""),
Example("""
class A {
var foo: Double = 132
static let bar: Double = 0.98
}
"""),
Example("""
@available(iOS 13, *)
func version() {
if #available(iOS 13, OSX 10.10, *) {
return
}
}
"""),
Example("""
enum Example: Int {
case positive = 2
case negative = -2
}
"""),
Example("""
class FooTests: XCTestCase {
let array: [Int] = []
let bar = array[42]
}
"""),
Example("""
class FooTests: XCTestCase {
class Bar {
let array: [Int] = []
let bar = array[42]
}
}
""")
],
triggeringExamples: [
Example("foo(↓321)"),
Example("bar(↓1_000.005_01)"),
Example("array[↓42]"),
Example("let box = array[↓12 + ↓14]"),
Example("let a = b + ↓2.0"),
Example("Color.primary.opacity(isAnimate ? ↓0.1 : ↓1.5)")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate, testParentClasses: configuration.testParentClasses)
}
}
private extension NoMagicNumbersRule {
final class Visitor: ViolationsSyntaxVisitor {
private let testParentClasses: Set<String>
init(viewMode: SyntaxTreeViewMode, testParentClasses: Set<String>) {
self.testParentClasses = testParentClasses
super.init(viewMode: viewMode)
}
override func visitPost(_ node: FloatLiteralExprSyntax) {
if node.isMemberOfATestClass(testParentClasses) == false, node.floatingDigits.isMagicNumber {
violations.append(node.floatingDigits.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: IntegerLiteralExprSyntax) {
if node.isMemberOfATestClass(testParentClasses) == false, node.digits.isMagicNumber {
violations.append(node.digits.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension TokenSyntax {
var isMagicNumber: Bool {
guard let number = Double(text.replacingOccurrences(of: "_", with: "")) else {
return false
}
if [0, 1].contains(number) {
return false
}
guard let grandparent = parent?.parent else {
return true
}
return !grandparent.is(InitializerClauseSyntax.self)
&& grandparent.as(PrefixOperatorExprSyntax.self)?.parent?.is(InitializerClauseSyntax.self) != true
}
}
private extension ExprSyntaxProtocol {
func isMemberOfATestClass(_ testParentClasses: Set<String>) -> Bool {
var parent = parent
while parent != nil {
if
let classDecl = parent?.as(ClassDeclSyntax.self),
classDecl.isXCTestCase(testParentClasses)
{
return true
}
parent = parent?.parent
}
return false
}
}

View File

@ -1,117 +0,0 @@
import SwiftSyntax
struct ObjectLiteralRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = ObjectLiteralConfiguration<Self>()
static let description = RuleDescription(
identifier: "object_literal",
name: "Object Literal",
description: "Prefer object literals over image and color inits",
kind: .idiomatic,
nonTriggeringExamples: [
Example("let image = #imageLiteral(resourceName: \"image.jpg\")"),
Example("let color = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)"),
Example("let image = UIImage(named: aVariable)"),
Example("let image = UIImage(named: \"interpolated \\(variable)\")"),
Example("let color = UIColor(red: value, green: value, blue: value, alpha: 1)"),
Example("let image = NSImage(named: aVariable)"),
Example("let image = NSImage(named: \"interpolated \\(variable)\")"),
Example("let color = NSColor(red: value, green: value, blue: value, alpha: 1)")
],
triggeringExamples: ["", ".init"].flatMap { (method: String) -> [Example] in
["UI", "NS"].flatMap { (prefix: String) -> [Example] in
[
Example("let image = ↓\(prefix)Image\(method)(named: \"foo\")"),
Example("let color = ↓\(prefix)Color\(method)(red: 0.3, green: 0.3, blue: 0.3, alpha: 1)"),
// swiftlint:disable:next line_length
Example("let color = ↓\(prefix)Color\(method)(red: 100 / 255.0, green: 50 / 255.0, blue: 0, alpha: 1)"),
Example("let color = ↓\(prefix)Color\(method)(white: 0.5, alpha: 1)")
]
}
}
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(validateImageLiteral: configuration.imageLiteral, validateColorLiteral: configuration.colorLiteral)
}
}
private extension ObjectLiteralRule {
final class Visitor: ViolationsSyntaxVisitor {
private let validateImageLiteral: Bool
private let validateColorLiteral: Bool
init(validateImageLiteral: Bool, validateColorLiteral: Bool) {
self.validateImageLiteral = validateImageLiteral
self.validateColorLiteral = validateColorLiteral
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionCallExprSyntax) {
guard validateColorLiteral || validateImageLiteral else {
return
}
let name = node.calledExpression.trimmedDescription
if validateImageLiteral, isImageNamedInit(node: node, name: name) {
violations.append(node.positionAfterSkippingLeadingTrivia)
} else if validateColorLiteral, isColorInit(node: node, name: name) {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
private func isImageNamedInit(node: FunctionCallExprSyntax, name: String) -> Bool {
guard inits(forClasses: ["UIImage", "NSImage"]).contains(name),
node.argumentList.compactMap(\.label?.text) == ["named"],
let argument = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
argument.isConstantString else {
return false
}
return true
}
private func isColorInit(node: FunctionCallExprSyntax, name: String) -> Bool {
guard inits(forClasses: ["UIColor", "NSColor"]).contains(name),
case let argumentsNames = node.argumentList.compactMap(\.label?.text),
argumentsNames == ["red", "green", "blue", "alpha"] || argumentsNames == ["white", "alpha"] else {
return false
}
return node.argumentList.allSatisfy { elem in
elem.expression.canBeExpressedAsColorLiteralParams
}
}
private func inits(forClasses names: [String]) -> [String] {
return names.flatMap { name in
[
name,
name + ".init"
]
}
}
}
}
private extension StringLiteralExprSyntax {
var isConstantString: Bool {
segments.allSatisfy { $0.is(StringSegmentSyntax.self) }
}
}
private extension ExprSyntax {
var canBeExpressedAsColorLiteralParams: Bool {
if self.is(FloatLiteralExprSyntax.self) ||
self.is(IntegerLiteralExprSyntax.self) ||
self.is(BinaryOperatorExprSyntax.self) {
return true
}
if let expr = self.as(SequenceExprSyntax.self) {
return expr.elements.allSatisfy(\.canBeExpressedAsColorLiteralParams)
}
return false
}
}

View File

@ -1,106 +0,0 @@
import SwiftSyntax
struct PatternMatchingKeywordsRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "pattern_matching_keywords",
name: "Pattern Matching Keywords",
description: "Combine multiple pattern matching bindings by moving keywords out of tuples",
kind: .idiomatic,
nonTriggeringExamples: [
Example("default"),
Example("case 1"),
Example("case bar"),
Example("case let (x, y)"),
Example("case .foo(let x)"),
Example("case let .foo(x, y)"),
Example("case .foo(let x), .bar(let x)"),
Example("case .foo(let x, var y)"),
Example("case var (x, y)"),
Example("case .foo(var x)"),
Example("case var .foo(x, y)")
].map(wrapInSwitch),
triggeringExamples: [
Example("case (↓let x, ↓let y)"),
Example("case (↓let x, ↓let y, .foo)"),
Example("case (↓let x, ↓let y, _)"),
Example("case .foo(↓let x, ↓let y)"),
Example("case (.yamlParsing(↓let x), .yamlParsing(↓let y))"),
Example("case (↓var x, ↓var y)"),
Example("case .foo(↓var x, ↓var y)"),
Example("case (.yamlParsing(↓var x), .yamlParsing(↓var y))")
].map(wrapInSwitch)
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension PatternMatchingKeywordsRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: CaseItemSyntax) {
let localViolations = TupleVisitor(viewMode: .sourceAccurate)
.walk(tree: node.pattern, handler: \.violations)
violations.append(contentsOf: localViolations)
}
}
final class TupleVisitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: TupleExprElementListSyntax) {
let list = node.flatteningEnumPatterns()
.compactMap { elem in
elem.expression.asValueBindingPattern()
}
guard list.count > 1,
let firstLetOrVar = list.first?.bindingKeyword.tokenKind else {
return
}
let hasViolation = list.allSatisfy { elem in
elem.bindingKeyword.tokenKind == firstLetOrVar
}
guard hasViolation else {
return
}
violations.append(contentsOf: list.compactMap { elem in
return elem.bindingKeyword.positionAfterSkippingLeadingTrivia
})
}
}
}
private extension TupleExprElementListSyntax {
func flatteningEnumPatterns() -> [TupleExprElementSyntax] {
flatMap { elem in
guard let pattern = elem.expression.as(FunctionCallExprSyntax.self),
pattern.calledExpression.is(MemberAccessExprSyntax.self) else {
return [elem]
}
return Array(pattern.argumentList)
}
}
}
private extension ExprSyntax {
func asValueBindingPattern() -> ValueBindingPatternSyntax? {
if let pattern = self.as(UnresolvedPatternExprSyntax.self) {
return pattern.pattern.as(ValueBindingPatternSyntax.self)
}
return nil
}
}
private func wrapInSwitch(_ example: Example) -> Example {
return example.with(code: """
switch foo {
\(example.code): break
}
""")
}

View File

@ -1,39 +0,0 @@
import SwiftSyntax
struct PreferNimbleRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "prefer_nimble",
name: "Prefer Nimble",
description: "Prefer Nimble matchers over XCTAssert functions",
kind: .idiomatic,
nonTriggeringExamples: [
Example("expect(foo) == 1"),
Example("expect(foo).to(equal(1))")
],
triggeringExamples: [
Example("↓XCTAssertTrue(foo)"),
Example("↓XCTAssertEqual(foo, 2)"),
Example("↓XCTAssertNotEqual(foo, 2)"),
Example("↓XCTAssertNil(foo)"),
Example("↓XCTAssert(foo)"),
Example("↓XCTAssertGreaterThan(foo, 10)")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension PreferNimbleRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
if let expr = node.calledExpression.as(IdentifierExprSyntax.self),
expr.identifier.text.starts(with: "XCTAssert") {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
}

View File

@ -1,150 +0,0 @@
import SwiftSyntax
struct PreferZeroOverExplicitInitRule: SwiftSyntaxCorrectableRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "prefer_zero_over_explicit_init",
name: "Prefer Zero Over Explicit Init",
description: "Prefer `.zero` over explicit init with zero parameters (e.g. `CGPoint(x: 0, y: 0)`)",
kind: .idiomatic,
nonTriggeringExamples: [
Example("CGRect(x: 0, y: 0, width: 0, height: 1)"),
Example("CGPoint(x: 0, y: -1)"),
Example("CGSize(width: 2, height: 4)"),
Example("CGVector(dx: -5, dy: 0)"),
Example("UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1)")
],
triggeringExamples: [
Example("↓CGPoint(x: 0, y: 0)"),
Example("↓CGPoint(x: 0.000000, y: 0)"),
Example("↓CGPoint(x: 0.000000, y: 0.000)"),
Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"),
Example("↓CGSize(width: 0, height: 0)"),
Example("↓CGVector(dx: 0, dy: 0)"),
Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)")
],
corrections: [
Example("↓CGPoint(x: 0, y: 0)"): Example("CGPoint.zero"),
Example("(↓CGPoint(x: 0, y: 0))"): Example("(CGPoint.zero)"),
Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"): Example("CGRect.zero"),
Example("↓CGSize(width: 0, height: 0.000)"): Example("CGSize.zero"),
Example("↓CGVector(dx: 0, dy: 0)"): Example("CGVector.zero"),
Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)"): Example("UIEdgeInsets.zero")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension PreferZeroOverExplicitInitRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
if node.hasViolation {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
private final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
guard node.hasViolation,
let name = node.name,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let newNode = MemberAccessExprSyntax(name: "zero")
.with(\.base, "\(raw: name)")
return super.visit(
newNode
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
)
}
}
}
private extension FunctionCallExprSyntax {
var hasViolation: Bool {
isCGPointZeroCall ||
isCGSizeCall ||
isCGRectCall ||
isCGVectorCall ||
isUIEdgeInsetsCall
}
var isCGPointZeroCall: Bool {
return name == "CGPoint" &&
argumentNames == ["x", "y"] &&
argumentsAreAllZero
}
var isCGSizeCall: Bool {
return name == "CGSize" &&
argumentNames == ["width", "height"] &&
argumentsAreAllZero
}
var isCGRectCall: Bool {
return name == "CGRect" &&
argumentNames == ["x", "y", "width", "height"] &&
argumentsAreAllZero
}
var isCGVectorCall: Bool {
return name == "CGVector" &&
argumentNames == ["dx", "dy"] &&
argumentsAreAllZero
}
var isUIEdgeInsetsCall: Bool {
return name == "UIEdgeInsets" &&
argumentNames == ["top", "left", "bottom", "right"] &&
argumentsAreAllZero
}
var name: String? {
guard let expr = calledExpression.as(IdentifierExprSyntax.self) else {
return nil
}
return expr.identifier.text
}
var argumentNames: [String?] {
argumentList.map(\.label?.text)
}
var argumentsAreAllZero: Bool {
argumentList.allSatisfy { arg in
if let intExpr = arg.expression.as(IntegerLiteralExprSyntax.self) {
return intExpr.isZero
} else if let floatExpr = arg.expression.as(FloatLiteralExprSyntax.self) {
return floatExpr.isZero
} else {
return false
}
}
}
}

View File

@ -1,272 +0,0 @@
import SwiftSyntax
struct PrivateOverFilePrivateRule: ConfigurationProviderRule, SwiftSyntaxCorrectableRule {
var configuration = PrivateOverFilePrivateConfiguration()
static let description = RuleDescription(
identifier: "private_over_fileprivate",
name: "Private over Fileprivate",
description: "Prefer `private` over `fileprivate` declarations",
kind: .idiomatic,
nonTriggeringExamples: [
Example("extension String {}"),
Example("private extension String {}"),
Example("public \n enum MyEnum {}"),
Example("open extension \n String {}"),
Example("internal extension String {}"),
Example("""
extension String {
fileprivate func Something(){}
}
"""),
Example("""
class MyClass {
fileprivate let myInt = 4
}
"""),
Example("""
class MyClass {
fileprivate(set) var myInt = 4
}
"""),
Example("""
struct Outter {
struct Inter {
fileprivate struct Inner {}
}
}
""")
],
triggeringExamples: [
Example("↓fileprivate enum MyEnum {}"),
Example("""
fileprivate class MyClass {
fileprivate(set) var myInt = 4
}
""")
],
corrections: [
Example("↓fileprivate enum MyEnum {}"): Example("private enum MyEnum {}"),
Example("↓fileprivate enum MyEnum { fileprivate class A {} }"):
Example("private enum MyEnum { fileprivate class A {} }"),
Example("↓fileprivate class MyClass {\nfileprivate(set) var myInt = 4\n}"):
Example("private class MyClass {\nfileprivate(set) var myInt = 4\n}")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(validateExtensions: configuration.validateExtensions)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
validateExtensions: configuration.validateExtensions,
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension PrivateOverFilePrivateRule {
final class Visitor: ViolationsSyntaxVisitor {
private let validateExtensions: Bool
init(validateExtensions: Bool) {
self.validateExtensions = validateExtensions
super.init(viewMode: .sourceAccurate)
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
if validateExtensions, let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind {
if let privateModifier = node.modifiers.fileprivateModifier {
violations.append(privateModifier.positionAfterSkippingLeadingTrivia)
}
return .skipChildren
}
}
private final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let validateExtensions: Bool
private let locationConverter: SourceLocationConverter
private let disabledRegions: [SourceRange]
init(validateExtensions: Bool, locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.validateExtensions = validateExtensions
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
// don't call super in any of the `visit` methods to avoid digging into the children
override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax {
guard validateExtensions, let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
override func visit(_ node: TypealiasDeclSyntax) -> DeclSyntax {
guard let modifier = node.modifiers.fileprivateModifier,
let modifierIndex = node.modifiers.fileprivateModifierIndex,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return DeclSyntax(node)
}
correctionPositions.append(modifier.positionAfterSkippingLeadingTrivia)
let newNode = node.with(\.modifiers, node.modifiers?.replacing(fileprivateModifierIndex: modifierIndex))
return DeclSyntax(newNode)
}
}
}
private extension ModifierListSyntax? {
var fileprivateModifierIndex: ModifierListSyntax.Index? {
self?.firstIndex(where: { $0.name.tokenKind == .keyword(.fileprivate) })
}
var fileprivateModifier: DeclModifierSyntax? {
fileprivateModifierIndex.flatMap { self?[$0] }
}
}
private extension ModifierListSyntax {
func replacing(fileprivateModifierIndex: ModifierListSyntax.Index) -> ModifierListSyntax? {
let fileprivateModifier = self[fileprivateModifierIndex]
return replacing(
childAt: self.distance(from: self.startIndex, to: fileprivateModifierIndex),
with: fileprivateModifier.with(
\.name,
.keyword(
.private,
leadingTrivia: fileprivateModifier.leadingTrivia,
trailingTrivia: fileprivateModifier.trailingTrivia
)
)
)
}
}

View File

@ -1,119 +0,0 @@
import Foundation
import SwiftSyntax
private let attributeNamesImplyingObjc: Set<String> = [
"IBAction", "IBOutlet", "IBInspectable", "GKInspectable", "IBDesignable", "NSManaged"
]
struct RedundantObjcAttributeRule: SwiftSyntaxRule, SubstitutionCorrectableRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "redundant_objc_attribute",
name: "Redundant @objc Attribute",
description: "Objective-C attribute (@objc) is redundant in declaration",
kind: .idiomatic,
nonTriggeringExamples: RedundantObjcAttributeRuleExamples.nonTriggeringExamples,
triggeringExamples: RedundantObjcAttributeRuleExamples.triggeringExamples,
corrections: RedundantObjcAttributeRuleExamples.corrections
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: AttributeListSyntax) {
if let objcAttribute = node.violatingObjCAttribute {
violations.append(objcAttribute.positionAfterSkippingLeadingTrivia)
}
}
}
return Visitor(viewMode: .sourceAccurate)
}
func violationRanges(in file: SwiftLintFile) -> [NSRange] {
makeVisitor(file: file)
.walk(tree: file.syntaxTree, handler: \.violations)
.compactMap { violation in
let end = AbsolutePosition(utf8Offset: violation.position.utf8Offset + "@objc".count)
return file.stringView.NSRange(start: violation.position, end: end)
}
}
}
private extension AttributeListSyntax {
var objCAttribute: AttributeSyntax? {
lazy
.compactMap { $0.as(AttributeSyntax.self) }
.first { $0.attributeNameText == "objc" && $0.argument == nil }
}
var hasAttributeImplyingObjC: Bool {
contains { element in
guard let attributeName = element.as(AttributeSyntax.self)?.attributeNameText else {
return false
}
return attributeNamesImplyingObjc.contains(attributeName)
}
}
}
private extension Syntax {
var isFunctionOrStoredProperty: Bool {
if self.is(FunctionDeclSyntax.self) {
return true
} else if let variableDecl = self.as(VariableDeclSyntax.self),
variableDecl.bindings.allSatisfy({ $0.accessor == nil }) {
return true
} else {
return false
}
}
var functionOrVariableModifiers: ModifierListSyntax? {
if let functionDecl = self.as(FunctionDeclSyntax.self) {
return functionDecl.modifiers
} else if let variableDecl = self.as(VariableDeclSyntax.self) {
return variableDecl.modifiers
}
return nil
}
}
private extension AttributeListSyntax {
var violatingObjCAttribute: AttributeSyntax? {
guard let objcAttribute = objCAttribute else {
return nil
}
if hasAttributeImplyingObjC, parent?.is(ExtensionDeclSyntax.self) != true {
return objcAttribute
} else if parent?.is(EnumDeclSyntax.self) == true {
return nil
} else if parent?.isFunctionOrStoredProperty == true,
let parentClassDecl = parent?.parent?.parent?.parent?.parent?.as(ClassDeclSyntax.self),
parentClassDecl.attributes.contains(attributeNamed: "objcMembers") {
return parent?.functionOrVariableModifiers.isPrivateOrFileprivate == true ? nil : objcAttribute
} else if let parentExtensionDecl = parent?.parent?.parent?.parent?.parent?.as(ExtensionDeclSyntax.self),
parentExtensionDecl.attributes?.objCAttribute != nil {
return objcAttribute
} else {
return nil
}
}
}
extension RedundantObjcAttributeRule {
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
var whitespaceAndNewlineOffset = 0
let nsCharSet = CharacterSet.whitespacesAndNewlines.bridge()
let nsContent = file.contents.bridge()
while nsCharSet
.characterIsMember(nsContent.character(at: violationRange.upperBound + whitespaceAndNewlineOffset)) {
whitespaceAndNewlineOffset += 1
}
let withTrailingWhitespaceAndNewlineRange = NSRange(location: violationRange.location,
length: violationRange.length + whitespaceAndNewlineOffset)
return (withTrailingWhitespaceAndNewlineRange, "")
}
}

View File

@ -1,212 +0,0 @@
import SwiftSyntax
struct RedundantOptionalInitializationRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "redundant_optional_initialization",
name: "Redundant Optional Initialization",
description: "Initializing an optional variable with nil is redundant",
kind: .idiomatic,
nonTriggeringExamples: [
Example("var myVar: Int?\n"),
Example("let myVar: Int? = nil\n"),
Example("var myVar: Int? = 0\n"),
Example("func foo(bar: Int? = 0) { }\n"),
Example("var myVar: Optional<Int>\n"),
Example("let myVar: Optional<Int> = nil\n"),
Example("var myVar: Optional<Int> = 0\n"),
// properties with body should be ignored
Example("""
var foo: Int? {
if bar != nil { }
return 0
}
"""),
// properties with a closure call
Example("""
var foo: Int? = {
if bar != nil { }
return 0
}()
"""),
// lazy variables need to be initialized
Example("lazy var test: Int? = nil"),
// local variables
Example("""
func funcName() {
var myVar: String?
}
"""),
Example("""
func funcName() {
let myVar: String? = nil
}
""")
],
triggeringExamples: triggeringExamples,
corrections: corrections
)
private static let triggeringExamples: [Example] = [
Example("var myVar: Int?↓ = nil\n"),
Example("var myVar: Optional<Int>↓ = nil\n"),
Example("var myVar: Int?↓=nil\n"),
Example("var myVar: Optional<Int>↓=nil\n)"),
Example("""
var myVar: String? = nil {
didSet { print("didSet") }
}
"""),
Example("""
func funcName() {
var myVar: String? = nil
}
""")
]
private static let corrections: [Example: Example] = [
Example("var myVar: Int?↓ = nil\n"): Example("var myVar: Int?\n"),
Example("var myVar: Optional<Int>↓ = nil\n"): Example("var myVar: Optional<Int>\n"),
Example("var myVar: Int?↓=nil\n"): Example("var myVar: Int?\n"),
Example("var myVar: Optional<Int>↓=nil\n"): Example("var myVar: Optional<Int>\n"),
Example("class C {\n#if true\nvar myVar: Int?↓ = nil\n#endif\n}"):
Example("class C {\n#if true\nvar myVar: Int?\n#endif\n}"),
Example("""
var myVar: Int? = nil {
didSet { }
}
"""):
Example("""
var myVar: Int? {
didSet { }
}
"""),
Example("""
var myVar: Int?=nil{
didSet { }
}
"""):
Example("""
var myVar: Int?{
didSet { }
}
"""),
Example("""
func foo() {
var myVar: String? = nil, b: Int
}
"""):
Example("""
func foo() {
var myVar: String?, b: Int
}
""")
]
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension RedundantOptionalInitializationRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: VariableDeclSyntax) {
guard node.bindingKeyword.tokenKind == .keyword(.var),
!node.modifiers.containsLazy else {
return
}
violations.append(contentsOf: node.bindings.compactMap(\.violationPosition))
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let locationConverter: SourceLocationConverter
private let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
guard node.bindingKeyword.tokenKind == .keyword(.var),
!node.modifiers.containsLazy else {
return super.visit(node)
}
let violations = node.bindings
.compactMap { binding in
binding.violationPosition.map { ($0, binding) }
}
.filter { position, _ in
!position.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
}
guard violations.isNotEmpty else {
return super.visit(node)
}
correctionPositions.append(contentsOf: violations.map(\.0))
let violatingBindings = violations.map(\.1)
let newBindings = PatternBindingListSyntax(node.bindings.map { binding in
guard violatingBindings.contains(binding) else {
return binding
}
let newBinding = binding.with(\.initializer, nil)
if newBinding.accessor != nil {
return newBinding
}
if binding.trailingComma != nil {
return newBinding.with(\.typeAnnotation, binding.typeAnnotation?.with(\.trailingTrivia, Trivia()))
}
return newBinding.with(\.trailingTrivia, binding.initializer?.trailingTrivia ?? Trivia())
})
return super.visit(node.with(\.bindings, newBindings))
}
}
}
private extension PatternBindingSyntax {
var violationPosition: AbsolutePosition? {
guard let initializer,
let type = typeAnnotation,
initializer.isInitializingToNil,
type.isOptionalType else {
return nil
}
return type.endPositionBeforeTrailingTrivia
}
}
private extension InitializerClauseSyntax {
var isInitializingToNil: Bool {
value.is(NilLiteralExprSyntax.self)
}
}
private extension TypeAnnotationSyntax {
var isOptionalType: Bool {
if type.is(OptionalTypeSyntax.self) {
return true
}
if let type = type.as(SimpleTypeIdentifierSyntax.self), let genericClause = type.genericArgumentClause {
return genericClause.arguments.count == 1 && type.name.text == "Optional"
}
return false
}
}

View File

@ -1,151 +0,0 @@
import SwiftSyntax
struct RedundantSetAccessControlRule: ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "redundant_set_access_control",
name: "Redundant Access Control for Setter",
description: "Property setter access level shouldn't be explicit if " +
"it's the same as the variable access level",
kind: .idiomatic,
nonTriggeringExamples: [
Example("private(set) public var foo: Int"),
Example("public let foo: Int"),
Example("public var foo: Int"),
Example("var foo: Int"),
Example("""
private final class A {
private(set) var value: Int
}
"""),
Example("""
fileprivate class A {
public fileprivate(set) var value: Int
}
""", excludeFromDocumentation: true),
Example("""
extension Color {
public internal(set) static var someColor = Color.anotherColor
}
""")
],
triggeringExamples: [
Example("↓private(set) private var foo: Int"),
Example("↓fileprivate(set) fileprivate var foo: Int"),
Example("↓internal(set) internal var foo: Int"),
Example("↓public(set) public var foo: Int"),
Example("""
open class Foo {
open(set) open var bar: Int
}
"""),
Example("""
class A {
internal(set) var value: Int
}
"""),
Example("""
internal class A {
internal(set) var value: Int
}
"""),
Example("""
fileprivate class A {
fileprivate(set) var value: Int
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension RedundantSetAccessControlRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
[FunctionDeclSyntax.self]
}
override func visitPost(_ node: VariableDeclSyntax) {
guard let modifiers = node.modifiers, let setAccessor = modifiers.setAccessor else {
return
}
let uniqueModifiers = Set(modifiers.map(\.name.tokenKind))
if uniqueModifiers.count != modifiers.count {
violations.append(modifiers.positionAfterSkippingLeadingTrivia)
return
}
if setAccessor.name.tokenKind == .keyword(.fileprivate),
modifiers.getAccessor == nil,
let closestDeclModifiers = node.closestDecl()?.modifiers {
let closestDeclIsFilePrivate = closestDeclModifiers.contains {
$0.name.tokenKind == .keyword(.fileprivate)
}
if closestDeclIsFilePrivate {
violations.append(modifiers.positionAfterSkippingLeadingTrivia)
return
}
}
if setAccessor.name.tokenKind == .keyword(.internal),
modifiers.getAccessor == nil,
let closesDecl = node.closestDecl(),
let closestDeclModifiers = closesDecl.modifiers {
let closestDeclIsInternal = closestDeclModifiers.isEmpty || closestDeclModifiers.contains {
$0.name.tokenKind == .keyword(.internal)
}
if closestDeclIsInternal {
violations.append(modifiers.positionAfterSkippingLeadingTrivia)
return
}
}
}
}
}
private extension SyntaxProtocol {
func closestDecl() -> DeclSyntax? {
if let decl = self.parent?.as(DeclSyntax.self) {
return decl
}
return parent?.closestDecl()
}
}
private extension DeclSyntax {
var modifiers: ModifierListSyntax? {
if let decl = self.as(ClassDeclSyntax.self) {
return decl.modifiers ?? ModifierListSyntax([])
} else if let decl = self.as(ActorDeclSyntax.self) {
return decl.modifiers ?? ModifierListSyntax([])
} else if let decl = self.as(StructDeclSyntax.self) {
return decl.modifiers ?? ModifierListSyntax([])
} else if let decl = self.as(ProtocolDeclSyntax.self) {
return decl.modifiers ?? ModifierListSyntax([])
} else if let decl = self.as(ExtensionDeclSyntax.self) {
return decl.modifiers ?? ModifierListSyntax([])
} else if let decl = self.as(EnumDeclSyntax.self) {
return decl.modifiers ?? ModifierListSyntax([])
}
return nil
}
}
private extension ModifierListSyntax {
var setAccessor: DeclModifierSyntax? {
first { $0.detail?.detail.tokenKind == .identifier("set") }
}
var getAccessor: DeclModifierSyntax? {
first { $0.detail == nil }
}
}

View File

@ -1,252 +0,0 @@
import SwiftSyntax
struct StrictFilePrivateRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "strict_fileprivate",
name: "Strict Fileprivate",
description: "`fileprivate` should be avoided",
kind: .idiomatic,
nonTriggeringExamples: [
Example("extension String {}"),
Example("private extension String {}"),
Example("""
public
extension String {
var i: Int { 1 }
}
"""),
Example("""
private enum E {
func f() {}
}
"""),
Example("""
public struct S {
internal let i: Int
}
"""),
Example("""
open class C {
private func f() {}
}
"""),
Example("""
internal actor A {}
"""),
Example("""
struct S1: P {
fileprivate let i = 2, j = 1
}
struct S2: P {
fileprivate var (k, l) = (1, 3)
}
protocol P {
var j: Int { get }
var l: Int { get }
}
""", excludeFromDocumentation: true),
Example("""
class C: P<Int> {
fileprivate func f() {}
}
protocol P<T> {
func f()
}
""", excludeFromDocumentation: true)
] + ["actor", "class", "enum", "extension", "struct"].map { type in
Example("""
\(type) T: P<Int> {
fileprivate func f() {}
fileprivate let i = 3
public fileprivate(set) var l = 3
}
protocol P<T> {
func f()
var i: Int { get }
var l: Int { get set }
}
""", excludeFromDocumentation: true)
},
triggeringExamples: [
Example("""
fileprivate class C {
fileprivate func f() {}
}
"""),
Example("""
fileprivate extension String {
fileprivate var isSomething: Bool { self == "something" }
}
"""),
Example("""
fileprivate actor A {
fileprivate let i = 1
}
"""),
Example("""
fileprivate struct C {
fileprivate(set) var myInt = 4
}
"""),
Example("""
struct Outter {
struct Inter {
fileprivate struct Inner {}
}
}
"""),
Example("""
fileprivate func f() {}
""", excludeFromDocumentation: true)
] + ["actor", "class", "enum", "extension", "struct"].map { type in
Example("""
\(type) T: P<Int> {
fileprivate func f() {}
fileprivate func g() {}
fileprivate let i = 2
public fileprivate(set) var j: Int { 1 }
fileprivate let a = 3, b = 4
public fileprivate(set) var k = 2
}
protocol P<T> {
func f()
var i: Int { get }
var k: Int { get }
}
protocol Q {
func g()
var j: Int { get }
}
""", excludeFromDocumentation: true)
}
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate, file: file.syntaxTree)
}
}
private enum ProtocolRequirementType: Equatable {
case method(String)
case getter(String)
case setter(String)
}
private extension StrictFilePrivateRule {
final class ProtocolCollector: ViolationsSyntaxVisitor {
private(set) var protocols = [String: [ProtocolRequirementType]]()
private var currentProtocolName: String = ""
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .allExcept(ProtocolDeclSyntax.self) }
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
currentProtocolName = node.identifier.text
return .visitChildren
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
protocols[currentProtocolName, default: []].append(.method(node.identifier.text))
return .skipChildren
}
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
for binding in node.bindings {
guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
case .accessors(let accessors) = binding.accessor else {
continue
}
if accessors.specifiesGetAccessor {
protocols[currentProtocolName, default: []].append(.getter(name))
}
if accessors.specifiesSetAccessor {
protocols[currentProtocolName, default: []].append(.setter(name))
}
}
return .skipChildren
}
}
final class Visitor: ViolationsSyntaxVisitor {
private let file: SourceFileSyntax
init(viewMode: SyntaxTreeViewMode, file: SourceFileSyntax) {
self.file = file
super.init(viewMode: viewMode)
}
private lazy var protocols = {
ProtocolCollector(viewMode: .sourceAccurate).walk(tree: file, handler: \.protocols)
}()
override func visitPost(_ node: DeclModifierSyntax) {
guard node.name.tokenKind == .keyword(.fileprivate), let grandparent = node.parent?.parent else {
return
}
guard grandparent.is(FunctionDeclSyntax.self) || grandparent.is(VariableDeclSyntax.self) else {
violations.append(node.positionAfterSkippingLeadingTrivia)
return
}
let protocolMethodNames = implementedTypesInDecl(of: node).flatMap { protocols[$0, default: []] }
if let funcDecl = grandparent.as(FunctionDeclSyntax.self),
protocolMethodNames.contains(.method(funcDecl.identifier.text)) {
return
}
if let varDecl = grandparent.as(VariableDeclSyntax.self) {
let isSpecificForSetter = node.detail?.detail.tokenKind == .identifier("set")
let firstImplementingProtocol = varDecl.bindings
.flatMap { binding in
let pattern = binding.pattern
if let name = pattern.as(IdentifierPatternSyntax.self)?.identifier.text {
return [name]
}
if let tuple = pattern.as(TuplePatternSyntax.self) {
return tuple.elements.compactMap {
$0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
}
}
return []
}
.first {
protocolMethodNames.contains(isSpecificForSetter ? .setter($0) : .getter($0))
}
if firstImplementingProtocol != nil {
return
}
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
private func implementedTypesInDecl(of node: SyntaxProtocol?) -> [String] {
guard let node else {
queuedFatalError("Given node is nil. That should not happen.")
}
if node.is(SourceFileSyntax.self) {
return []
}
if let actorDecl = node.as(ActorDeclSyntax.self) {
return actorDecl.inheritanceClause.inheritedTypeNames
}
if let classDecl = node.as(ClassDeclSyntax.self) {
return classDecl.inheritanceClause.inheritedTypeNames
}
if let enumDecl = node.as(EnumDeclSyntax.self) {
return enumDecl.inheritanceClause.inheritedTypeNames
}
if let extensionDecl = node.as(ExtensionDeclSyntax.self) {
return extensionDecl.inheritanceClause.inheritedTypeNames
}
if let structDecl = node.as(StructDeclSyntax.self) {
return structDecl.inheritanceClause.inheritedTypeNames
}
return implementedTypesInDecl(of: node.parent)
}
}
}
private extension TypeInheritanceClauseSyntax? {
var inheritedTypeNames: [String] {
self?.inheritedTypeCollection.compactMap { $0.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text } ?? []
}
}

View File

@ -1,80 +0,0 @@
import SwiftSyntax
struct TrailingSemicolonRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "trailing_semicolon",
name: "Trailing Semicolon",
description: "Lines should not have trailing semicolons",
kind: .idiomatic,
nonTriggeringExamples: [
Example("let a = 0\n"),
Example("let a = 0; let b = 0")
],
triggeringExamples: [
Example("let a = 0↓;\n"),
Example("let a = 0↓;\nlet b = 1\n"),
Example("let a = 0↓; // a comment\n")
],
corrections: [
Example("let a = 0↓;\n"): Example("let a = 0\n"),
Example("let a = 0↓;\nlet b = 1\n"): Example("let a = 0\nlet b = 1\n"),
Example("let foo = 12↓; // comment\n"): Example("let foo = 12 // comment\n")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension TrailingSemicolonRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: TokenSyntax) {
if node.isTrailingSemicolon {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: TokenSyntax) -> TokenSyntax {
guard
node.isTrailingSemicolon,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
return .unknown("").with(\.trailingTrivia, node.trailingTrivia)
}
}
}
private extension TokenSyntax {
var isTrailingSemicolon: Bool {
tokenKind == .semicolon &&
(
trailingTrivia.containsNewlines() ||
(nextToken(viewMode: .sourceAccurate)?.leadingTrivia.containsNewlines() == true)
)
}
}

View File

@ -1,150 +0,0 @@
import Foundation
import SwiftSyntax
struct TypeNameRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = TypeNameConfiguration()
static let description = RuleDescription(
identifier: "type_name",
name: "Type Name",
description: """
Type name should only contain alphanumeric characters, start with an uppercase character and span between \
3 and 40 characters in length.
Private types may start with an underscore.
""",
kind: .idiomatic,
nonTriggeringExamples: TypeNameRuleExamples.nonTriggeringExamples,
triggeringExamples: TypeNameRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(configuration: configuration)
}
}
private extension TypeNameRule {
final class Visitor: ViolationsSyntaxVisitor {
private let configuration: TypeNameConfiguration
init(configuration: TypeNameConfiguration) {
self.configuration = configuration
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: StructDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}
override func visitPost(_ node: ClassDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}
override func visitPost(_ node: TypealiasDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers, inheritedTypes: nil) {
violations.append(violation)
}
}
override func visitPost(_ node: AssociatedtypeDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}
override func visitPost(_ node: EnumDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}
override func visitPost(_ node: ActorDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}
override func visitPost(_ node: ProtocolDeclSyntax) {
if configuration.validateProtocols,
let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}
private func violation(identifier: TokenSyntax,
modifiers: ModifierListSyntax?,
inheritedTypes: InheritedTypeListSyntax?) -> ReasonedRuleViolation? {
let originalName = identifier.text
let nameConfiguration = configuration.nameConfiguration
guard !nameConfiguration.shouldExclude(name: originalName) else { return nil }
let name = originalName
.strippingBackticks()
.strippingLeadingUnderscoreIfPrivate(modifiers: modifiers)
.strippingTrailingSwiftUIPreviewProvider(inheritedTypes: inheritedTypes)
if !nameConfiguration.allowedSymbolsAndAlphanumerics.isSuperset(of: CharacterSet(charactersIn: name)) {
return ReasonedRuleViolation(
position: identifier.positionAfterSkippingLeadingTrivia,
reason: "Type name '\(name)' should only contain alphanumeric and other allowed characters",
severity: .error
)
} else if let caseCheckSeverity = nameConfiguration.validatesStartWithLowercase.severity,
name.first?.isLowercase == true {
return ReasonedRuleViolation(
position: identifier.positionAfterSkippingLeadingTrivia,
reason: "Type name '\(name)' should start with an uppercase character",
severity: caseCheckSeverity
)
} else if let severity = nameConfiguration.severity(forLength: name.count) {
return ReasonedRuleViolation(
position: identifier.positionAfterSkippingLeadingTrivia,
reason: "Type name '\(name)' should be between \(nameConfiguration.minLengthThreshold) and " +
"\(nameConfiguration.maxLengthThreshold) characters long",
severity: severity
)
}
return nil
}
}
}
private extension String {
func strippingBackticks() -> String {
replacingOccurrences(of: "`", with: "")
}
func strippingTrailingSwiftUIPreviewProvider(inheritedTypes: InheritedTypeListSyntax?) -> String {
guard let inheritedTypes,
hasSuffix("_Previews"),
let lastPreviewsIndex = lastIndex(of: "_Previews"),
inheritedTypes.typeNames.contains("PreviewProvider") else {
return self
}
return substring(from: 0, length: lastPreviewsIndex)
}
func strippingLeadingUnderscoreIfPrivate(modifiers: ModifierListSyntax?) -> String {
if first == "_", modifiers.isPrivateOrFileprivate {
return String(self[index(after: startIndex)...])
}
return self
}
}
private extension InheritedTypeListSyntax {
var typeNames: Set<String> {
Set(compactMap { $0.typeName.as(SimpleTypeIdentifierSyntax.self) }.map(\.name.text))
}
}

View File

@ -1,62 +0,0 @@
internal struct TypeNameRuleExamples {
static let nonTriggeringExamples: [Example] = [
Example("class MyType {}"),
Example("private struct _MyType {}"),
Example("enum \(repeatElement("A", count: 40).joined()) {}"),
Example("struct MyView_Previews: PreviewProvider", excludeFromDocumentation: true),
Example("private class _MyView_Previews: PreviewProvider", excludeFromDocumentation: true),
Example("typealias Foo = Void"),
Example("private typealias Foo = Void"),
Example("""
protocol Foo {
associatedtype Bar
}
"""),
Example("""
protocol Foo {
associatedtype Bar: Equatable
}
"""),
Example("enum MyType {\ncase value\n}"),
Example("protocol P {}", configuration: ["validate_protocols": false]),
Example("""
struct SomeStruct {
enum `Type` {
case x, y, z
}
}
""")
]
static let triggeringExamples: [Example] = [
Example("class ↓myType {}"),
Example("enum ↓_MyType {}"),
Example("private struct ↓MyType_ {}"),
Example("private class ↓`_` {}", excludeFromDocumentation: true),
Example("struct ↓My {}"),
Example("struct ↓\(repeatElement("A", count: 41).joined()) {}"),
Example("class ↓MyView_Previews"),
Example("private struct ↓_MyView_Previews"),
Example("struct ↓MyView_Previews_Previews: PreviewProvider", excludeFromDocumentation: true),
Example("typealias ↓X = Void"),
Example("private typealias ↓Foo_Bar = Void"),
Example("private typealias ↓foo = Void"),
Example("typealias ↓\(repeatElement("A", count: 41).joined()) = Void"),
Example("""
protocol Foo {
associatedtype X
}
"""),
Example("""
protocol Foo {
associatedtype Foo_Bar: Equatable
}
"""),
Example("""
protocol Foo {
associatedtype \(repeatElement("A", count: 41).joined())
}
"""),
Example("protocol ↓X {}")
]
}

View File

@ -1,178 +0,0 @@
import SwiftSyntax
struct UnavailableFunctionRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "unavailable_function",
name: "Unavailable Function",
description: "Unimplemented functions should be marked as unavailable",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
class ViewController: UIViewController {
@available(*, unavailable)
public required init?(coder aDecoder: NSCoder) {
preconditionFailure("init(coder:) has not been implemented")
}
}
"""),
Example("""
func jsonValue(_ jsonString: String) -> NSObject {
let data = jsonString.data(using: .utf8)!
let result = try! JSONSerialization.jsonObject(with: data, options: [])
if let dict = (result as? [String: Any])?.bridge() {
return dict
} else if let array = (result as? [Any])?.bridge() {
return array
}
fatalError()
}
"""),
Example("""
func resetOnboardingStateAndCrash() -> Never {
resetUserDefaults()
// Crash the app to re-start the onboarding flow.
fatalError("Onboarding re-start crash.")
}
""")
],
triggeringExamples: [
Example("""
class ViewController: UIViewController {
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
"""),
Example("""
class ViewController: UIViewController {
public required init?(coder aDecoder: NSCoder) {
let reason = "init(coder:) has not been implemented"
fatalError(reason)
}
}
"""),
Example("""
class ViewController: UIViewController {
public required init?(coder aDecoder: NSCoder) {
preconditionFailure("init(coder:) has not been implemented")
}
}
"""),
Example("""
func resetOnboardingStateAndCrash() {
resetUserDefaults()
// Crash the app to re-start the onboarding flow.
fatalError("Onboarding re-start crash.")
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension UnavailableFunctionRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionDeclSyntax) {
guard !node.returnsNever,
!node.attributes.hasUnavailableAttribute,
node.body.containsTerminatingCall,
!node.body.containsReturn else {
return
}
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
override func visitPost(_ node: InitializerDeclSyntax) {
guard !node.attributes.hasUnavailableAttribute,
node.body.containsTerminatingCall,
!node.body.containsReturn else {
return
}
violations.append(node.initKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private extension FunctionDeclSyntax {
var returnsNever: Bool {
if let expr = signature.output?.returnType.as(SimpleTypeIdentifierSyntax.self) {
return expr.name.text == "Never"
}
return false
}
}
private extension AttributeListSyntax? {
var hasUnavailableAttribute: Bool {
guard let attrs = self else {
return false
}
return attrs.contains { elem in
guard let attr = elem.as(AttributeSyntax.self),
let arguments = attr.argument?.as(AvailabilitySpecListSyntax.self) else {
return false
}
let attributeName = attr.attributeNameText
return attributeName == "available" && arguments.contains { arg in
arg.entry.as(TokenSyntax.self)?.tokenKind.isUnavailableKeyword == true
}
}
}
}
private extension CodeBlockSyntax? {
var containsTerminatingCall: Bool {
guard let statements = self?.statements else {
return false
}
let terminatingFunctions: Set = [
"abort",
"fatalError",
"preconditionFailure"
]
return statements.contains { item in
guard let function = item.item.as(FunctionCallExprSyntax.self),
let identifierExpr = function.calledExpression.as(IdentifierExprSyntax.self) else {
return false
}
return terminatingFunctions.contains(identifierExpr.identifier.text)
}
}
var containsReturn: Bool {
guard let statements = self?.statements else {
return false
}
return ReturnFinderVisitor(viewMode: .sourceAccurate)
.walk(tree: statements, handler: \.containsReturn)
}
}
private final class ReturnFinderVisitor: SyntaxVisitor {
private(set) var containsReturn = false
override func visitPost(_ node: ReturnStmtSyntax) {
containsReturn = true
}
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
}

View File

@ -1,88 +0,0 @@
import SwiftSyntax
struct UnusedEnumeratedRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "unused_enumerated",
name: "Unused Enumerated",
description: "When the index or the item is not used, `.enumerated()` can be removed.",
kind: .idiomatic,
nonTriggeringExamples: [
Example("for (idx, foo) in bar.enumerated() { }\n"),
Example("for (_, foo) in bar.enumerated().something() { }\n"),
Example("for (_, foo) in bar.something() { }\n"),
Example("for foo in bar.enumerated() { }\n"),
Example("for foo in bar { }\n"),
Example("for (idx, _) in bar.enumerated().something() { }\n"),
Example("for (idx, _) in bar.something() { }\n"),
Example("for idx in bar.indices { }\n"),
Example("for (section, (event, _)) in data.enumerated() {}\n")
],
triggeringExamples: [
Example("for (↓_, foo) in bar.enumerated() { }\n"),
Example("for (↓_, foo) in abc.bar.enumerated() { }\n"),
Example("for (↓_, foo) in abc.something().enumerated() { }\n"),
Example("for (idx, ↓_) in bar.enumerated() { }\n")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension UnusedEnumeratedRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: ForInStmtSyntax) {
guard let tuplePattern = node.pattern.as(TuplePatternSyntax.self),
tuplePattern.elements.count == 2,
let functionCall = node.sequenceExpr.asFunctionCall,
functionCall.isEnumerated,
let firstElement = tuplePattern.elements.first,
let secondElement = tuplePattern.elements.last,
case let firstTokenIsUnderscore = firstElement.isUnderscore,
case let lastTokenIsUnderscore = secondElement.isUnderscore,
firstTokenIsUnderscore || lastTokenIsUnderscore else {
return
}
let position: AbsolutePosition
let reason: String
if firstTokenIsUnderscore {
position = firstElement.positionAfterSkippingLeadingTrivia
reason = "When the index is not used, `.enumerated()` can be removed"
} else {
position = secondElement.positionAfterSkippingLeadingTrivia
reason = "When the item is not used, `.indices` should be used instead of `.enumerated()`"
}
violations.append(ReasonedRuleViolation(position: position, reason: reason))
}
}
}
private extension FunctionCallExprSyntax {
var isEnumerated: Bool {
guard let memberAccess = calledExpression.as(MemberAccessExprSyntax.self),
memberAccess.base != nil,
memberAccess.name.text == "enumerated",
hasNoArguments else {
return false
}
return true
}
var hasNoArguments: Bool {
trailingClosure == nil &&
(additionalTrailingClosures?.isEmpty ?? true) &&
argumentList.isEmpty
}
}
private extension TuplePatternElementSyntax {
var isUnderscore: Bool {
pattern.is(WildcardPatternSyntax.self)
}
}

View File

@ -1,162 +0,0 @@
import SwiftOperators
import SwiftSyntax
struct XCTSpecificMatcherRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = XCTSpecificMatcherConfiguration()
static let description = RuleDescription(
identifier: "xct_specific_matcher",
name: "XCTest Specific Matcher",
description: "Prefer specific XCTest matchers over `XCTAssertEqual` and `XCTAssertNotEqual`.",
kind: .idiomatic,
nonTriggeringExamples: XCTSpecificMatcherRuleExamples.nonTriggeringExamples,
triggeringExamples: XCTSpecificMatcherRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(configuration: configuration)
}
}
private extension XCTSpecificMatcherRule {
final class Visitor: ViolationsSyntaxVisitor {
let configuration: XCTSpecificMatcherConfiguration
init(configuration: XCTSpecificMatcherConfiguration) {
self.configuration = configuration
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionCallExprSyntax) {
if configuration.matchers.contains(.twoArgumentAsserts),
let suggestion = TwoArgsXCTAssert.violations(in: node) {
violations.append(ReasonedRuleViolation(
position: node.positionAfterSkippingLeadingTrivia,
reason: "Prefer the specific matcher '\(suggestion)' instead"
))
} else if configuration.matchers.contains(.oneArgumentAsserts),
let suggestion = OneArgXCTAssert.violations(in: node) {
violations.append(ReasonedRuleViolation(
position: node.positionAfterSkippingLeadingTrivia,
reason: "Prefer the specific matcher '\(suggestion)' instead"
))
}
}
}
}
private enum OneArgXCTAssert: String {
case assert = "XCTAssert"
case `true` = "XCTAssertTrue"
case `false` = "XCTAssertFalse"
private enum Comparison: String {
case equal = "=="
case unequal = "!="
}
private func suggestion(for comparisonOperator: Comparison) -> String {
switch (self, comparisonOperator) {
case (.assert, .equal): return "XCTAssertEqual"
case (.true, .equal): return "XCTAssertEqual"
case (.assert, .unequal): return "XCTAssertNotEqual"
case (.true, .unequal): return "XCTAssertNotEqual"
case (.false, .equal): return "XCTAssertNotEqual"
case (.false, .unequal): return "XCTAssertEqual"
}
}
static func violations(in node: FunctionCallExprSyntax) -> String? {
guard let name = node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text,
let matcher = Self(rawValue: name),
let argument = node.argumentList.first?.expression.as(SequenceExprSyntax.self),
let folded = try? OperatorTable.standardOperators.foldSingle(argument),
let operatorExpr = folded.as(InfixOperatorExprSyntax.self),
let binOp = operatorExpr.operatorOperand.as(BinaryOperatorExprSyntax.self),
let kind = Comparison(rawValue: binOp.operatorToken.text),
accept(operand: operatorExpr.leftOperand), accept(operand: operatorExpr.rightOperand) else {
return nil
}
return matcher.suggestion(for: kind)
}
private static func accept(operand: ExprSyntax) -> Bool {
// Check if the expression could be a type object like `String.self`. Note, however, that `1.self`
// is also valid Swift. There is no way to be sure here.
if operand.as(MemberAccessExprSyntax.self)?.name.text == "self" {
return false
}
if operand.as(TupleExprSyntax.self)?.elementList.count ?? 0 > 1 {
return false
}
return true
}
}
private enum TwoArgsXCTAssert: String {
case equal = "XCTAssertEqual"
case notEqual = "XCTAssertNotEqual"
private static let protectedArguments: Set<String> = [
"false", "true", "nil"
]
private func suggestion(for protectedArgument: String, hasOptional: Bool) -> String? {
switch (self, protectedArgument, hasOptional) {
case (.equal, "true", false): return "XCTAssertTrue"
case (.equal, "false", false): return "XCTAssertFalse"
case (.equal, "nil", _): return "XCTAssertNil"
case (.notEqual, "true", false): return "XCTAssertFalse"
case (.notEqual, "false", false): return "XCTAssertTrue"
case (.notEqual, "nil", _): return "XCTAssertNotNil"
default: return nil
}
}
static func violations(in node: FunctionCallExprSyntax) -> String? {
guard let name = node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text,
let matcher = Self(rawValue: name) else {
return nil
}
//
// - Gets the first two arguments and creates an array where the protected
// word is the first one (if any).
//
// Examples:
//
// - XCTAssertEqual(foo, true) -> [true, foo]
// - XCTAssertEqual(true, foo) -> [true, foo]
// - XCTAssertEqual(foo, true, "toto") -> [true, foo]
// - XCTAssertEqual(1, 2, accuracy: 0.1, "toto") -> [1, 2]
//
let arguments = node.argumentList
.prefix(2)
.map { $0.expression.trimmedDescription }
.sorted { arg1, _ -> Bool in
return protectedArguments.contains(arg1)
}
//
// - Checks if the number of arguments is two (otherwise there's no need to continue).
// - Checks if the first argument is a protected word (otherwise there's no need to continue).
// - Gets the suggestion for the given protected word (taking in consideration the presence of
// optionals.
//
// Examples:
//
// - equal, [true, foo.bar] -> XCTAssertTrue
// - equal, [true, foo?.bar] -> no violation
// - equal, [nil, foo.bar] -> XCTAssertNil
// - equal, [nil, foo?.bar] -> XCTAssertNil
// - equal, [1, 2] -> no violation
//
guard arguments.count == 2,
let argument = arguments.first, protectedArguments.contains(argument),
let hasOptional = arguments.last?.contains("?"),
let suggestedMatcher = matcher.suggestion(for: argument, hasOptional: hasOptional) else {
return nil
}
return suggestedMatcher
}
}

View File

@ -1,194 +0,0 @@
struct BlanketDisableCommandRule: ConfigurationProviderRule {
var configuration = BlanketDisableCommandConfiguration()
static let description = RuleDescription(
identifier: "blanket_disable_command",
name: "Blanket Disable Command",
description: "swiftlint:disable commands should be re-enabled before the end of the file",
kind: .lint,
nonTriggeringExamples: [
Example("""
// swiftlint:disable unused_import
// swiftlint:enable unused_import
"""),
Example("""
// swiftlint:disable unused_import unused_declaration
// swiftlint:enable unused_import
// swiftlint:enable unused_declaration
"""),
Example("// swiftlint:disable:this unused_import"),
Example("// swiftlint:disable:next unused_import"),
Example("// swiftlint:disable:previous unused_import")
],
triggeringExamples: [
Example("// swiftlint:disable ↓unused_import"),
Example("""
// swiftlint:disable unused_import unused_declaration
// swiftlint:enable unused_import
"""),
Example("""
// swiftlint:disable unused_import
// swiftlint:disable unused_import
// swiftlint:enable unused_import
"""),
Example("""
// swiftlint:enable unused_import
""")
].skipWrappingInCommentTests().skipDisableCommandTests()
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
var violations: [StyleViolation] = []
var ruleIdentifierToCommandMap: [RuleIdentifier: Command] = [:]
var disabledRuleIdentifiers: Set<RuleIdentifier> = []
for command in file.commands {
if command.action == .disable {
violations += validateAlreadyDisabledRules(
for: command,
in: file,
disabledRuleIdentifiers: disabledRuleIdentifiers
)
}
if command.action == .enable {
violations += validateAlreadyEnabledRules(
for: command,
in: file,
disabledRuleIdentifiers: disabledRuleIdentifiers
)
}
if command.modifier != nil {
continue
}
if command.action == .disable {
disabledRuleIdentifiers.formUnion(command.ruleIdentifiers)
command.ruleIdentifiers.forEach { ruleIdentifierToCommandMap[$0] = command }
}
if command.action == .enable {
disabledRuleIdentifiers.subtract(command.ruleIdentifiers)
command.ruleIdentifiers.forEach { ruleIdentifierToCommandMap.removeValue(forKey: $0) }
}
}
violations += validateBlanketDisables(
in: file,
disabledRuleIdentifiers: disabledRuleIdentifiers,
ruleIdentifierToCommandMap: ruleIdentifierToCommandMap
)
violations += validateAlwaysBlanketDisable(file: file)
return violations
}
private func violation(
for command: Command,
ruleIdentifier: RuleIdentifier,
in file: SwiftLintFile,
reason: String
) -> StyleViolation {
violation(for: command, ruleIdentifier: ruleIdentifier.stringRepresentation, in: file, reason: reason)
}
private func violation(
for command: Command,
ruleIdentifier: String,
in file: SwiftLintFile,
reason: String
) -> StyleViolation {
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: command.location(of: ruleIdentifier, in: file),
reason: reason
)
}
private func validateAlreadyDisabledRules(
for command: Command,
in file: SwiftLintFile,
disabledRuleIdentifiers: Set<RuleIdentifier>
) -> [StyleViolation] {
let alreadyDisabledRuleIdentifiers = command.ruleIdentifiers.intersection(disabledRuleIdentifiers)
return alreadyDisabledRuleIdentifiers.map {
let reason = "The disabled '\($0.stringRepresentation)' rule was already disabled"
return violation(for: command, ruleIdentifier: $0, in: file, reason: reason)
}
}
private func validateAlreadyEnabledRules(
for command: Command,
in file: SwiftLintFile,
disabledRuleIdentifiers: Set<RuleIdentifier>
) -> [StyleViolation] {
let notDisabledRuleIdentifiers = command.ruleIdentifiers.subtracting(disabledRuleIdentifiers)
return notDisabledRuleIdentifiers.map {
let reason = "The enabled '\($0.stringRepresentation)' rule was not disabled"
return violation(for: command, ruleIdentifier: $0, in: file, reason: reason)
}
}
private func validateBlanketDisables(
in file: SwiftLintFile,
disabledRuleIdentifiers: Set<RuleIdentifier>,
ruleIdentifierToCommandMap: [RuleIdentifier: Command]
) -> [StyleViolation] {
let allowedRuleIdentifiers = configuration.allowedRuleIdentifiers.union(
configuration.alwaysBlanketDisableRuleIdentifiers
)
return disabledRuleIdentifiers.compactMap { disabledRuleIdentifier in
if allowedRuleIdentifiers.contains(disabledRuleIdentifier.stringRepresentation) {
return nil
}
if let command = ruleIdentifierToCommandMap[disabledRuleIdentifier] {
let reason = "The disabled '\(disabledRuleIdentifier.stringRepresentation)' rule " +
"should be re-enabled before the end of the file"
return violation(for: command, ruleIdentifier: disabledRuleIdentifier, in: file, reason: reason)
}
return nil
}
}
private func validateAlwaysBlanketDisable(file: SwiftLintFile) -> [StyleViolation] {
var violations: [StyleViolation] = []
guard configuration.alwaysBlanketDisableRuleIdentifiers.isEmpty == false else {
return []
}
for command in file.commands {
let ruleIdentifiers: Set<String> = Set(command.ruleIdentifiers.map { $0.stringRepresentation })
let intersection = ruleIdentifiers.intersection(configuration.alwaysBlanketDisableRuleIdentifiers)
if command.action == .enable {
violations.append(contentsOf: intersection.map {
let reason = "The '\($0)' rule applies to the whole file and thus doesn't need to be re-enabled"
return violation(for: command, ruleIdentifier: $0, in: file, reason: reason)
})
} else if command.modifier != nil {
violations.append(contentsOf: intersection.map {
let reason = "The '\($0)' rule applies to the whole file and thus cannot be disabled locally " +
"with 'previous', 'this' or 'next'"
return violation(for: command, ruleIdentifier: $0, in: file, reason: reason)
})
}
}
return violations
}
}
private extension Command {
func location(of ruleIdentifier: String, in file: SwiftLintFile) -> Location {
var location = character
if line > 0, line <= file.lines.count {
let line = file.lines[line - 1].content
if let ruleIdentifierIndex = line.range(of: ruleIdentifier)?.lowerBound {
location = line.distance(from: line.startIndex, to: ruleIdentifierIndex) + 1
}
}
return Location(file: file.file.path, line: line, character: location)
}
}

View File

@ -1,184 +0,0 @@
import SwiftSyntax
struct CompilerProtocolInitRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "compiler_protocol_init",
name: "Compiler Protocol Init",
description: "The initializers declared in compiler protocols such as `ExpressibleByArrayLiteral` " +
"shouldn't be called directly.",
kind: .lint,
nonTriggeringExamples: [
Example("let set: Set<Int> = [1, 2]\n"),
Example("let set = Set(array)\n")
],
triggeringExamples: [
Example("let set = ↓Set(arrayLiteral: 1, 2)\n"),
Example("let set = ↓Set (arrayLiteral: 1, 2)\n"),
Example("let set = ↓Set.init(arrayLiteral: 1, 2)\n"),
Example("let set = ↓Set.init(arrayLiteral : 1, 2)\n")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension CompilerProtocolInitRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard node.trailingClosure == nil else {
return
}
let arguments = node.argumentList.compactMap(\.label)
guard ExpressibleByCompiler.possibleNumberOfArguments.contains(arguments.count) else {
return
}
guard let name = node.functionName, ExpressibleByCompiler.allInitNames.contains(name) else {
return
}
let argumentsNames = arguments.map(\.text)
for compilerProtocol in ExpressibleByCompiler.allProtocols {
guard compilerProtocol.initCallNames.contains(name),
compilerProtocol.match(arguments: argumentsNames) else {
continue
}
violations.append(ReasonedRuleViolation(
position: node.positionAfterSkippingLeadingTrivia,
reason: "Initializers declared in compiler protocol \(compilerProtocol.protocolName) " +
"shouldn't be called directly"
))
return
}
}
}
}
private extension FunctionCallExprSyntax {
// doing this instead of calling `.description` as it's faster
var functionName: String? {
if let expr = calledExpression.as(IdentifierExprSyntax.self) {
return expr.identifier.text
} else if let expr = calledExpression.as(MemberAccessExprSyntax.self),
let base = expr.base?.as(IdentifierExprSyntax.self) {
return base.identifier.text + "." + expr.name.text
}
// we don't care about other possible expressions as they wouldn't match the calls we're interested in
return nil
}
}
private struct ExpressibleByCompiler {
let protocolName: String
let initCallNames: Set<String>
private let arguments: Set<[String]>
private init(protocolName: String, types: Set<String>, arguments: Set<[String]>) {
self.protocolName = protocolName
self.arguments = arguments
initCallNames = Set(types.flatMap { [$0, "\($0).init"] })
}
static let allProtocols = [byArrayLiteral, byNilLiteral, byBooleanLiteral,
byFloatLiteral, byIntegerLiteral, byUnicodeScalarLiteral,
byExtendedGraphemeClusterLiteral, byStringLiteral,
byStringInterpolation, byDictionaryLiteral]
static let possibleNumberOfArguments: Set<Int> = {
allProtocols.reduce(into: Set<Int>()) { partialResult, entry in
partialResult.insert(entry.arguments.count)
}
}()
static let allInitNames: Set<String> = {
allProtocols.reduce(into: Set<String>()) { partialResult, entry in
partialResult.formUnion(entry.initCallNames)
}
}()
func match(arguments: [String]) -> Bool {
return self.arguments.contains(arguments)
}
private static let byArrayLiteral: ExpressibleByCompiler = {
let types: Set = [
"Array",
"ArraySlice",
"ContiguousArray",
"IndexPath",
"NSArray",
"NSCountedSet",
"NSMutableArray",
"NSMutableOrderedSet",
"NSMutableSet",
"NSOrderedSet",
"NSSet",
"SBElementArray",
"Set",
"IndexSet"
]
return Self(protocolName: "ExpressibleByArrayLiteral", types: types, arguments: [["arrayLiteral"]])
}()
private static let byNilLiteral = Self(
protocolName: "ExpressibleByNilLiteral",
types: ["Optional"],
arguments: [["nilLiteral"]]
)
private static let byBooleanLiteral = Self(
protocolName: "ExpressibleByBooleanLiteral",
types: ["Bool", "NSDecimalNumber", "NSNumber", "ObjCBool"],
arguments: [["booleanLiteral"]]
)
private static let byFloatLiteral = Self(
protocolName: "ExpressibleByFloatLiteral",
types: ["Decimal", "NSDecimalNumber", "NSNumber"],
arguments: [["floatLiteral"]]
)
private static let byIntegerLiteral = Self(
protocolName: "ExpressibleByIntegerLiteral",
types: ["Decimal", "Double", "Float", "Float80", "NSDecimalNumber", "NSNumber"],
arguments: [["integerLiteral"]]
)
private static let byUnicodeScalarLiteral = Self(
protocolName: "ExpressibleByUnicodeScalarLiteral",
types: ["StaticString", "String", "UnicodeScalar"],
arguments: [["unicodeScalarLiteral"]]
)
private static let byExtendedGraphemeClusterLiteral = Self(
protocolName: "ExpressibleByExtendedGraphemeClusterLiteral",
types: ["Character", "StaticString", "String"],
arguments: [["extendedGraphemeClusterLiteral"]]
)
private static let byStringLiteral = Self(
protocolName: "ExpressibleByStringLiteral",
types: ["CSLocalizedString", "NSMutableString", "NSString", "Selector", "StaticString", "String"],
arguments: [["stringLiteral"]]
)
private static let byStringInterpolation = Self(
protocolName: "ExpressibleByStringInterpolation",
types: ["String"],
arguments: [["stringInterpolation"], ["stringInterpolationSegment"]]
)
private static let byDictionaryLiteral = Self(
protocolName: "ExpressibleByDictionaryLiteral",
types: ["Dictionary", "DictionaryLiteral", "NSDictionary", "NSMutableDictionary"],
arguments: [["dictionaryLiteral"]]
)
}

View File

@ -1,98 +0,0 @@
import SwiftSyntax
struct DiscardedNotificationCenterObserverRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "discarded_notification_center_observer",
name: "Discarded Notification Center Observer",
description: "When registering for a notification using a block, the opaque observer that is " +
"returned should be stored so it can be removed later",
kind: .lint,
nonTriggeringExamples: [
Example("let foo = nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }\n"),
Example("""
let foo = nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })
"""),
Example("func foo() -> Any {\n" +
" return nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })\n" +
"}\n"),
Example("var obs: [Any?] = []\n" +
"obs.append(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }))\n"),
Example("""
var obs: [String: Any?] = []
obs["foo"] = nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })
"""),
Example("var obs: [Any?] = []\n" +
"obs.append(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }))\n"),
Example("func foo(_ notif: Any) {\n" +
" obs.append(notif)\n" +
"}\n" +
"foo(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }))\n"),
Example("""
var obs: [NSObjectProtocol] = [
nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }),
nc.addObserver(forName: .CKAccountChanged, object: nil, queue: nil, using: { })
]
""")
],
triggeringExamples: [
Example("↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }\n"),
Example("_ = ↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }\n"),
Example("↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })\n"),
Example("""
@discardableResult func foo() -> Any {
return nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension DiscardedNotificationCenterObserverRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
case .identifier("addObserver") = calledExpression.name.tokenKind,
case let argumentLabels = node.argumentList.map({ $0.label?.text }),
argumentLabels.starts(with: ["forName", "object", "queue"])
else {
return
}
if
let firstParent = node.parent?.as(ReturnStmtSyntax.self),
let secondParent = firstParent.parent?.as(CodeBlockItemSyntax.self),
let thirdParent = secondParent.parent?.as(CodeBlockItemListSyntax.self),
let fourthParent = thirdParent.parent?.as(CodeBlockSyntax.self),
let fifthParent = fourthParent.parent?.as(FunctionDeclSyntax.self),
!fifthParent.attributes.hasDiscardableResultAttribute
{
return // result is returned from a function
} else if node.parent?.is(TupleExprElementSyntax.self) == true {
return // result is passed as an argument to a function
} else if node.parent?.is(ArrayElementSyntax.self) == true {
return // result is an array literal element
} else if
let previousToken = node.previousToken(viewMode: .sourceAccurate),
case .equal = previousToken.tokenKind,
previousToken.previousToken(viewMode: .sourceAccurate)?.tokenKind != .wildcard
{
return // result is assigned to something other than the wildcard keyword (`_`)
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
private extension AttributeListSyntax? {
var hasDiscardableResultAttribute: Bool {
contains(attributeNamed: "discardableResult")
}
}

View File

@ -1,61 +0,0 @@
import SwiftSyntax
struct DiscouragedDirectInitRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = DiscouragedDirectInitConfiguration()
static let description = RuleDescription(
identifier: "discouraged_direct_init",
name: "Discouraged Direct Initialization",
description: "Discouraged direct initialization of types that can be harmful",
kind: .lint,
nonTriggeringExamples: [
Example("let foo = UIDevice.current"),
Example("let foo = Bundle.main"),
Example("let foo = Bundle(path: \"bar\")"),
Example("let foo = Bundle(identifier: \"bar\")"),
Example("let foo = Bundle.init(path: \"bar\")"),
Example("let foo = Bundle.init(identifier: \"bar\")"),
Example("let foo = NSError(domain: \"bar\", code: 0)"),
Example("let foo = NSError.init(domain: \"bar\", code: 0)"),
Example("func testNSError()")
],
triggeringExamples: [
Example("↓UIDevice()"),
Example("↓Bundle()"),
Example("let foo = ↓UIDevice()"),
Example("let foo = ↓Bundle()"),
Example("let foo = ↓NSError()"),
Example("let foo = bar(bundle: ↓Bundle(), device: ↓UIDevice(), error: ↓NSError())"),
Example("↓UIDevice.init()"),
Example("↓Bundle.init()"),
Example("↓NSError.init()"),
Example("let foo = ↓UIDevice.init()"),
Example("let foo = ↓Bundle.init()"),
Example("let foo = bar(bundle: ↓Bundle.init(), device: ↓UIDevice.init(), error: ↓NSError.init())")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(discouragedInits: configuration.discouragedInits)
}
}
private extension DiscouragedDirectInitRule {
final class Visitor: ViolationsSyntaxVisitor {
private let discouragedInits: Set<String>
init(discouragedInits: Set<String>) {
self.discouragedInits = discouragedInits
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionCallExprSyntax) {
guard node.argumentList.isEmpty, node.trailingClosure == nil,
discouragedInits.contains(node.calledExpression.trimmedDescription) else {
return
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}

View File

@ -1,219 +0,0 @@
import SwiftSyntax
struct DuplicateConditionsRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.error)
static let description = RuleDescription(
identifier: "duplicate_conditions",
name: "Duplicate Conditions",
description: "Duplicate sets of conditions in the same branch instruction should be avoided",
kind: .lint,
nonTriggeringExamples: [
Example("""
if x < 5 {
foo()
} else if y == "s" {
bar()
}
"""),
Example("""
if x < 5 {
foo()
}
if x < 5 {
bar()
}
"""),
Example("""
if x < 5, y == "s" {
foo()
} else if x < 5 {
bar()
}
"""),
Example("""
switch x {
case \"a\":
foo()
bar()
}
"""),
Example("""
switch x {
case \"a\" where y == "s":
foo()
case \"a\" where y == "t":
bar()
}
"""),
Example("""
if let x = maybeAbc {
foo()
} else if let x = maybePqr {
bar()
}
"""),
Example("""
if let x = maybeAbc, let z = x.maybeY {
foo()
} else if let x = maybePqr, let z = x.maybeY {
bar()
}
"""),
Example("""
if case .p = x {
foo()
} else if case .q = x {
bar()
}
"""),
Example("""
if true {
if true { foo() }
}
""")
],
triggeringExamples: [
Example("""
if x < 5 {
foo()
} else if y == "s" {
bar()
} else if x < 5 {
baz()
}
"""),
Example("""
if z {
if x < 5 {
foo()
} else if y == "s" {
bar()
} else if x < 5 {
baz()
}
}
"""),
Example("""
if x < 5, y == "s" {
foo()
} else if x < 10 {
bar()
} else if y == "s", x < 5 {
baz()
}
"""),
Example("""
switch x {
case \"a\", \"b\":
foo()
case \"c\", ↓\"a\":
bar()
}
"""),
Example("""
switch x {
case \"a\" where y == "s":
foo()
case \"a\" where y == "s":
bar()
}
"""),
Example("""
if let xyz = maybeXyz {
foo()
} else if let xyz = maybeXyz {
bar()
}
"""),
Example("""
if let x = maybeAbc, let z = x.maybeY {
foo()
} else if let x = maybeAbc, let z = x.maybeY {
bar()
}
"""),
Example("""
if #available(macOS 10.15, *) {
foo()
} else if #available(macOS 10.15, *) {
bar()
}
"""),
Example("""
if case .p = x {
foo()
} else if case .p = x {
bar()
}
"""),
Example("""
if x < 5 {}
else if x < 5 {}
else if x < 5 {}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension DuplicateConditionsRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: IfExprSyntax) {
if node.parent?.is(IfExprSyntax.self) == true {
// We can skip these cases - they will be picked up when we visit the top level `if`
return
}
var maybeCurr: IfExprSyntax? = node
var statementChain: [IfExprSyntax] = []
while let curr = maybeCurr {
statementChain.append(curr)
maybeCurr = curr.elseBody?.as(IfExprSyntax.self)
}
let positionsByConditions = statementChain
.reduce(into: [Set<String>: [AbsolutePosition]]()) { acc, elt in
let conditions = elt.conditions.map { $0.condition.trimmedDescription }
let location = elt.conditions.positionAfterSkippingLeadingTrivia
acc[Set(conditions), default: []].append(location)
}
addViolations(Array(positionsByConditions.values))
}
override func visitPost(_ node: SwitchCaseListSyntax) {
let switchCases = node.compactMap { $0.as(SwitchCaseSyntax.self) }
let positionsByCondition = switchCases
.reduce(into: [String: [AbsolutePosition]]()) { acc, elt in
// Defaults don't have a condition to worry about
guard case let .case(caseLabel) = elt.label else { return }
for caseItem in caseLabel.caseItems {
let pattern = caseItem
.pattern
.trimmedDescription
let whereClause = caseItem
.whereClause?
.trimmedDescription
?? ""
let location = caseItem.positionAfterSkippingLeadingTrivia
acc[pattern + whereClause, default: []].append(location)
}
}
addViolations(Array(positionsByCondition.values))
}
private func addViolations(_ positionsByCondition: [[AbsolutePosition]]) {
let duplicatedPositions = positionsByCondition
.filter { $0.count > 1 }
.flatMap { $0 }
violations.append(contentsOf: duplicatedPositions)
}
}
}

View File

@ -1,136 +0,0 @@
import SwiftSyntax
struct DuplicatedKeyInDictionaryLiteralRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "duplicated_key_in_dictionary_literal",
name: "Duplicated Key in Dictionary Literal",
description: "Dictionary literals with duplicated keys will crash at runtime",
kind: .lint,
nonTriggeringExamples: [
Example("""
[
1: "1",
2: "2"
]
"""),
Example("""
[
"1": 1,
"2": 2
]
"""),
Example("""
[
foo: "1",
bar: "2"
]
"""),
Example("""
[
UUID(): "1",
UUID(): "2"
]
"""),
Example("""
[
#line: "1",
#line: "2"
]
""")
],
triggeringExamples: [
Example("""
[
1: "1",
2: "2",
1: "one"
]
"""),
Example("""
[
"1": 1,
"2": 2,
"2": 2
]
"""),
Example("""
[
foo: "1",
bar: "2",
baz: "3",
foo: "4",
zaz: "5"
]
"""),
Example("""
[
.one: "1",
.two: "2",
.three: "3",
.one: "1",
.four: "4",
.five: "5"
]
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension DuplicatedKeyInDictionaryLiteralRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ list: DictionaryElementListSyntax) {
let keys = list.map(\.keyExpression).compactMap { expr -> DictionaryKey? in
expr.stringContent.map {
DictionaryKey(position: expr.positionAfterSkippingLeadingTrivia, content: $0)
}
}
guard keys.count >= 2 else {
return
}
let newViolations = keys
.reduce(into: [String: [DictionaryKey]]()) { result, key in
result[key.content, default: []].append(key)
}
.flatMap { _, value -> [AbsolutePosition] in
guard value.count > 1 else {
return []
}
return value.dropFirst().map(\.position)
}
violations.append(contentsOf: newViolations)
}
}
}
private struct DictionaryKey {
let position: AbsolutePosition
let content: String
}
private extension ExprSyntax {
var stringContent: String? {
if let string = self.as(StringLiteralExprSyntax.self) {
return string.description
} else if let int = self.as(IntegerLiteralExprSyntax.self) {
return int.description
} else if let float = self.as(FloatLiteralExprSyntax.self) {
return float.description
} else if let memberAccess = self.as(MemberAccessExprSyntax.self) {
return memberAccess.description
} else if let identifier = self.as(IdentifierExprSyntax.self) {
return identifier.identifier.text
}
return nil
}
}

View File

@ -1,51 +0,0 @@
import SwiftSyntax
struct EmptyXCTestMethodRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = EmptyXCTestMethodConfiguration()
static let description = RuleDescription(
identifier: "empty_xctest_method",
name: "Empty XCTest Method",
description: "Empty XCTest method should be avoided",
kind: .lint,
nonTriggeringExamples: EmptyXCTestMethodRuleExamples.nonTriggeringExamples,
triggeringExamples: EmptyXCTestMethodRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
EmptyXCTestMethodRuleVisitor(testParentClasses: configuration.testParentClasses)
}
}
private final class EmptyXCTestMethodRuleVisitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
private let testParentClasses: Set<String>
init(testParentClasses: Set<String>) {
self.testParentClasses = testParentClasses
super.init(viewMode: .sourceAccurate)
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
node.isXCTestCase(testParentClasses) ? .visitChildren : .skipChildren
}
override func visitPost(_ node: FunctionDeclSyntax) {
if (node.modifiers.containsOverride || node.isTestMethod) && node.hasEmptyBody {
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private extension FunctionDeclSyntax {
var hasEmptyBody: Bool {
if let body {
return body.statements.isEmpty
}
return false
}
var isTestMethod: Bool {
identifier.text.hasPrefix("test") && signature.input.parameterList.isEmpty
}
}

View File

@ -1,44 +0,0 @@
import SwiftSyntax
struct IBInspectableInExtensionRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "ibinspectable_in_extension",
name: "IBInspectable in Extension",
description: "Extensions shouldn't add @IBInspectable properties",
kind: .lint,
nonTriggeringExamples: [
Example("""
class Foo {
@IBInspectable private var x: Int
}
""")
],
triggeringExamples: [
Example("""
extension Foo {
@IBInspectable private var x: Int
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension IBInspectableInExtensionRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
.allExcept(ExtensionDeclSyntax.self, VariableDeclSyntax.self)
}
override func visitPost(_ node: AttributeSyntax) {
if node.attributeNameText == "IBInspectable" {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
}

View File

@ -1,100 +0,0 @@
import SwiftSyntax
struct IdenticalOperandsRule: ConfigurationProviderRule, SwiftSyntaxRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
private static let operators = ["==", "!=", "===", "!==", ">", ">=", "<", "<="]
static let description = RuleDescription(
identifier: "identical_operands",
name: "Identical Operands",
description: "Comparing two identical operands is likely a mistake",
kind: .lint,
nonTriggeringExamples: operators.flatMap { operation in
[
Example("1 \(operation) 2"),
Example("foo \(operation) bar"),
Example("prefixedFoo \(operation) foo"),
Example("foo.aProperty \(operation) foo.anotherProperty"),
Example("self.aProperty \(operation) self.anotherProperty"),
Example("\"1 \(operation) 1\""),
Example("self.aProperty \(operation) aProperty"),
Example("lhs.aProperty \(operation) rhs.aProperty"),
Example("lhs.identifier \(operation) rhs.identifier"),
Example("i \(operation) index"),
Example("$0 \(operation) 0"),
Example("keyValues?.count ?? 0 \(operation) 0"),
Example("string \(operation) string.lowercased()"),
Example("""
let num: Int? = 0
_ = num != nil && num \(operation) num?.byteSwapped
"""),
Example("num \(operation) num!.byteSwapped"),
Example("1 + 1 \(operation) 1 + 2"),
Example("f( i : 2) \(operation) f (i: 3 )")
]
} + [
Example("func evaluate(_ mode: CommandMode) -> Result<Options, CommandantError<CommandantError<()>>>"),
Example("let array = Array<Array<Int>>()"),
Example("guard Set(identifiers).count != identifiers.count else { return }"),
Example(#"expect("foo") == "foo""#),
Example("type(of: model).cachePrefix == cachePrefix"),
Example("histogram[156].0 == 0x003B8D96 && histogram[156].1 == 1"),
Example(#"[Wrapper(type: .three), Wrapper(type: .one)].sorted { "\($0.type)" > "\($1.type)"}"#),
Example(#"array.sorted { "\($0)" < "\($1)" }"#)
],
triggeringExamples: operators.flatMap { operation in
[
Example("↓1 \(operation) 1"),
Example("↓foo \(operation) foo"),
Example("↓foo.aProperty \(operation) foo.aProperty"),
Example("↓self.aProperty \(operation) self.aProperty"),
Example("↓$0 \(operation) $0"),
Example("↓a?.b \(operation) a?.b"),
Example("if (↓elem \(operation) elem) {}"),
Example("XCTAssertTrue(↓s3 \(operation) s3)"),
Example("if let tab = tabManager.selectedTab, ↓tab.webView \(operation) tab.webView"),
Example("↓1 + 1 \(operation) 1 + 1"),
Example(" ↓f( i : 2) \(operation) f (i: \n 2 )")
]
} + [
Example("""
return lhs.foo == lhs.foo &&
lhs.bar == rhs.bar
"""),
Example("""
return lhs.foo == rhs.foo &&
lhs.bar == lhs.bar
""")
]
)
func preprocess(file: SwiftLintFile) -> SourceFileSyntax? {
file.foldedSyntaxTree
}
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension IdenticalOperandsRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: InfixOperatorExprSyntax) {
guard let operatorNode = node.operatorOperand.as(BinaryOperatorExprSyntax.self),
IdenticalOperandsRule.operators.contains(operatorNode.operatorToken.text) else {
return
}
if node.leftOperand.normalizedDescription == node.rightOperand.normalizedDescription {
violations.append(node.leftOperand.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension ExprSyntax {
var normalizedDescription: String {
debugDescription(includeTrivia: false)
}
}

View File

@ -1,116 +0,0 @@
import SwiftSyntax
// TODO: [12/23/2024] Remove deprecation warning after ~2 years.
private let warnDeprecatedOnceImpl: Void = {
Issue.ruleDeprecated(ruleID: InertDeferRule.description.identifier).print()
}()
private func warnDeprecatedOnce() {
_ = warnDeprecatedOnceImpl
}
struct InertDeferRule: ConfigurationProviderRule, SwiftSyntaxRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "inert_defer",
name: "Inert Defer",
description: "If defer is at the end of its parent scope, it will be executed right where it is anyway",
kind: .lint,
nonTriggeringExamples: [
Example("""
func example3() {
defer { /* deferred code */ }
print("other code")
}
"""),
Example("""
func example4() {
if condition {
defer { /* deferred code */ }
print("other code")
}
}
"""),
Example("""
func f() {
#if os(macOS)
defer { print(2) }
#else
defer { print(3) }
#endif
print(1)
}
""", excludeFromDocumentation: true)
],
triggeringExamples: [
Example("""
func example0() {
defer { /* deferred code */ }
}
"""),
Example("""
func example1() {
defer { /* deferred code */ }
// comment
}
"""),
Example("""
func example2() {
if condition {
defer { /* deferred code */ }
// comment
}
}
"""),
Example("""
func f(arg: Int) {
if arg == 1 {
defer { print(2) }
// a comment
} else {
defer { print(3) }
}
print(1)
#if os(macOS)
defer { print(4) }
#else
defer { print(5) }
#endif
}
""", excludeFromDocumentation: true)
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
warnDeprecatedOnce()
return Visitor(viewMode: .sourceAccurate)
}
}
private extension InertDeferRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: DeferStmtSyntax) {
guard node.isLastStatement else {
return
}
if let ifConfigClause = node.parent?.parent?.parent?.as(IfConfigClauseSyntax.self),
ifConfigClause.parent?.parent?.isLastStatement == false {
return
}
violations.append(node.deferKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private extension SyntaxProtocol {
var isLastStatement: Bool {
if let codeBlockItem = parent?.as(CodeBlockItemSyntax.self),
let codeBlockList = codeBlockItem.parent?.as(CodeBlockItemListSyntax.self) {
return codeBlockList.last == codeBlockItem
}
return false
}
}

View File

@ -1,44 +0,0 @@
struct InvalidSwiftLintCommandRule: ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "invalid_swiftlint_command",
name: "Invalid SwiftLint Command",
description: "swiftlint command does not have a valid action or modifier",
kind: .lint,
nonTriggeringExamples: [
Example("// swiftlint:disable unused_import"),
Example("// swiftlint:enable unused_import"),
Example("// swiftlint:disable:next unused_import"),
Example("// swiftlint:disable:previous unused_import"),
Example("// swiftlint:disable:this unused_import")
],
triggeringExamples: [
Example("// swiftlint:"),
Example("// swiftlint: "),
Example("// swiftlint::"),
Example("// swiftlint:: "),
Example("// swiftlint:disable"),
Example("// swiftlint:dissable unused_import"),
Example("// swiftlint:enaaaable unused_import"),
Example("// swiftlint:disable:nxt unused_import"),
Example("// swiftlint:enable:prevus unused_import"),
Example("// swiftlint:enable:ths unused_import"),
Example("// swiftlint:enable"),
Example("// swiftlint:enable:"),
Example("// swiftlint:enable: "),
Example("// swiftlint:disable: unused_import")
].skipWrappingInCommentTests()
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
file.invalidCommands.map {
let location = Location(file: file.path, line: $0.line, character: $0.character)
return StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: location
)
}
}
}

View File

@ -1,70 +0,0 @@
import SwiftIDEUtils
import SwiftSyntax
struct LocalDocCommentRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "local_doc_comment",
name: "Local Doc Comment",
description: "Prefer regular comments over doc comments in local scopes",
kind: .lint,
nonTriggeringExamples: [
Example("""
func foo() {
// Local scope documentation should use normal comments.
print("foo")
}
"""),
Example("""
/// My great property
var myGreatProperty: String!
"""),
Example("""
/// Look here for more info: https://github.com.
var myGreatProperty: String!
"""),
Example("""
/// Look here for more info:
/// https://github.com.
var myGreatProperty: String!
""")
],
triggeringExamples: [
Example("""
func foo() {
/// Docstring inside a function declaration
print("foo")
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(classifications: file.syntaxClassifications.filter { $0.kind != .none })
}
}
private extension LocalDocCommentRule {
final class Visitor: ViolationsSyntaxVisitor {
private let docCommentRanges: [ByteSourceRange]
init(classifications: [SyntaxClassifiedRange]) {
self.docCommentRanges = classifications
.filter { $0.kind == .docLineComment || $0.kind == .docBlockComment }
.map(\.range)
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionDeclSyntax) {
guard let body = node.body else {
return
}
let violatingRange = docCommentRanges.first { $0.intersects(body.byteRange) }
if let violatingRange {
violations.append(AbsolutePosition(utf8Offset: violatingRange.offset))
}
}
}
}

View File

@ -1,259 +0,0 @@
import SwiftSyntax
struct LowerACLThanParentRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxCorrectableRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "lower_acl_than_parent",
name: "Lower ACL than Parent",
description: "Ensure declarations have a lower access control level than their enclosing parent",
kind: .lint,
nonTriggeringExamples: [
Example("public struct Foo { public func bar() {} }"),
Example("internal struct Foo { func bar() {} }"),
Example("struct Foo { func bar() {} }"),
Example("struct Foo { internal func bar() {} }"),
Example("open class Foo { public func bar() {} }"),
Example("open class Foo { open func bar() {} }"),
Example("fileprivate struct Foo { private func bar() {} }"),
Example("private struct Foo { private func bar(id: String) }"),
Example("extension Foo { public func bar() {} }"),
Example("private struct Foo { fileprivate func bar() {} }"),
Example("private func foo(id: String) {}"),
Example("private class Foo { func bar() {} }"),
Example("public extension Foo { struct Bar { public func baz() {} }}"),
Example("public extension Foo { struct Bar { internal func baz() {} }}"),
Example("internal extension Foo { struct Bar { internal func baz() {} }}"),
Example("extension Foo { struct Bar { internal func baz() {} }}")
],
triggeringExamples: [
Example("struct Foo { ↓public func bar() {} }"),
Example("enum Foo { ↓public func bar() {} }"),
Example("public class Foo { ↓open func bar() }"),
Example("class Foo { ↓public private(set) var bar: String? }"),
Example("private struct Foo { ↓public func bar() {} }"),
Example("private class Foo { ↓public func bar() {} }"),
Example("private actor Foo { ↓public func bar() {} }"),
Example("fileprivate struct Foo { ↓public func bar() {} }"),
Example("class Foo { ↓public func bar() {} }"),
Example("actor Foo { ↓public func bar() {} }"),
Example("private struct Foo { ↓internal func bar() {} }"),
Example("fileprivate struct Foo { ↓internal func bar() {} }"),
Example("extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("internal extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("private extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("fileprivate extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("private extension Foo { struct Bar { ↓internal func baz() {} }}"),
Example("fileprivate extension Foo { struct Bar { ↓internal func baz() {} }}"),
Example("public extension Foo { struct Bar { struct Baz { ↓public func qux() {} }}}"),
Example("final class Foo { ↓public func bar() {} }")
],
corrections: [
Example("struct Foo { ↓public func bar() {} }"):
Example("struct Foo { func bar() {} }"),
Example("enum Foo { ↓public func bar() {} }"):
Example("enum Foo { func bar() {} }"),
Example("public class Foo { ↓open func bar() }"):
Example("public class Foo { public func bar() }"),
Example("class Foo { ↓public private(set) var bar: String? }"):
Example("class Foo { private(set) var bar: String? }"),
Example("private struct Foo { ↓public func bar() {} }"):
Example("private struct Foo { func bar() {} }"),
Example("private class Foo { ↓public func bar() {} }"):
Example("private class Foo { func bar() {} }"),
Example("private actor Foo { ↓public func bar() {} }"):
Example("private actor Foo { func bar() {} }"),
Example("class Foo { ↓public func bar() {} }"):
Example("class Foo { func bar() {} }"),
Example("actor Foo { ↓public func bar() {} }"):
Example("actor Foo { func bar() {} }"),
Example("""
struct Foo {
public func bar() {}
}
"""):
Example("""
struct Foo {
func bar() {}
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension LowerACLThanParentRule {
private final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: DeclModifierSyntax) {
if node.isHigherACLThanParent {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: DeclModifierSyntax) -> DeclModifierSyntax {
guard
node.isHigherACLThanParent,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let newNode: DeclModifierSyntax
if node.name.tokenKind == .keyword(.open) {
newNode = DeclModifierSyntax(
leadingTrivia: node.leadingTrivia,
name: .keyword(.public),
trailingTrivia: .space
)
} else {
newNode = DeclModifierSyntax(
leadingTrivia: node.leadingTrivia,
name: .identifier("")
)
}
return super.visit(newNode)
}
}
}
private extension DeclModifierSyntax {
var isHigherACLThanParent: Bool {
guard let nearestNominalParent = parent?.nearestNominalParent() else {
return false
}
switch name.tokenKind {
case .keyword(.internal)
where nearestNominalParent.modifiers.isPrivate ||
nearestNominalParent.modifiers.isFileprivate:
return true
case .keyword(.internal)
where !nearestNominalParent.modifiers.containsACLModifier:
guard let nominalExtension = nearestNominalParent.nearestNominalExtensionDeclParent() else {
return false
}
return nominalExtension.modifiers.isPrivate ||
nominalExtension.modifiers.isFileprivate
case .keyword(.public)
where nearestNominalParent.modifiers.isPrivate ||
nearestNominalParent.modifiers.isFileprivate ||
nearestNominalParent.modifiers.isInternal:
return true
case .keyword(.public)
where !nearestNominalParent.modifiers.containsACLModifier:
guard let nominalExtension = nearestNominalParent.nearestNominalExtensionDeclParent() else {
return true
}
return !nominalExtension.modifiers.isPublic
case .keyword(.open) where !nearestNominalParent.modifiers.isOpen:
return true
default:
return false
}
}
}
private extension SyntaxProtocol {
func nearestNominalParent() -> Syntax? {
guard let parent else {
return nil
}
return parent.isNominalTypeDecl ? parent : parent.nearestNominalParent()
}
func nearestNominalExtensionDeclParent() -> Syntax? {
guard let parent, !parent.isNominalTypeDecl else {
return nil
}
return parent.isExtensionDecl ? parent : parent.nearestNominalExtensionDeclParent()
}
}
private extension Syntax {
var isNominalTypeDecl: Bool {
self.is(StructDeclSyntax.self) ||
self.is(ClassDeclSyntax.self) ||
self.is(ActorDeclSyntax.self) ||
self.is(EnumDeclSyntax.self)
}
var isExtensionDecl: Bool {
self.is(ExtensionDeclSyntax.self)
}
var modifiers: ModifierListSyntax? {
if let node = self.as(StructDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(ClassDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(ActorDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(EnumDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(ExtensionDeclSyntax.self) {
return node.modifiers
} else {
return nil
}
}
}
private extension ModifierListSyntax? {
var isPrivate: Bool {
self?.contains(where: { $0.name.tokenKind == .keyword(.private) }) == true
}
var isInternal: Bool {
self?.contains(where: { $0.name.tokenKind == .keyword(.internal) }) == true
}
var isPublic: Bool {
self?.contains(where: { $0.name.tokenKind == .keyword(.public) }) == true
}
var isOpen: Bool {
self?.contains(where: { $0.name.tokenKind == .keyword(.open) }) == true
}
var containsACLModifier: Bool {
guard self?.isEmpty == false else {
return false
}
let aclTokens: Set<TokenKind> = [
.keyword(.private),
.keyword(.fileprivate),
.keyword(.internal),
.keyword(.public),
.keyword(.open)
]
return self?.contains(where: {
aclTokens.contains($0.name.tokenKind)
}) == true
}
}

View File

@ -1,79 +0,0 @@
import SwiftSyntax
struct NSLocalizedStringKeyRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "nslocalizedstring_key",
name: "NSLocalizedString Key",
description: "Static strings should be used as key/comment" +
" in NSLocalizedString in order for genstrings to work",
kind: .lint,
nonTriggeringExamples: [
Example("NSLocalizedString(\"key\", comment: \"\")"),
Example("NSLocalizedString(\"key\" + \"2\", comment: \"\")"),
Example("NSLocalizedString(\"key\", comment: \"comment\")"),
Example("""
NSLocalizedString("This is a multi-" +
"line string", comment: "")
"""),
Example("""
let format = NSLocalizedString("%@, %@.", comment: "Accessibility label for a post in the post list." +
" The parameters are the title, and date respectively." +
" For example, \"Let it Go, 1 hour ago.\"")
""")
],
triggeringExamples: [
Example("NSLocalizedString(↓method(), comment: \"\")"),
Example("NSLocalizedString(↓\"key_\\(param)\", comment: \"\")"),
Example("NSLocalizedString(\"key\", comment: ↓\"comment with \\(param)\")"),
Example("NSLocalizedString(↓\"key_\\(param)\", comment: ↓method())")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension NSLocalizedStringKeyRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard node.calledExpression.as(IdentifierExprSyntax.self)?.identifier.text == "NSLocalizedString" else {
return
}
if let keyArgument = node.argumentList.first(where: { $0.label == nil })?.expression,
keyArgument.hasViolation {
violations.append(keyArgument.positionAfterSkippingLeadingTrivia)
}
if let commentArgument = node.argumentList.first(where: { $0.label?.text == "comment" })?.expression,
commentArgument.hasViolation {
violations.append(commentArgument.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension ExprSyntax {
var hasViolation: Bool {
if let strExpr = self.as(StringLiteralExprSyntax.self) {
return strExpr.segments.contains { segment in
!segment.is(StringSegmentSyntax.self)
}
}
if let sequenceExpr = self.as(SequenceExprSyntax.self) {
return sequenceExpr.elements.contains { expr in
if expr.is(BinaryOperatorExprSyntax.self) {
return false
}
return expr.hasViolation
}
}
return true
}
}

View File

@ -1,49 +0,0 @@
import SwiftSyntax
// this rule exists due to a compiler bug: https://github.com/apple/swift/issues/51036
struct NSNumberInitAsFunctionReferenceRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "ns_number_init_as_function_reference",
name: "NSNumber Init as Function Reference",
description: "Passing `NSNumber.init` or `NSDecimalNumber.init` as a function reference is dangerous " +
"as it can cause the wrong initializer to be used, causing crashes; use `.init(value:)` instead",
kind: .lint,
nonTriggeringExamples: [
Example("[0, 0.2].map(NSNumber.init(value:))"),
Example("[0, 0.2].map { NSNumber(value: $0) }"),
Example("[0, 0.2].map(NSDecimalNumber.init(value:))"),
Example("[0, 0.2].map { NSDecimalNumber(value: $0) }")
],
triggeringExamples: [
Example("[0, 0.2].map(↓NSNumber.init)"),
Example("[0, 0.2].map(↓NSDecimalNumber.init)")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension NSNumberInitAsFunctionReferenceRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: MemberAccessExprSyntax) {
guard node.declNameArguments.isEmptyOrNil,
node.name.text == "init",
let baseText = node.base?.as(IdentifierExprSyntax.self)?.identifier.text,
baseText == "NSNumber" || baseText == "NSDecimalNumber" else {
return
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
private extension DeclNameArgumentsSyntax? {
var isEmptyOrNil: Bool {
self?.arguments.isEmpty ?? true
}
}

View File

@ -1,88 +0,0 @@
import SwiftSyntax
struct NSObjectPreferIsEqualRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "nsobject_prefer_isequal",
name: "NSObject Prefer isEqual",
description: "NSObject subclasses should implement isEqual instead of ==",
kind: .lint,
nonTriggeringExamples: NSObjectPreferIsEqualRuleExamples.nonTriggeringExamples,
triggeringExamples: NSObjectPreferIsEqualRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension NSObjectPreferIsEqualRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .extensionsAndProtocols }
override func visitPost(_ node: FunctionDeclSyntax) {
if node.isSelfEqualFunction, node.isInObjcClass {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension ClassDeclSyntax {
var isObjC: Bool {
if attributes.isObjc {
return true
}
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.contains { type in
type.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text == "NSObject"
}
}
}
private extension FunctionDeclSyntax {
var isSelfEqualFunction: Bool {
guard
modifiers.isStatic,
identifier.text == "==",
returnsBool,
case let parameterList = signature.input.parameterList,
parameterList.count == 2,
let lhs = parameterList.first,
let rhs = parameterList.last,
lhs.firstName.text == "lhs",
rhs.firstName.text == "rhs",
lhs.type.trimmedDescription == rhs.type.trimmedDescription
else {
return false
}
return true
}
var returnsBool: Bool {
signature.output?.returnType.as(SimpleTypeIdentifierSyntax.self)?.name.text == "Bool"
}
}
private extension SyntaxProtocol {
var isInObjcClass: Bool {
if let parentClass = parent?.as(ClassDeclSyntax.self) {
return parentClass.isObjC
} else if parent?.as(DeclSyntax.self) != nil {
return false
}
return parent?.isInObjcClass ?? false
}
}
private extension AttributeListSyntax? {
var isObjc: Bool {
contains(attributeNamed: "objc") || contains(attributeNamed: "objcMembers")
}
}

View File

@ -1,54 +0,0 @@
import SwiftSyntax
struct NotificationCenterDetachmentRule: SwiftSyntaxRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "notification_center_detachment",
name: "Notification Center Detachment",
description: "An object should only remove itself as an observer in `deinit`",
kind: .lint,
nonTriggeringExamples: NotificationCenterDetachmentRuleExamples.nonTriggeringExamples,
triggeringExamples: NotificationCenterDetachmentRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension NotificationCenterDetachmentRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard node.isNotificationCenterDettachmentCall,
let arg = node.argumentList.first,
arg.label == nil,
let expr = arg.expression.as(IdentifierExprSyntax.self),
expr.identifier.tokenKind == .keyword(.self) else {
return
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
}
}
private extension FunctionCallExprSyntax {
var isNotificationCenterDettachmentCall: Bool {
guard trailingClosure == nil,
argumentList.count == 1,
let expr = calledExpression.as(MemberAccessExprSyntax.self),
expr.name.text == "removeObserver",
let baseExpr = expr.base?.as(MemberAccessExprSyntax.self),
baseExpr.name.text == "default",
baseExpr.base?.as(IdentifierExprSyntax.self)?.identifier.text == "NotificationCenter" else {
return false
}
return true
}
}

View File

@ -1,85 +0,0 @@
import SwiftSyntax
struct OverrideInExtensionRule: ConfigurationProviderRule, OptInRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "override_in_extension",
name: "Override in Extension",
description: "Extensions shouldn't override declarations",
kind: .lint,
nonTriggeringExamples: [
Example("extension Person {\n var age: Int { return 42 }\n}\n"),
Example("extension Person {\n func celebrateBirthday() {}\n}\n"),
Example("class Employee: Person {\n override func celebrateBirthday() {}\n}\n"),
Example("""
class Foo: NSObject {}
extension Foo {
override var description: String { return "" }
}
"""),
Example("""
struct Foo {
class Bar: NSObject {}
}
extension Foo.Bar {
override var description: String { return "" }
}
""")
],
triggeringExamples: [
Example("extension Person {\n override ↓var age: Int { return 42 }\n}\n"),
Example("extension Person {\n override ↓func celebrateBirthday() {}\n}\n")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
let allowedExtensions = ClassNameCollectingVisitor(viewMode: .sourceAccurate)
.walk(tree: file.syntaxTree, handler: \.classNames)
return Visitor(allowedExtensions: allowedExtensions)
}
}
private extension OverrideInExtensionRule {
final class Visitor: ViolationsSyntaxVisitor {
private let allowedExtensions: Set<String>
init(allowedExtensions: Set<String>) {
self.allowedExtensions = allowedExtensions
super.init(viewMode: .sourceAccurate)
}
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .allExcept(ExtensionDeclSyntax.self) }
override func visitPost(_ node: FunctionDeclSyntax) {
if node.modifiers.containsOverride {
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: VariableDeclSyntax) {
if node.modifiers.containsOverride {
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
guard let type = node.extendedType.as(SimpleTypeIdentifierSyntax.self),
!allowedExtensions.contains(type.name.text) else {
return .skipChildren
}
return .visitChildren
}
}
}
private class ClassNameCollectingVisitor: ViolationsSyntaxVisitor {
private(set) var classNames: Set<String> = []
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: ClassDeclSyntax) {
classNames.insert(node.identifier.text)
}
}

View File

@ -1,92 +0,0 @@
import Foundation
import SourceKittenFramework
import SwiftIDEUtils
struct PeriodSpacingRule: SourceKitFreeRule, ConfigurationProviderRule, OptInRule, SubstitutionCorrectableRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "period_spacing",
name: "Period Spacing",
description: "Periods should not be followed by more than one space",
kind: .style,
nonTriggeringExamples: [
Example("let pi = 3.2"),
Example("let pi = Double.pi"),
Example("let pi = Double. pi"),
Example("let pi = Double. pi"),
Example("// A. Single."),
Example("/// - code: Identifier of the error. Integer."),
Example("""
// value: Multiline.
// Comment.
"""),
Example("""
/**
Sentence ended in period.
- Sentence 2 new line characters after.
**/
""")
],
triggeringExamples: [
Example("/* Only god knows why. ↓ This symbol does nothing. */", testWrappingInComment: false),
Example("// Only god knows why. ↓ This symbol does nothing.", testWrappingInComment: false),
Example("// Single. Double. ↓ End.", testWrappingInComment: false),
Example("// Single. Double. ↓ Triple. ↓ End.", testWrappingInComment: false),
Example("// Triple. ↓ Quad. ↓ End.", testWrappingInComment: false),
Example("/// - code: Identifier of the error. ↓ Integer.", testWrappingInComment: false)
],
corrections: [
Example("/* Why. ↓ Symbol does nothing. */"): Example("/* Why. Symbol does nothing. */"),
Example("// Why. ↓ Symbol does nothing."): Example("// Why. Symbol does nothing."),
Example("// Single. Double. ↓ End."): Example("// Single. Double. End."),
Example("// Single. Double. ↓ Triple. ↓ End."): Example("// Single. Double. Triple. End."),
Example("// Triple. ↓ Quad. ↓ End."): Example("// Triple. Quad. End."),
Example("/// - code: Identifier. ↓ Integer."): Example("/// - code: Identifier. Integer.")
]
)
func violationRanges(in file: SwiftLintFile) -> [NSRange] {
// Find all comment tokens in the file and regex search them for violations
file.syntaxClassifications
.filter(\.kind.isComment)
.map { $0.range.toSourceKittenByteRange() }
.compactMap { (range: ByteRange) -> [NSRange]? in
return file.stringView
.substringWithByteRange(range)
.map(StringView.init)
.map { commentBody in
// Look for a period followed by two or more whitespaces but not new line or carriage returns
return regex(#"\.[^\S\r\n]{2,}"#)
.matches(in: commentBody)
.compactMap { result in
// Set the location to start from the second whitespace till the last one.
return file.stringView.byteRangeToNSRange(
ByteRange(
// Safe to mix NSRange offsets with byte offsets here because the
// regex can't contain multi-byte characters
location: ByteCount(range.lowerBound.value + result.range.lowerBound + 2),
length: ByteCount(result.range.length.advanced(by: -2))
)
)
}
}
}
.flatMap { $0 }
}
func validate(file: SwiftLintFile) -> [StyleViolation] {
return violationRanges(in: file).map { range in
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: range.location)
)
}
}
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
return (violationRange, "")
}
}

View File

@ -1,129 +0,0 @@
import SwiftSyntax
struct PrivateOutletRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = PrivateOutletConfiguration()
static let description = RuleDescription(
identifier: "private_outlet",
name: "Private Outlets",
description: "IBOutlets should be private to avoid leaking UIKit to higher layers",
kind: .lint,
nonTriggeringExamples: [
Example("class Foo {\n @IBOutlet private var label: UILabel?\n}\n"),
Example("class Foo {\n @IBOutlet private var label: UILabel!\n}\n"),
Example("class Foo {\n var notAnOutlet: UILabel\n}\n"),
Example("class Foo {\n @IBOutlet weak private var label: UILabel?\n}\n"),
Example("class Foo {\n @IBOutlet private weak var label: UILabel?\n}\n"),
Example("class Foo {\n @IBOutlet fileprivate weak var label: UILabel?\n}\n"),
// allow_private_set
Example(
"class Foo {\n @IBOutlet private(set) var label: UILabel?\n}\n",
configuration: ["allow_private_set": true]
),
Example(
"class Foo {\n @IBOutlet private(set) var label: UILabel!\n}\n",
configuration: ["allow_private_set": true]
),
Example(
"class Foo {\n @IBOutlet weak private(set) var label: UILabel?\n}\n",
configuration: ["allow_private_set": true]
),
Example(
"class Foo {\n @IBOutlet private(set) weak var label: UILabel?\n}\n",
configuration: ["allow_private_set": true]
),
Example(
"class Foo {\n @IBOutlet fileprivate(set) weak var label: UILabel?\n}\n",
configuration: ["allow_private_set": true]
)
],
triggeringExamples: [
Example("class Foo {\n @IBOutlet ↓var label: UILabel?\n}\n"),
Example("class Foo {\n @IBOutlet ↓var label: UILabel!\n}\n"),
Example("class Foo {\n @IBOutlet private(set) ↓var label: UILabel?\n}\n"),
Example("class Foo {\n @IBOutlet fileprivate(set) ↓var label: UILabel?\n}\n"),
Example("""
import Gridicons
class BlogDetailsSectionHeaderView: UITableViewHeaderFooterView {
typealias EllipsisCallback = (BlogDetailsSectionHeaderView) -> Void
@IBOutlet private var titleLabel: UILabel?
@objc @IBOutlet private(set) var ellipsisButton: UIButton? {
didSet {
ellipsisButton?.setImage(UIImage.gridicon(.ellipsis), for: .normal)
}
}
@objc var title: String = "" {
didSet {
titleLabel?.text = title.uppercased()
}
}
@objc var ellipsisButtonDidTouch: EllipsisCallback?
override func awakeFromNib() {
super.awakeFromNib()
titleLabel?.textColor = .textSubtle
}
@IBAction func ellipsisTapped() {
ellipsisButtonDidTouch?(self)
}
}
""", configuration: ["allow_private_set": false], excludeFromDocumentation: true)
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(allowPrivateSet: configuration.allowPrivateSet)
}
}
private extension PrivateOutletRule {
final class Visitor: ViolationsSyntaxVisitor {
private let allowPrivateSet: Bool
init(allowPrivateSet: Bool) {
self.allowPrivateSet = allowPrivateSet
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: MemberDeclListItemSyntax) {
guard
let decl = node.decl.as(VariableDeclSyntax.self),
decl.attributes.contains(attributeNamed: "IBOutlet"),
decl.modifiers?.isPrivateOrFilePrivate != true
else {
return
}
if allowPrivateSet && decl.modifiers?.isPrivateOrFilePrivateSet == true {
return
}
violations.append(decl.bindingKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private extension ModifierListSyntax {
var isPrivateOrFilePrivate: Bool {
contains(where: \.isPrivateOrFilePrivate)
}
var isPrivateOrFilePrivateSet: Bool {
contains(where: \.isPrivateOrFilePrivateSet)
}
}
private extension ModifierListSyntax.Element {
var isPrivateOrFilePrivate: Bool {
(name.text == "private" || name.text == "fileprivate") && detail == nil
}
var isPrivateOrFilePrivateSet: Bool {
(name.text == "private" || name.text == "fileprivate") && detail?.detail.text == "set"
}
}

View File

@ -1,60 +0,0 @@
import SwiftSyntax
struct PrivateSubjectRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "private_subject",
name: "Private Combine Subject",
description: "Combine Subject should be private",
kind: .lint,
nonTriggeringExamples: PrivateSubjectRuleExamples.nonTriggeringExamples,
triggeringExamples: PrivateSubjectRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension PrivateSubjectRule {
final class Visitor: ViolationsSyntaxVisitor {
private let subjectTypes: Set<String> = ["PassthroughSubject", "CurrentValueSubject"]
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
[FunctionDeclSyntax.self, VariableDeclSyntax.self, SubscriptDeclSyntax.self]
}
override func visitPost(_ node: VariableDeclSyntax) {
guard !node.modifiers.isPrivateOrFileprivate,
!node.modifiers.containsStaticOrClass else {
return
}
for binding in node.bindings {
// Looks for violations matching the format:
//
// * `let subject: PassthroughSubject<Bool, Never>`
// * `let subject: PassthroughSubject<Bool, Never> = .init()`
// * `let subject: CurrentValueSubject<Bool, Never>`
// * `let subject: CurrentValueSubject<String, Never> = .init("toto")`
if let type = binding.typeAnnotation?.type.as(SimpleTypeIdentifierSyntax.self),
subjectTypes.contains(type.name.text) {
violations.append(binding.pattern.positionAfterSkippingLeadingTrivia)
continue
}
// Looks for violations matching the format:
//
// * `let subject = PassthroughSubject<Bool, Never>()`
// * `let subject = CurrentValueSubject<String, Never>("toto")`
if let functionCall = binding.initializer?.value.as(FunctionCallExprSyntax.self),
let specializeExpr = functionCall.calledExpression.as(SpecializeExprSyntax.self),
let identifierExpr = specializeExpr.expression.as(IdentifierExprSyntax.self),
subjectTypes.contains(identifierExpr.identifier.text) {
violations.append(binding.pattern.positionAfterSkippingLeadingTrivia)
}
}
}
}
}

View File

@ -1,279 +0,0 @@
import Foundation
import SwiftSyntax
struct PrivateUnitTestRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, CacheDescriptionProvider {
var configuration = PrivateUnitTestConfiguration()
var cacheDescription: String {
return configuration.cacheDescription
}
static let description = RuleDescription(
identifier: "private_unit_test",
name: "Private Unit Test",
description: "Unit tests marked private are silently skipped",
kind: .lint,
nonTriggeringExamples: [
Example("""
class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
internal class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
public class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
@objc private class FooTest: XCTestCase {
@objc private func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
// Non-test classes
Example("""
private class Foo: NSObject {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
private class Foo {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
// Non-test methods
Example("""
public class FooTest: XCTestCase {
private func test1(param: Int) {}
private func test2() -> String { "" }
private func atest() {}
private static func test3() {}
}
""")
],
triggeringExamples: [
Example("""
private class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
"""),
Example("""
class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
"""),
Example("""
internal class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
"""),
Example("""
public class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
""")
],
corrections: [
Example("""
private class Test: XCTestCase {}
"""): Example("""
class Test: XCTestCase {}
"""),
Example("""
class Test: XCTestCase {
private func test1() {}
private func test2(i: Int) {}
@objc private func test3() {}
internal func test4() {}
}
"""): Example("""
class Test: XCTestCase {
func test1() {}
private func test2(i: Int) {}
@objc private func test3() {}
internal func test4() {}
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(parentClassRegex: configuration.regex)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
parentClassRegex: configuration.regex,
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private class Visitor: ViolationsSyntaxVisitor {
private let parentClassRegex: NSRegularExpression
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
init(parentClassRegex: NSRegularExpression) {
self.parentClassRegex = parentClassRegex
super.init(viewMode: .sourceAccurate)
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
!node.isPrivate && node.hasParent(matching: parentClassRegex) ? .visitChildren : .skipChildren
}
override func visitPost(_ node: ClassDeclSyntax) {
if node.isPrivate, node.hasParent(matching: parentClassRegex) {
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
if node.isTestMethod, node.isPrivate {
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let parentClassRegex: NSRegularExpression
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(parentClassRegex: NSRegularExpression,
locationConverter: SourceLocationConverter,
disabledRegions: [SourceRange]) {
self.parentClassRegex = parentClassRegex
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
guard
node.isPrivate,
node.hasParent(matching: parentClassRegex),
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let (modifiers, declKeyword) = withoutPrivate(modifiers: node.modifiers, declKeyword: node.classKeyword)
return super.visit(node.with(\.modifiers, modifiers).with(\.classKeyword, declKeyword))
}
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
guard
node.isTestMethod,
node.isPrivate,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let (modifiers, declKeyword) = withoutPrivate(modifiers: node.modifiers, declKeyword: node.funcKeyword)
return super.visit(node.with(\.modifiers, modifiers).with(\.funcKeyword, declKeyword))
}
private func withoutPrivate(modifiers: ModifierListSyntax?,
declKeyword: TokenSyntax) -> (ModifierListSyntax?, TokenSyntax) {
guard let modifiers else {
return (nil, declKeyword)
}
var filteredModifiers = [DeclModifierSyntax]()
var leadingTrivia = Trivia()
for modifier in modifiers {
let accumulatedLeadingTrivia = leadingTrivia + (modifier.leadingTrivia)
if modifier.name.tokenKind == .keyword(.private) {
leadingTrivia = accumulatedLeadingTrivia
} else {
filteredModifiers.append(modifier.with(\.leadingTrivia, accumulatedLeadingTrivia))
leadingTrivia = Trivia()
}
}
let declKeyword = declKeyword.with(\.leadingTrivia, leadingTrivia + (declKeyword.leadingTrivia))
return (ModifierListSyntax(filteredModifiers), declKeyword)
}
}
private extension ClassDeclSyntax {
func hasParent(matching pattern: NSRegularExpression) -> Bool {
inheritanceClause?.inheritedTypeCollection.contains { type in
if let name = type.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text {
return pattern.numberOfMatches(in: name, range: name.fullNSRange) > 0
}
return false
} ?? false
}
var isPrivate: Bool {
resultInPrivateProperty(modifiers: modifiers, attributes: attributes)
}
}
private extension FunctionDeclSyntax {
var isPrivate: Bool {
resultInPrivateProperty(modifiers: modifiers, attributes: attributes)
}
var isTestMethod: Bool {
identifier.text.hasPrefix("test")
&& signature.input.parameterList.isEmpty
&& signature.output == nil
&& !(modifiers?.hasStatic ?? false)
}
}
private extension ModifierListSyntax {
var hasPrivate: Bool {
contains { $0.name.tokenKind == .keyword(.private) }
}
var hasStatic: Bool {
contains { $0.name.tokenKind == .keyword(.static) }
}
}
private func resultInPrivateProperty(modifiers: ModifierListSyntax?, attributes: AttributeListSyntax?) -> Bool {
guard let modifiers, modifiers.hasPrivate else {
return false
}
return !attributes.contains(attributeNamed: "objc")
}

View File

@ -1,48 +0,0 @@
import SwiftSyntax
struct ProhibitedInterfaceBuilderRule: ConfigurationProviderRule, SwiftSyntaxRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "prohibited_interface_builder",
name: "Prohibited Interface Builder",
description: "Creating views using Interface Builder should be avoided",
kind: .lint,
nonTriggeringExamples: [
wrapExample("var label: UILabel!"),
wrapExample("@objc func buttonTapped(_ sender: UIButton) {}")
],
triggeringExamples: [
wrapExample("@IBOutlet ↓var label: UILabel!"),
wrapExample("@IBAction ↓func buttonTapped(_ sender: UIButton) {}")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension ProhibitedInterfaceBuilderRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: VariableDeclSyntax) {
if node.isIBOutlet {
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
if node.isIBAction {
violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
}
private func wrapExample(_ text: String, file: StaticString = #file, line: UInt = #line) -> Example {
return Example("""
class ViewController: UIViewController {
\(text)
}
""", file: file, line: line)
}

View File

@ -1,65 +0,0 @@
import SwiftSyntax
struct QuickDiscouragedFocusedTestRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "quick_discouraged_focused_test",
name: "Quick Discouraged Focused Test",
description: "Non-focused tests won't run as long as this test is focused",
kind: .lint,
nonTriggeringExamples: QuickDiscouragedFocusedTestRuleExamples.nonTriggeringExamples,
triggeringExamples: QuickDiscouragedFocusedTestRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension QuickDiscouragedFocusedTestRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: FunctionCallExprSyntax) {
if let identifierExpr = node.calledExpression.as(IdentifierExprSyntax.self),
case let name = identifierExpr.identifier.text,
QuickFocusedCallKind(rawValue: name) != nil {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
node.containsInheritance ? .visitChildren : .skipChildren
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
node.isSpecFunction ? .visitChildren : .skipChildren
}
}
}
private extension ClassDeclSyntax {
var containsInheritance: Bool {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.isNotEmpty
}
}
private extension FunctionDeclSyntax {
var isSpecFunction: Bool {
return identifier.tokenKind == .identifier("spec") &&
signature.input.parameterList.isEmpty &&
modifiers.containsOverride
}
}
private enum QuickFocusedCallKind: String {
case fdescribe
case fcontext
case fit
case fitBehavesLike
}

View File

@ -1,66 +0,0 @@
import SwiftSyntax
struct QuickDiscouragedPendingTestRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "quick_discouraged_pending_test",
name: "Quick Discouraged Pending Test",
description: "This test won't run as long as it's marked pending",
kind: .lint,
nonTriggeringExamples: QuickDiscouragedPendingTestRuleExamples.nonTriggeringExamples,
triggeringExamples: QuickDiscouragedPendingTestRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension QuickDiscouragedPendingTestRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: FunctionCallExprSyntax) {
if let identifierExpr = node.calledExpression.as(IdentifierExprSyntax.self),
case let name = identifierExpr.identifier.text,
QuickPendingCallKind(rawValue: name) != nil {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
node.containsInheritance ? .visitChildren : .skipChildren
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
node.isSpecFunction ? .visitChildren : .skipChildren
}
}
}
private extension ClassDeclSyntax {
var containsInheritance: Bool {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.isNotEmpty
}
}
private extension FunctionDeclSyntax {
var isSpecFunction: Bool {
return identifier.tokenKind == .identifier("spec") &&
signature.input.parameterList.isEmpty &&
modifiers.containsOverride
}
}
private enum QuickPendingCallKind: String {
case pending
case xdescribe
case xcontext
case xit
case xitBehavesLike
}

View File

@ -1,98 +0,0 @@
import SwiftSyntax
struct StrongIBOutletRule: ConfigurationProviderRule, SwiftSyntaxCorrectableRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "strong_iboutlet",
name: "Strong IBOutlet",
description: "@IBOutlets shouldn't be declared as weak",
kind: .lint,
nonTriggeringExamples: [
wrapExample("@IBOutlet var label: UILabel?"),
wrapExample("weak var label: UILabel?")
],
triggeringExamples: [
wrapExample("@IBOutlet ↓weak var label: UILabel?"),
wrapExample("@IBOutlet ↓unowned var label: UILabel!"),
wrapExample("@IBOutlet ↓weak var textField: UITextField?")
],
corrections: [
wrapExample("@IBOutlet ↓weak var label: UILabel?"):
wrapExample("@IBOutlet var label: UILabel?"),
wrapExample("@IBOutlet ↓unowned var label: UILabel!"):
wrapExample("@IBOutlet var label: UILabel!"),
wrapExample("@IBOutlet ↓weak var textField: UITextField?"):
wrapExample("@IBOutlet var textField: UITextField?")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension StrongIBOutletRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: VariableDeclSyntax) {
if let violationPosition = node.violationPosition {
violations.append(violationPosition)
}
}
}
private final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
guard let violationPosition = node.violationPosition,
let weakOrUnownedModifier = node.weakOrUnownedModifier,
let modifiers = node.modifiers,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
let newModifiers = ModifierListSyntax(modifiers.filter { $0 != weakOrUnownedModifier })
let newNode = node.with(\.modifiers, newModifiers)
correctionPositions.append(violationPosition)
return super.visit(newNode)
}
}
}
private extension VariableDeclSyntax {
var violationPosition: AbsolutePosition? {
guard let keyword = weakOrUnownedKeyword, isIBOutlet else {
return nil
}
return keyword.positionAfterSkippingLeadingTrivia
}
var weakOrUnownedKeyword: TokenSyntax? {
weakOrUnownedModifier?.name
}
}
private func wrapExample(_ text: String, file: StaticString = #file, line: UInt = #line) -> Example {
return Example("""
class ViewController: UIViewController {
\(text)
}
""", file: file, line: line)
}

View File

@ -1,171 +0,0 @@
import Foundation
import SwiftSyntax
struct TestCaseAccessibilityRule: SwiftSyntaxRule, OptInRule,
ConfigurationProviderRule, SubstitutionCorrectableRule {
var configuration = TestCaseAccessibilityConfiguration()
static let description = RuleDescription(
identifier: "test_case_accessibility",
name: "Test Case Accessibility",
description: "Test cases should only contain private non-test members",
kind: .lint,
nonTriggeringExamples: TestCaseAccessibilityRuleExamples.nonTriggeringExamples,
triggeringExamples: TestCaseAccessibilityRuleExamples.triggeringExamples,
corrections: TestCaseAccessibilityRuleExamples.corrections
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(allowedPrefixes: configuration.allowedPrefixes, testParentClasses: configuration.testParentClasses)
}
func violationRanges(in file: SwiftLintFile) -> [NSRange] {
makeVisitor(file: file)
.walk(tree: file.syntaxTree, handler: \.violations)
.compactMap {
file.stringView.NSRange(start: $0.position, end: $0.position)
}
}
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
(violationRange, "private ")
}
}
private extension TestCaseAccessibilityRule {
final class Visitor: ViolationsSyntaxVisitor {
private let allowedPrefixes: Set<String>
private let testParentClasses: Set<String>
init(allowedPrefixes: Set<String>, testParentClasses: Set<String>) {
self.allowedPrefixes = allowedPrefixes
self.testParentClasses = testParentClasses
super.init(viewMode: .sourceAccurate)
}
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: ClassDeclSyntax) {
guard !testParentClasses.isDisjoint(with: node.inheritedTypes) else {
return
}
violations.append(
contentsOf: XCTestClassVisitor(allowedPrefixes: allowedPrefixes)
.walk(tree: node.memberBlock, handler: \.violations)
)
}
}
final class XCTestClassVisitor: ViolationsSyntaxVisitor {
private let allowedPrefixes: Set<String>
init(allowedPrefixes: Set<String>) {
self.allowedPrefixes = allowedPrefixes
super.init(viewMode: .sourceAccurate)
}
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: VariableDeclSyntax) {
guard !node.modifiers.isPrivateOrFileprivate,
!XCTestHelpers.isXCTestVariable(node) else {
return
}
for binding in node.bindings {
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self),
case let name = pattern.identifier.text,
!allowedPrefixes.contains(where: name.hasPrefix) else {
continue
}
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
return
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
guard hasViolation(modifiers: node.modifiers, identifierToken: node.identifier),
!XCTestHelpers.isXCTestFunction(node) else {
return
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
override func visitPost(_ node: ClassDeclSyntax) {
if hasViolation(modifiers: node.modifiers, identifierToken: node.identifier) {
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: EnumDeclSyntax) {
if hasViolation(modifiers: node.modifiers, identifierToken: node.identifier) {
violations.append(node.enumKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: StructDeclSyntax) {
if hasViolation(modifiers: node.modifiers, identifierToken: node.identifier) {
violations.append(node.structKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: ActorDeclSyntax) {
if hasViolation(modifiers: node.modifiers, identifierToken: node.identifier) {
violations.append(node.actorKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: TypealiasDeclSyntax) {
if hasViolation(modifiers: node.modifiers, identifierToken: node.identifier) {
violations.append(node.typealiasKeyword.positionAfterSkippingLeadingTrivia)
}
}
private func hasViolation(modifiers: ModifierListSyntax?, identifierToken: TokenSyntax) -> Bool {
guard !modifiers.isPrivateOrFileprivate else {
return false
}
return !allowedPrefixes.contains(where: identifierToken.text.hasPrefix)
}
}
}
private extension ClassDeclSyntax {
var inheritedTypes: [String] {
inheritanceClause?.inheritedTypeCollection.compactMap { type in
type.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text
} ?? []
}
}
private enum XCTestHelpers {
private static let testVariableNames: Set = [
"allTests"
]
static func isXCTestFunction(_ function: FunctionDeclSyntax) -> Bool {
guard !function.modifiers.containsOverride else {
return true
}
return !function.modifiers.containsStaticOrClass &&
function.identifier.text.hasPrefix("test") &&
function.signature.input.parameterList.isEmpty
}
static func isXCTestVariable(_ variable: VariableDeclSyntax) -> Bool {
guard !variable.modifiers.containsOverride else {
return true
}
return
variable.modifiers.containsStaticOrClass &&
variable.bindings
.compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text }
.allSatisfy(testVariableNames.contains)
}
}

View File

@ -1,344 +0,0 @@
import SwiftSyntax
struct UnhandledThrowingTaskRule: ConfigurationProviderRule, SwiftSyntaxRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.error)
static let description = RuleDescription(
identifier: "unhandled_throwing_task",
name: "Unhandled Throwing Task",
description: """
Errors thrown inside this task are not handled, which may be unexpected. \
Handle errors inside the task, or use `try await` to access the Tasks value and handle errors. \
See this forum thread for more details: \
https://forums.swift.org/t/task-initializer-with-throwing-closure-swallows-error/56066
""",
kind: .lint,
nonTriggeringExamples: [
Example("""
Task<Void, Never> {
try await myThrowingFunction()
}
"""),
Example("""
Task {
try? await myThrowingFunction()
}
"""),
Example("""
Task {
try! await myThrowingFunction()
}
"""),
Example("""
Task<Void, String> {
let text = try myThrowingFunction()
return text
}
"""),
Example("""
Task {
do {
try myThrowingFunction()
} catch let e {
print(e)
}
}
"""),
Example("""
func someFunction() throws {
Task {
anotherFunction()
do {
try myThrowingFunction()
} catch {
print(error)
}
}
try something()
}
"""),
Example("""
let task = Task {
try await myThrowingFunction()
}
"""),
Example("""
var task = Task {
try await myThrowingFunction()
}
"""),
Example("""
try await Task {
try await myThrowingFunction()
}.value
"""),
Example("""
executor.task = Task {
try await isolatedOpen(.init(executor.asUnownedSerialExecutor()))
}
"""),
Example("""
let result = await Task {
throw CancellationError()
}.result
"""),
Example("""
func makeTask() -> Task<String, Error> {
return Task {
try await someThrowingFunction()
}
}
"""),
Example("""
func makeTask() -> Task<String, Error> {
// Implicit return
Task {
try await someThrowingFunction()
}
}
"""),
Example("""
Task {
return Result {
try someThrowingFunc()
}
}
""")
],
triggeringExamples: [
Example("""
Task {
try await myThrowingFunction()
}
"""),
Example("""
Task {
let text = try myThrowingFunction()
return text
}
"""),
Example("""
Task {
do {
try myThrowingFunction()
}
}
"""),
Example("""
Task {
do {
try myThrowingFunction()
} catch let e as FooError {
print(e)
}
}
"""),
Example("""
Task {
do {
throw FooError.bar
}
}
"""),
Example("""
Task {
throw FooError.bar
}
"""),
Example("""
Task<_, _> {
throw FooError.bar
}
"""),
Example("""
Task<Void,_> {
throw FooError.bar
}
"""),
Example("""
Task {
do {
try foo()
} catch {
try bar()
}
}
"""),
Example("""
Task {
do {
try foo()
} catch {
throw BarError()
}
}
"""),
Example("""
func doTask() {
Task {
try await someThrowingFunction()
}
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension UnhandledThrowingTaskRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
if node.hasViolation {
violations.append(node.calledExpression.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension FunctionCallExprSyntax {
var hasViolation: Bool {
isTaskWithImplicitErrorType &&
doesThrow &&
!(isAssigned || isValueOrResultAccessed || isReturnValue)
}
var isTaskWithImplicitErrorType: Bool {
if let typeIdentifier = calledExpression.as(IdentifierExprSyntax.self),
typeIdentifier.identifier.text == "Task" {
return true
}
if let specializedExpression = calledExpression.as(SpecializeExprSyntax.self),
let typeIdentifier = specializedExpression.expression.as(IdentifierExprSyntax.self),
typeIdentifier.identifier.text == "Task",
let lastGeneric = specializedExpression.genericArgumentClause
.arguments.last?.argumentType.as(SimpleTypeIdentifierSyntax.self),
lastGeneric.typeName == "_" {
return true
}
return false
}
var isAssigned: Bool {
guard let parent else {
return false
}
if parent.is(InitializerClauseSyntax.self) {
return true
}
if let list = parent.as(ExprListSyntax.self),
list.contains(where: { $0.is(AssignmentExprSyntax.self) }) {
return true
}
return false
}
var isValueOrResultAccessed: Bool {
guard let parent = parent?.as(MemberAccessExprSyntax.self) else {
return false
}
return parent.name.text == "value" || parent.name.text == "result"
}
var doesThrow: Bool {
ThrowsVisitor(viewMode: .sourceAccurate)
.walk(tree: self, handler: \.doesThrow)
}
}
/// If the `doesThrow` property is true after visiting, then this node throws an error that is "unhandled."
/// Try statements inside a `do` with a `catch` that handles all errors will not be marked as throwing.
private final class ThrowsVisitor: SyntaxVisitor {
var doesThrow = false
override func visit(_ node: DoStmtSyntax) -> SyntaxVisitorContinueKind {
// No need to continue traversing if we already throw.
if doesThrow {
return .skipChildren
}
// If there are no catch clauses, visit children to see if there are any try expressions.
guard let lastCatchClause = node.catchClauses?.last else {
return .visitChildren
}
let catchItems = lastCatchClause.catchItems ?? []
// If we have a value binding pattern, only an IdentifierPatternSyntax will catch
// any error; if it's not an IdentifierPatternSyntax, we need to visit children.
if let pattern = catchItems.last?.pattern?.as(ValueBindingPatternSyntax.self),
!pattern.valuePattern.is(IdentifierPatternSyntax.self) {
return .visitChildren
}
// Check the catch clause tree for unhandled throws.
if ThrowsVisitor(viewMode: .sourceAccurate).walk(tree: lastCatchClause, handler: \.doesThrow) {
doesThrow = true
}
// We don't need to visit children of the `do` node, since all errors are handled by the catch.
return .skipChildren
}
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
// No need to continue traversing if we already throw.
if doesThrow {
return .skipChildren
}
// Result initializers with trailing closures handle thrown errors.
if let typeIdentifier = node.calledExpression.as(IdentifierExprSyntax.self),
typeIdentifier.identifier.text == "Result",
node.trailingClosure != nil {
return .skipChildren
}
return .visitChildren
}
override func visitPost(_ node: TryExprSyntax) {
if node.questionOrExclamationMark == nil {
doesThrow = true
}
}
override func visitPost(_ node: ThrowStmtSyntax) {
doesThrow = true
}
}
private extension SyntaxProtocol {
var isExplicitReturnValue: Bool {
parent?.is(ReturnStmtSyntax.self) == true
}
var isImplicitReturnValue: Bool {
// 4th parent: FunctionDecl
// 3rd parent: | CodeBlock
// 2nd parent: | CodeBlockItemList
// 1st parent: | CodeBlockItem
// Current node: | FunctionDeclSyntax
guard
let parentFunctionDecl = parent?.parent?.parent?.parent?.as(FunctionDeclSyntax.self),
parentFunctionDecl.body?.statements.count == 1,
parentFunctionDecl.signature.output != nil
else {
return false
}
return true
}
var isReturnValue: Bool {
isExplicitReturnValue || isImplicitReturnValue
}
}

Some files were not shown because too many files have changed in this diff Show More