Compare commits
1 Commits
main
...
marcelo/ib
Author | SHA1 | Date |
---|---|---|
![]() |
a999c78155 |
|
@ -1 +0,0 @@
|
|||
.build
|
6
.bazelrc
6
.bazelrc
|
@ -1,14 +1,8 @@
|
|||
common --enable_bzlmod
|
||||
|
||||
try-import %workspace%/ci.bazelrc
|
||||
try-import %workspace%/user.bazelrc
|
||||
|
||||
build --macos_minimum_os=12.0 --host_macos_minimum_os=12.0
|
||||
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 \
|
||||
--compilation_mode=opt \
|
||||
|
|
|
@ -1 +1 @@
|
|||
6.2.0
|
||||
5.3.0
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
fixedReleaser:
|
||||
login: jpsim
|
||||
email: jp@jpsim.com
|
|
@ -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": {}
|
||||
}
|
|
@ -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'
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"url": "https://github.com/realm/SwiftLint/releases/download/{TAG}/bazel.tar.gz",
|
||||
"integrity": "",
|
||||
"strip_prefix": ""
|
||||
}
|
|
@ -5,25 +5,17 @@ steps:
|
|||
- bazel build :swiftlint
|
||||
- echo "+++ Test"
|
||||
- bazel test --test_output=errors //Tests/...
|
||||
- label: "SwiftPM"
|
||||
commands:
|
||||
- echo "+++ Test"
|
||||
- swift test --parallel -Xswiftc -DDISABLE_FOCUSED_EXAMPLES
|
||||
- label: "Danger"
|
||||
commands:
|
||||
- echo "--- Install Bundler"
|
||||
- gem install bundler
|
||||
- echo "--- Bundle Install"
|
||||
- bundle install
|
||||
- echo "--- Build Danger"
|
||||
- bazel build //tools:danger
|
||||
- echo "+++ Run Danger"
|
||||
- bundle exec danger --verbose
|
||||
- label: "TSan Tests"
|
||||
- ./bazel-bin/tools/danger --verbose
|
||||
- label: "Analyze"
|
||||
commands:
|
||||
- echo "+++ Analyze"
|
||||
- bazel test -c opt --test_output=streamed --test_timeout=1800 --spawn_strategy=local analyze
|
||||
- label: "TSan"
|
||||
commands:
|
||||
- echo "+++ Test"
|
||||
- bazel test --test_output=errors --build_tests_only --features=tsan --test_timeout=1000 //Tests/...
|
||||
- label: "Sourcery"
|
||||
commands:
|
||||
- echo "+++ Run Sourcery"
|
||||
- make --always-make sourcery
|
||||
- echo "+++ Diff Files"
|
||||
- git diff --quiet HEAD
|
||||
- bazel test --xcode_version_config=//bazel:xcode_config --test_output=streamed --build_tests_only --features=tsan --test_timeout=600 //Tests/...
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
*
|
||||
!Plugins
|
||||
!Source
|
||||
!Tests
|
||||
!Package.*
|
||||
|
|
|
@ -9,10 +9,10 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Extract DOCKER_TAG using tag name
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
|
@ -21,7 +22,6 @@ xcuserdata
|
|||
*.moved-aside
|
||||
*.xcuserstate
|
||||
*.xcscmblueprint
|
||||
default.profraw
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module: SwiftLintCore
|
||||
module: SwiftLintFramework
|
||||
author: JP Simard, SwiftLint Contributors
|
||||
author_url: https://jpsim.com
|
||||
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
|
||||
theme: fullwidth
|
||||
clean: true
|
||||
copyright: '© 2023 [JP Simard](https://jpsim.com) under MIT.'
|
||||
copyright: '© 2020 [JP Simard](https://jpsim.com) under MIT.'
|
||||
|
||||
documentation: rule_docs/*.md
|
||||
hide_unlisted_documentation: true
|
||||
custom_categories_unlisted_prefix: ''
|
||||
exclude:
|
||||
# TODO: Document extensions
|
||||
- Source/SwiftLintCore/Extensions/*.swift
|
||||
- Source/SwiftLintFramework/Rules/**/*.swift
|
||||
custom_categories:
|
||||
- name: Rules
|
||||
children:
|
||||
|
@ -23,7 +22,7 @@ custom_categories:
|
|||
children:
|
||||
- CSVReporter
|
||||
- CheckstyleReporter
|
||||
- CodeClimateReporter
|
||||
- CodeclimateReporter
|
||||
- EmojiReporter
|
||||
- GitHubActionsLoggingReporter
|
||||
- HTMLReporter
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
@testable import SwiftLintBuiltInRules
|
||||
@_spi(TestHelper)
|
||||
@testable import SwiftLintCore
|
||||
import SwiftLintTestHelpers
|
||||
import SwiftLintFramework
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable:next blanket_disable_command
|
||||
// swiftlint:disable file_length single_test_class type_name
|
||||
|
||||
{% for rule in types.structs %}
|
||||
{% if rule.name|hasSuffix:"Rule" %}
|
||||
class {{ rule.name }}GeneratedTests: SwiftLintTestCase {
|
||||
class {{ rule.name }}GeneratedTests: XCTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule({{ rule.name }}.description)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
/// 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 %}
|
||||
{% endfor %}]
|
||||
{% endfor %}] + extraRules())
|
|
@ -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 %}
|
||||
]
|
132
.swiftlint.yml
132
.swiftlint.yml
|
@ -1,5 +1,4 @@
|
|||
included:
|
||||
- Plugins
|
||||
- Source
|
||||
- Tests
|
||||
excluded:
|
||||
|
@ -8,55 +7,75 @@ analyzer_rules:
|
|||
- unused_declaration
|
||||
- unused_import
|
||||
opt_in_rules:
|
||||
- all
|
||||
disabled_rules:
|
||||
- anonymous_argument_in_multiline_closure
|
||||
- anyobject_protocol
|
||||
- closure_body_length
|
||||
- conditional_returns_on_newline
|
||||
- convenience_type
|
||||
- discouraged_optional_collection
|
||||
- explicit_acl
|
||||
- explicit_enum_raw_value
|
||||
- explicit_top_level_acl
|
||||
- explicit_type_interface
|
||||
- file_types_order
|
||||
- force_unwrapping
|
||||
- function_default_parameter_at_end
|
||||
- implicit_return
|
||||
- implicitly_unwrapped_optional
|
||||
- indentation_width
|
||||
- inert_defer
|
||||
- missing_docs
|
||||
- multiline_arguments
|
||||
- multiline_arguments_brackets
|
||||
- multiline_function_chains
|
||||
- multiline_literal_brackets
|
||||
- multiline_parameters
|
||||
- multiline_parameters_brackets
|
||||
- no_extension_access_modifier
|
||||
- no_fallthrough_only
|
||||
- no_grouping_extension
|
||||
- no_magic_numbers
|
||||
- prefer_nimble
|
||||
- array_init
|
||||
- attributes
|
||||
- closure_end_indentation
|
||||
- closure_spacing
|
||||
- collection_alignment
|
||||
- contains_over_filter_count
|
||||
- contains_over_filter_is_empty
|
||||
- contains_over_first_not_nil
|
||||
- contains_over_range_nil_comparison
|
||||
- discouraged_none_name
|
||||
- discouraged_object_literal
|
||||
- empty_collection_literal
|
||||
- empty_count
|
||||
- empty_string
|
||||
- empty_xctest_method
|
||||
- enum_case_associated_values_count
|
||||
- explicit_init
|
||||
- extension_access_modifier
|
||||
- fallthrough
|
||||
- fatal_error_message
|
||||
- file_header
|
||||
- file_name
|
||||
- first_where
|
||||
- flatmap_over_map_reduce
|
||||
- identical_operands
|
||||
- joined_default_parameter
|
||||
- last_where
|
||||
- legacy_multiple
|
||||
- literal_expression_end_indentation
|
||||
- lower_acl_than_parent
|
||||
- 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
|
||||
- prefixed_toplevel_constant
|
||||
- redundant_self_in_closure
|
||||
- required_deinit
|
||||
- self_binding
|
||||
- sorted_enum_cases
|
||||
- strict_fileprivate
|
||||
- superfluous_else
|
||||
- switch_case_on_newline
|
||||
- todo
|
||||
- trailing_closure
|
||||
- type_contents_order
|
||||
- unused_capture_list
|
||||
- vertical_whitespace_between_cases
|
||||
- prefer_self_type_over_type_of_self
|
||||
- private_action
|
||||
- private_outlet
|
||||
- prohibited_interface_builder
|
||||
- prohibited_super_call
|
||||
- quick_discouraged_call
|
||||
- quick_discouraged_focused_test
|
||||
- quick_discouraged_pending_test
|
||||
- reduce_into
|
||||
- redundant_nil_coalescing
|
||||
- redundant_type_annotation
|
||||
- return_value_from_void_function
|
||||
- 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:
|
||||
excluded:
|
||||
- id
|
||||
|
@ -65,27 +84,15 @@ number_separator:
|
|||
minimum_length: 5
|
||||
file_name:
|
||||
excluded:
|
||||
- Exports.swift
|
||||
- GeneratedTests.swift
|
||||
- SwiftSyntax+SwiftLint.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:
|
||||
rule_id:
|
||||
included: Source/SwiftLintBuiltInRules/Rules/.+/\w+\.swift
|
||||
included: Source/SwiftLintFramework/Rules/.+/\w+\.swift
|
||||
name: Rule ID
|
||||
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
|
||||
fatal_error:
|
||||
name: Fatal Error
|
||||
|
@ -105,4 +112,3 @@ custom_rules:
|
|||
unused_import:
|
||||
always_keep_imports:
|
||||
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
|
||||
- SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused
|
||||
|
|
127
BUILD
127
BUILD
|
@ -1,78 +1,39 @@
|
|||
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_universal_binary")
|
||||
load(
|
||||
"@build_bazel_rules_swift//swift:swift.bzl",
|
||||
"swift_binary",
|
||||
"swift_library",
|
||||
)
|
||||
load(
|
||||
"@rules_xcodeproj//xcodeproj:defs.bzl",
|
||||
"@com_github_buildbuddy_io_rules_xcodeproj//xcodeproj:xcodeproj.bzl",
|
||||
"xcode_schemes",
|
||||
"xcodeproj",
|
||||
)
|
||||
|
||||
# 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(
|
||||
name = "SwiftLintFramework",
|
||||
srcs = glob(
|
||||
["Source/SwiftLintFramework/**/*.swift"],
|
||||
),
|
||||
exclude = ["Source/SwiftLintFramework/Rules/ExcludedFromBazel/ExtraRules.swift"],
|
||||
) + ["@swiftlint_extra_rules//:extra_rules"],
|
||||
module_name = "SwiftLintFramework",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":SwiftLintBuiltInRules",
|
||||
":SwiftLintCore",
|
||||
":SwiftLintExtraRules",
|
||||
],
|
||||
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
|
||||
"@com_github_apple_swift_syntax//:SwiftSyntax",
|
||||
"@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(
|
||||
name = "swiftlint.library",
|
||||
swift_binary(
|
||||
name = "swiftlint",
|
||||
srcs = glob(["Source/swiftlint/**/*.swift"]),
|
||||
module_name = "swiftlint",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":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
|
||||
|
||||
filegroup(
|
||||
|
@ -135,8 +61,6 @@ filegroup(
|
|||
srcs = [
|
||||
"BUILD",
|
||||
"LICENSE",
|
||||
"MODULE.bazel",
|
||||
"//:DyldWarningWorkaroundSources",
|
||||
"//:LintInputs",
|
||||
"//Tests:BUILD",
|
||||
"//bazel:release_files",
|
||||
|
@ -168,37 +92,35 @@ shasum -a 256 "$${outs[0]}" > "$${outs[1]}"
|
|||
|
||||
xcodeproj(
|
||||
name = "xcodeproj",
|
||||
build_mode = "bazel",
|
||||
project_name = "SwiftLint",
|
||||
schemes = [
|
||||
xcode_schemes.scheme(
|
||||
name = "SwiftLint",
|
||||
launch_action = xcode_schemes.launch_action(
|
||||
"swiftlint",
|
||||
args = [
|
||||
"--progress",
|
||||
],
|
||||
),
|
||||
launch_action = xcode_schemes.launch_action("//:swiftlint"),
|
||||
test_action = xcode_schemes.test_action([
|
||||
"//Tests:CLITests",
|
||||
"//Tests:SwiftLintFrameworkTests",
|
||||
"//Tests:GeneratedTests",
|
||||
"//Tests:IntegrationTests",
|
||||
"//Tests:ExtraRulesTests",
|
||||
]),
|
||||
),
|
||||
],
|
||||
top_level_targets = [
|
||||
"//:swiftlint",
|
||||
"//Tests:CLITests",
|
||||
"//Tests:SwiftLintFrameworkTests",
|
||||
"//Tests:GeneratedTests",
|
||||
"//Tests:IntegrationTests",
|
||||
"//Tests:ExtraRulesTests",
|
||||
],
|
||||
)
|
||||
|
||||
# Analyze
|
||||
|
||||
filegroup(
|
||||
name = "SourceAndTestFiles",
|
||||
srcs = glob([
|
||||
"Source/**",
|
||||
"Tests/SwiftLintFrameworkTests/**",
|
||||
]),
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "analyze",
|
||||
srcs = ["//tools:test-analyze.sh"],
|
||||
|
@ -206,6 +128,7 @@ sh_test(
|
|||
"Package.resolved",
|
||||
"Package.swift",
|
||||
":LintInputs",
|
||||
":SourceAndTestFiles",
|
||||
":swiftlint",
|
||||
],
|
||||
)
|
||||
|
|
865
CHANGELOG.md
865
CHANGELOG.md
|
@ -2,557 +2,6 @@
|
|||
|
||||
#### 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.
|
||||
[JP Simard](https://github.com/jpsim)
|
||||
|
||||
|
@ -563,12 +12,7 @@
|
|||
|
||||
* 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.
|
||||
[JP Simard](https://github.com/jpsim)
|
||||
|
||||
* 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)
|
||||
[JP Simard](https://github.com/jpims)
|
||||
|
||||
#### Experimental
|
||||
|
||||
|
@ -585,178 +29,40 @@
|
|||
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
|
||||
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)
|
||||
|
||||
* 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
|
||||
to exclude initializers without any parameters.
|
||||
[Marcelo Fabri](https://github.com/marcelofabri)
|
||||
[#4107](https://github.com/realm/SwiftLint/issues/4107)
|
||||
|
||||
* Add new `ns_number_init_as_function_reference` rule to catch `NSNumber.init`
|
||||
and `NSDecimalNumber.init` being used as function references since it
|
||||
can cause the wrong initializer to be used, causing crashes. See
|
||||
https://github.com/apple/swift/issues/51036 for more info.
|
||||
* Rewrite some rules with SwiftSyntax, fixing some false positives and catching
|
||||
more violations:
|
||||
- `anyobject_protocol`
|
||||
- `array_init`
|
||||
- `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
|
||||
View has a tap gesture added to it without having the button or
|
||||
|
@ -764,15 +70,11 @@
|
|||
[Ryan Cole](https://github.com/rcole34)
|
||||
|
||||
* 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
|
||||
messages.
|
||||
[JP Simard](https://github.com/jpsim)
|
||||
|
||||
* 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)
|
||||
[JP Simard](https://github.com/jpims)
|
||||
|
||||
* Add `library_content_provider` file type to `file_types_order` rule
|
||||
to allow `LibraryContentProvider` to be ordered independent from `main_type`.
|
||||
|
@ -783,86 +85,11 @@
|
|||
[Martin Redington](https://github.com/mildm8nnered)
|
||||
[#4200](https://github.com/realm/SwiftLint/issues/4200)
|
||||
|
||||
* Add a new `shorthand_optional_binding` opt-in rule that triggers in Swift 5.7
|
||||
when a shadowing optional binding is created in an `if` or `guard` statement.
|
||||
* Add a new `if_let_shadowing` opt-in rule that triggers in Swift 5.7 when a
|
||||
shadowing optional binding is created in an if- or guard-statement.
|
||||
[SimplyDanny](https://github.com/SimplyDanny)
|
||||
[#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
|
||||
|
||||
* Respect `validates_start_with_lowercase` option when linting function names.
|
||||
|
@ -889,27 +116,6 @@
|
|||
[SimplyDanny](https://github.com/SimplyDanny)
|
||||
[#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
|
||||
|
||||
_Note: The default branch for the SwiftLint git repository was renamed from
|
||||
|
@ -1194,6 +400,15 @@ macOS < 12.
|
|||
|
||||
#### 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
|
||||
`TypesafeArrayInitRule` with identifier `typesafe_array_init` that
|
||||
avoids the false positives present in the lint rule.
|
||||
|
|
|
@ -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
|
||||
|
||||
All changes, no matter how trivial, must be done via pull request. Commits
|
||||
|
@ -64,7 +58,7 @@ $ make docker_test
|
|||
|
||||
## 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.
|
||||
|
||||
|
@ -104,11 +98,11 @@ configuration object via the `configuration` property:
|
|||
* If none of the provided `RuleConfiguration`s are applicable, you can create one
|
||||
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,
|
||||
[`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,
|
||||
[`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:
|
||||
|
||||
``` yaml
|
||||
|
@ -177,8 +171,15 @@ To bring up a new Buildkite worker from MacStadium:
|
|||
1. Update macOS to the latest version
|
||||
1. Install Homebrew: https://brew.sh
|
||||
1. Install Buildkite agent and other tools via Homebrew:
|
||||
`brew install aria2 bazelisk htop buildkite/buildkite/buildkite-agent robotsandpencils/made/xcodes`
|
||||
1. Install latest Xcode version: `xcodes update && xcodes install 14.0.0`
|
||||
1. Add `DANGER_GITHUB_API_TOKEN` and `HOME` to `/opt/homebrew/etc/buildkite-agent/hooks/environment`
|
||||
`brew install buildkite/buildkite/buildkite-agent aria2 htop`
|
||||
1. Install the xcodes CLI by downloading the zip and moving it to `/usr/local/bin`:
|
||||
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` /
|
||||
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`
|
||||
|
|
|
@ -14,9 +14,9 @@ modified_files = git.modified_files + git.added_files
|
|||
# including in a CHANGELOG for example
|
||||
has_app_changes = !modified_files.grep(/Source/).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_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
|
||||
if !modified_files.include?('CHANGELOG.md') && has_app_changes
|
||||
|
@ -51,8 +51,7 @@ end
|
|||
|
||||
file = Tempfile.new('violations')
|
||||
|
||||
force_flag = has_danger_changes ? "--force" : ""
|
||||
Open3.popen3("tools/oss-check -v #{force_flag} 2> #{file.path}") do |_, stdout, _, _|
|
||||
Open3.popen3("tools/oss-check -v 2> #{file.path}") do |_, stdout, _, _|
|
||||
while char = stdout.getc
|
||||
print char
|
||||
end
|
||||
|
|
13
Dockerfile
13
Dockerfile
|
@ -9,16 +9,18 @@ RUN apt-get update && apt-get install -y \
|
|||
libxml2-dev \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
WORKDIR /workdir/
|
||||
COPY Plugins Plugins/
|
||||
COPY Source Source/
|
||||
COPY Tests Tests/
|
||||
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"
|
||||
RUN swift build $SWIFT_FLAGS --product swiftlint
|
||||
RUN swift build $SWIFT_FLAGS
|
||||
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
|
||||
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/swift/linux/libBlocksRuntime.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
|
||||
|
||||
RUN swiftlint version
|
||||
RUN echo "_ = 0" | swiftlint --use-stdin
|
||||
|
||||
CMD ["swiftlint"]
|
||||
|
|
80
Gemfile.lock
80
Gemfile.lock
|
@ -1,14 +1,15 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.6)
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
activesupport (7.0.4.3)
|
||||
activesupport (6.1.6.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.4)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
|
@ -19,15 +20,15 @@ GEM
|
|||
cork
|
||||
nap
|
||||
open4 (~> 1.3)
|
||||
cocoapods (1.12.1)
|
||||
cocoapods (1.11.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.12.1)
|
||||
cocoapods-core (= 1.11.3)
|
||||
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-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)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
|
@ -35,10 +36,10 @@ GEM
|
|||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
ruby-macho (>= 1.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.12.1)
|
||||
activesupport (>= 5.0, < 8)
|
||||
cocoapods-core (1.11.3)
|
||||
activesupport (>= 5.0, < 7)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
|
@ -57,42 +58,61 @@ GEM
|
|||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.1.10)
|
||||
cork (0.3.0)
|
||||
colored2 (~> 3.1)
|
||||
danger (9.2.0)
|
||||
danger (8.6.1)
|
||||
claide (~> 1.0)
|
||||
claide-plugins (>= 0.9.2)
|
||||
colored2 (~> 3.1)
|
||||
cork (~> 0.1)
|
||||
faraday (>= 0.9.0, < 3.0)
|
||||
faraday (>= 0.9.0, < 2.0)
|
||||
faraday-http-cache (~> 2.0)
|
||||
git (~> 1.7)
|
||||
kramdown (~> 2.3)
|
||||
kramdown-parser-gfm (~> 1.0)
|
||||
no_proxy_fix
|
||||
octokit (~> 5.0)
|
||||
octokit (~> 4.7)
|
||||
terminal-table (>= 1, < 4)
|
||||
escape (0.0.4)
|
||||
ethon (0.16.0)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
faraday (2.7.4)
|
||||
faraday-net_http (>= 2.0, < 3.1)
|
||||
faraday (1.10.2)
|
||||
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)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-http-cache (2.4.1)
|
||||
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)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
git (1.18.0)
|
||||
git (1.12.0)
|
||||
addressable (~> 2.8)
|
||||
rchardet (~> 1.8)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jazzy (0.14.3)
|
||||
jazzy (0.14.2)
|
||||
cocoapods (~> 1.5)
|
||||
mustache (~> 1.1)
|
||||
open4 (~> 1.3)
|
||||
|
@ -102,26 +122,27 @@ GEM
|
|||
sassc (~> 2.1)
|
||||
sqlite3 (~> 1.3)
|
||||
xcinvoke (~> 0.3.0)
|
||||
json (2.6.3)
|
||||
json (2.6.2)
|
||||
kramdown (2.4.0)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liferaft (0.0.6)
|
||||
minitest (5.18.0)
|
||||
minitest (5.16.3)
|
||||
molinillo (0.8.0)
|
||||
multipart-post (2.2.3)
|
||||
mustache (1.1.1)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
no_proxy_fix (0.1.2)
|
||||
octokit (5.6.1)
|
||||
octokit (4.25.1)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
open4 (1.3.4)
|
||||
public_suffix (4.0.7)
|
||||
rchardet (1.8.0)
|
||||
redcarpet (3.6.0)
|
||||
redcarpet (3.5.1)
|
||||
rexml (3.2.5)
|
||||
rouge (3.30.0)
|
||||
ruby-macho (2.5.1)
|
||||
|
@ -131,14 +152,14 @@ GEM
|
|||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
sqlite3 (1.6.2-arm64-darwin)
|
||||
sqlite3 (1.4.4)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.6)
|
||||
tzinfo (2.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.4.2)
|
||||
unicode-display_width (2.2.0)
|
||||
xcinvoke (0.3.0)
|
||||
liferaft (~> 0.0.6)
|
||||
xcodeproj (1.22.0)
|
||||
|
@ -148,10 +169,11 @@ GEM
|
|||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-21
|
||||
arm64-darwin-22
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods
|
||||
|
@ -159,4 +181,4 @@ DEPENDENCIES
|
|||
jazzy
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.12
|
||||
2.3.8
|
||||
|
|
27
MODULE.bazel
27
MODULE.bazel
|
@ -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")
|
69
Makefile
69
Makefile
|
@ -7,7 +7,10 @@ XCODEFLAGS=-scheme 'swiftlint' \
|
|||
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
|
||||
|
||||
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=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint
|
||||
|
||||
|
@ -30,19 +33,15 @@ VERSION_STRING=$(shell ./tools/get-version)
|
|||
|
||||
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
|
||||
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
|
||||
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
|
||||
Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
|
||||
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
|
||||
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
|
||||
|
||||
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil
|
||||
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery
|
||||
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.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
|
||||
Tests/SwiftLintFrameworkTests/GeneratedTests.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/GeneratedTests.stencil
|
||||
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
|
||||
mv .sourcery/GeneratedTests.generated.swift Tests/SwiftLintFrameworkTests/GeneratedTests.swift
|
||||
|
||||
test: clean_xcode
|
||||
$(BUILD_TOOL) $(XCODEFLAGS) test
|
||||
|
@ -63,18 +62,25 @@ analyze_autocorrect: write_xcodebuild_log
|
|||
clean:
|
||||
rm -f "$(OUTPUT_PACKAGE)"
|
||||
rm -rf "$(TEMPORARY_FOLDER)"
|
||||
rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256"
|
||||
rm -f "./*.zip"
|
||||
swift package clean
|
||||
|
||||
clean_xcode:
|
||||
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
|
||||
|
||||
build:
|
||||
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)"
|
||||
bazel build --config release universal_swiftlint
|
||||
$(eval SWIFTLINT_BINARY := $(shell bazel cquery --config release --output=files universal_swiftlint))
|
||||
mv "$(SWIFTLINT_BINARY)" "$(SWIFTLINT_EXECUTABLE)"
|
||||
chmod +w "$(SWIFTLINT_EXECUTABLE)"
|
||||
build_x86_64:
|
||||
swift build $(SWIFT_BUILD_FLAGS) --arch x86_64
|
||||
|
||||
build_arm64:
|
||||
swift build $(SWIFT_BUILD_FLAGS) --arch arm64
|
||||
|
||||
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)"
|
||||
|
||||
build_with_disable_sandbox:
|
||||
|
@ -117,11 +123,10 @@ zip_linux: docker_image
|
|||
|
||||
zip_linux_release:
|
||||
$(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"
|
||||
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
|
||||
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
|
||||
gh release upload "$(VERSION_STRING)" swiftlint_linux.zip
|
||||
|
||||
package: build
|
||||
$(eval PACKAGE_ROOT := $(shell mktemp -d))
|
||||
|
@ -137,11 +142,13 @@ bazel_release:
|
|||
bazel build :release
|
||||
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 build --platform linux/amd64 --force-rm --tag swiftlint .
|
||||
|
||||
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 run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset
|
||||
|
@ -152,8 +159,8 @@ display_compilation_time:
|
|||
|
||||
publish:
|
||||
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
|
||||
bundle install
|
||||
bundle exec pod trunk push SwiftLint.podspec
|
||||
# Workaround for https://github.com/CocoaPods/CocoaPods/issues/11185
|
||||
arch -arch x86_64 pod trunk push SwiftLint.podspec
|
||||
|
||||
docs:
|
||||
swift run swiftlint generate-docs
|
||||
|
@ -163,30 +170,18 @@ docs:
|
|||
get_version:
|
||||
@echo "$(VERSION_STRING)"
|
||||
|
||||
release:
|
||||
push_version:
|
||||
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
|
||||
$(error git state is not clean)
|
||||
endif
|
||||
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
|
||||
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
|
||||
@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 -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)"
|
||||
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
|
||||
git commit -a -m "release $(NEW_VERSION)"
|
||||
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
|
||||
git push origin HEAD
|
||||
git push origin $(NEW_VERSION)
|
||||
./tools/create-github-release.sh "$(NEW_VERSION)"
|
||||
make publish
|
||||
./tools/add-new-changelog-section.sh
|
||||
git commit -a -m "Add new changelog section"
|
||||
git push origin HEAD
|
||||
|
||||
%:
|
||||
@:
|
||||
|
|
|
@ -1,77 +1,70 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"identity" : "collectionconcurrencykit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
|
||||
"package": "CollectionConcurrencyKit",
|
||||
"repositoryURL": "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b4f23e24b5a1bff301efc5e70871083ca029ff95",
|
||||
"version": "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cryptoswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
|
||||
"package": "SourceKitten",
|
||||
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
|
||||
"state": {
|
||||
"revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f",
|
||||
"version" : "1.7.2"
|
||||
"branch": null,
|
||||
"revision": "b5f9bb749057dd396e93f97956bef64672bc2a04",
|
||||
"version": "0.33.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sourcekitten",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/jpsim/SourceKitten.git",
|
||||
"package": "swift-argument-parser",
|
||||
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
|
||||
"state": {
|
||||
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
|
||||
"version" : "0.34.1"
|
||||
"branch": null,
|
||||
"revision": "9f39744e025c7d377987f30b03770805dcb0bcd1",
|
||||
"version": "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||
"package": "SwiftSyntax",
|
||||
"repositoryURL": "https://github.com/apple/swift-syntax.git",
|
||||
"state": {
|
||||
"revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
|
||||
"version" : "1.2.1"
|
||||
"branch": null,
|
||||
"revision": "093e5ee151d206454e2c1950d81333c4d4a4472e",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"package": "SwiftyTextTable",
|
||||
"repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
|
||||
"version": "0.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swxmlhash",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/drmohundro/SWXMLHash.git",
|
||||
"package": "SWXMLHash",
|
||||
"repositoryURL": "https://github.com/drmohundro/SWXMLHash.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4d0f62f561458cbe1f732171e625f03195151b60",
|
||||
"version": "7.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "yams",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/jpsim/Yams.git",
|
||||
"package": "Yams",
|
||||
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
||||
"state": {
|
||||
"revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a",
|
||||
"version" : "5.0.5"
|
||||
"branch": null,
|
||||
"revision": "01835dc202670b5bb90d07f3eae41867e9ed29f6",
|
||||
"version": "5.0.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
|
|
113
Package.swift
113
Package.swift
|
@ -1,32 +1,37 @@
|
|||
// swift-tools-version:5.7
|
||||
// swift-tools-version:5.5
|
||||
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(
|
||||
name: "SwiftLint",
|
||||
platforms: [.macOS(.v12)],
|
||||
products: [
|
||||
.executable(name: "swiftlint", targets: ["swiftlint"]),
|
||||
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]),
|
||||
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])
|
||||
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")),
|
||||
.package(url: "https://github.com/apple/swift-syntax.git", exact: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"),
|
||||
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")),
|
||||
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"),
|
||||
.package(name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.1.3")),
|
||||
.package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax.git", .revision("093e5ee151d206454e2c1950d81333c4d4a4472e")),
|
||||
.package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.33.0"),
|
||||
.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/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"),
|
||||
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.7.2"))
|
||||
],
|
||||
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0")
|
||||
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.6.0"))] : []),
|
||||
targets: [
|
||||
.plugin(
|
||||
name: "SwiftLintPlugin",
|
||||
capability: .buildTool(),
|
||||
dependencies: [
|
||||
.target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),
|
||||
.target(name: "swiftlint", condition: .when(platforms: [.linux]))
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "swiftlint",
|
||||
dependencies: [
|
||||
|
@ -36,86 +41,18 @@ let package = Package(
|
|||
"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(
|
||||
name: "SwiftLintFramework",
|
||||
dependencies: [
|
||||
"SwiftLintBuiltInRules",
|
||||
"SwiftLintCore",
|
||||
"SwiftLintExtraRules"
|
||||
]
|
||||
),
|
||||
.target(name: "DyldWarningWorkaround"),
|
||||
.target(
|
||||
name: "SwiftLintTestHelpers",
|
||||
dependencies: [
|
||||
"SwiftLintFramework"
|
||||
],
|
||||
path: "Tests/SwiftLintTestHelpers"
|
||||
dependencies: frameworkDependencies
|
||||
),
|
||||
.testTarget(
|
||||
name: "SwiftLintFrameworkTests",
|
||||
dependencies: [
|
||||
"SwiftLintFramework",
|
||||
"SwiftLintTestHelpers"
|
||||
"SwiftLintFramework"
|
||||
],
|
||||
exclude: [
|
||||
"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"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
124
README.md
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
[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
|
||||
|
||||
Put this in your `MODULE.bazel`:
|
||||
|
||||
```bzl
|
||||
bazel_dep(name = "swiftlint", version = "0.50.4", repo_name = "SwiftLint")
|
||||
```
|
||||
|
||||
Or put this in your `WORKSPACE`:
|
||||
Put this in your `WORKSPACE`:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>WORKSPACE</summary>
|
||||
|
||||
```bzl
|
||||
```python
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_apple",
|
||||
sha256 = "f94e6dddf74739ef5cb30f000e13a2a613f6ebfa5e63588305a71fce8a8a9911",
|
||||
url = "https://github.com/bazelbuild/rules_apple/releases/download/1.1.3/rules_apple.1.1.3.tar.gz",
|
||||
sha256 = "36072d4f3614d309d6a703da0dfe48684ec4c65a89611aeb9590b45af7a3e592",
|
||||
url = "https://github.com/bazelbuild/rules_apple/releases/download/1.0.1/rules_apple.1.0.1.tar.gz",
|
||||
)
|
||||
|
||||
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,
|
||||
we encourage you to watch this presentation or read the transcript:
|
||||
|
||||
[](https://youtu.be/9Z1nTMTejqU)
|
||||
[](https://academy.realm.io/posts/slug-jp-simard-swiftlint/)
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
if [[ "$(uname -m)" == arm64 ]]; then
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
|
||||
export PATH="$PATH:/opt/homebrew/bin"
|
||||
if which swiftlint > /dev/null; then
|
||||
swiftlint
|
||||
else
|
||||
|
@ -192,52 +183,12 @@ If you've installed SwiftLint via CocoaPods the script should look like this:
|
|||
"${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
|
||||
Swift packages.
|
||||
|
||||
> Due to limitations with Swift Package Manager Plug-ins this is only
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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")]
|
||||
),
|
||||
```
|
||||
To integrate SwiftLint with AppCode, install
|
||||
[this plugin](https://plugins.jetbrains.com/plugin/9175) and configure
|
||||
SwiftLint's installed path in the plugin's preferences.
|
||||
The `fix` action is available via `⌥⏎`.
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
|
@ -308,7 +259,6 @@ SUBCOMMANDS:
|
|||
docs Open SwiftLint documentation website in the default web browser
|
||||
generate-docs Generates markdown documentation for all rules
|
||||
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
|
||||
version Display the current version of SwiftLint
|
||||
|
||||
|
@ -376,21 +326,12 @@ Once [installed](https://pre-commit.com/#install), add this to the
|
|||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/realm/SwiftLint
|
||||
rev: 0.50.3
|
||||
rev: 0.44.0
|
||||
hooks:
|
||||
- id: swiftlint
|
||||
```
|
||||
|
||||
Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` can be used to update to the current version.
|
||||
|
||||
SwiftLint can be configured using `entry` to apply fixes and fail on errors:
|
||||
```yaml
|
||||
- repo: https://github.com/realm/SwiftLint
|
||||
rev: 0.50.3
|
||||
hooks:
|
||||
- id: swiftlint
|
||||
entry: swiftlint --fix --strict
|
||||
```
|
||||
Adjust `rev` to the SwiftLint version of your choice.
|
||||
|
||||
## Rules
|
||||
|
||||
|
@ -401,7 +342,7 @@ continues to contribute more over time.
|
|||
You can find an updated list of rules and more information about them
|
||||
[here](https://realm.github.io/SwiftLint/rule-directory.html).
|
||||
|
||||
You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules)
|
||||
You can also check [Source/SwiftLintFramework/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintFramework/Rules)
|
||||
directory to see their implementation.
|
||||
|
||||
### Opt-In Rules
|
||||
|
@ -479,9 +420,7 @@ run SwiftLint from. The following parameters can be configured:
|
|||
Rule inclusion:
|
||||
|
||||
* `disabled_rules`: Disable rules from the default enabled set.
|
||||
* `opt_in_rules`: Enable rules that are not part of the default set. The
|
||||
special `all` identifier will enable all opt in linter rules, except the ones
|
||||
listed in `disabled_rules`.
|
||||
* `opt_in_rules`: Enable rules that are not part of the default set.
|
||||
* `only_rules`: Only the rules specified in this list will be enabled.
|
||||
Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
|
||||
* `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
|
||||
# - vertical_whitespace
|
||||
|
||||
analyzer_rules: # Rules run by `swiftlint analyze`
|
||||
- explicit_self
|
||||
|
||||
included: # paths to include during linting. `--path` is ignored if present.
|
||||
- Source
|
||||
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/ExcludedFile.swift
|
||||
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
|
||||
|
||||
# If true, SwiftLint will not fail if no lintable files are found.
|
||||
allow_zero_lintable_files: false
|
||||
analyzer_rules: # Rules run by `swiftlint analyze`
|
||||
- explicit_self
|
||||
|
||||
# configurable rules can be customized from this configuration file
|
||||
# binary rules can set their severity level
|
||||
|
@ -550,29 +485,13 @@ identifier_name:
|
|||
- id
|
||||
- URL
|
||||
- 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,
|
||||
by using `${SOME_VARIABLE}` in a string.
|
||||
|
||||
### 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
|
||||
#### Defining Custom Rules
|
||||
|
||||
You can define custom regex-based rules in your configuration file using the
|
||||
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
|
||||
`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
|
||||
|
||||
SwiftLint can automatically correct certain violations. Files on disk are
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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) 来表示源代码文件的更多精确结果。
|
||||
|
||||
|
@ -167,7 +167,7 @@ SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区
|
|||
|
||||
你可以在 [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` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。
|
||||
|
||||
|
|
11
README_KR.md
11
README_KR.md
|
@ -1,6 +1,6 @@
|
|||
# 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) 표현을 사용합니다.
|
||||
|
||||
|
@ -75,10 +75,7 @@ fi
|
|||
그 이유는, 애플 실리콘 기반 맥에서 Homebrew는 기본적으로 바이너리들을 `/opt/homebrew/bin`에 저장하기 때문입니다. SwiftLint가 어디 있는지 찾는 것을 Xcode에 알려주기 위해, build phase에서 `/opt/homebrew/bin`를 `PATH` 환경 변수에 동시에 추가하여야 합니다.
|
||||
|
||||
```bash
|
||||
if [[ "$(uname -m)" == arm64 ]]; then
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
|
||||
export PATH="$PATH:/opt/homebrew/bin"
|
||||
if which swiftlint > /dev/null; then
|
||||
swiftlint
|
||||
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`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
|
||||
|
||||
|
|
17
Releasing.md
17
Releasing.md
|
@ -7,8 +7,19 @@ For SwiftLint contributors, follow these steps to cut a release:
|
|||
* FabricSoftenerRule
|
||||
* Top Loading
|
||||
* 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
|
||||
`xcode-select`ed
|
||||
1. Release new version: `make release "0.2.0: Tumble Dry"`
|
||||
1. Wait for the Docker CI job to finish then run: `make zip_linux_release`
|
||||
`xcode-select`ed.
|
||||
1. Create the pkg installer, framework zip, portable zip,
|
||||
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,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__
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
@_exported import SwiftLintCore
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 == "]")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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("!=")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
""")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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, "")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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 } ?? []
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 {}")
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"]]
|
||||
)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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, "")
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue